Package cmdchain

import "gitlab.com/zoralab/cerigo/algo/cmdchain"

cmdchain is the chain of command software design pattern algorithm used for encapsulating processes and serialize their executions. It is also known as “chain of responsibilities” behavioral design pattern.

Likewise, it offers the ability to perform an unified functional calls across many functions. With its ability to execute the chained functions in serial manner, it allows a full and clear control over the process flow.

“chain of responsibilities” is commonly used for:

  1. delegate non-relational executions
  2. network responses
  3. process piping
  4. block-process oriented executions

Example

main function

ackage cmdchain

import (
"fmt"
)

const (
cmdGetAlternateInput = 1
)

// 0. Map your command chains before starting to write one. In this example,
//    the calculator performs as follows:
//    (i)      GetInput
//    (ii)     Add
//    (iii)    Print
//    (iv)     PrepareNext
//    (v)      GetInput
//    (vi)     Minus
//    (vii)    Print
//    (viii)   PrepareNext
//
//    It timeout or error occurs:
//    (x-i)    HandleError

// 1. build your data type to use across the chain.
type calculator struct {
aint
bint
totalint
opsstring
countint
}

// 2. build your chain of commands' command functions. Make sure that each
//    functions has a link to one another.
func HandleError(c *Chain) {
fmt.Printf("[ Sandbox ERROR ] %v\n", c.GetError())
}

func InterruptPrint(c *Chain) {
fmt.Printf("[ INFO ] chain got interrupted. servicing...\n")
}

func Add(c *Chain) {
cal := getCalculator(c)
if cal == nil {
return
}

cal.total = cal.a + cal.b
cal.ops = "+"
cal.count++

c.Run(Print)// 2.1 - running the next command
}

func Minus(c *Chain) {
cal := getCalculator(c)
if cal == nil {
return
}

cal.total = cal.a - cal.b
cal.ops = "-"
cal.count++

c.Run(Print)// 2.1 - running the next command
}

func Print(c *Chain) {
cal := getCalculator(c)
if cal == nil {
return
}

s := fmt.Sprintf("ops %v: %v %v %v = %v\n",
cal.count,
cal.a,
cal.ops,
cal.b,
cal.total)
fmt.Printf("%s", s)

c.Interrupts(InterruptPrint)// 2.2 - setting an interruptive run
c.Run(PrepareNext)// 2.1 - running the next command
}

func PrepareNext(c *Chain) {
cal := getCalculator(c)
if cal == nil {
return
}

cal.a = cal.total
cal.b = 0
cal.total = 0

if cal.ops == "-" {
c.Done()// 2.3 - notify Chain that the chain is done.
return
}

c.Run(GetInput)// 2.1 - running the next command
}

func GetInput(c *Chain) {
cal := getCalculator(c)
if cal == nil {
return
}

if cal.a == 0 {
// 2.4 - intercept next process block with a registered command
c.Intercept(cmdGetAlternateInput)
}

cal.b = 123

if cal.ops == "+" {
c.Run(Minus)// 2.1 - running the next command
} else {
c.Run(Add)// 2.1 - running the next command
}
}

func SetInitialInput(c *Chain) {
cal := getCalculator(c)
if cal == nil {
return
}

cal.a = 399

c.Run(GetInput)// 2.1 - running the next command
}

func getCalculator(c *Chain) *calculator {
cal, ok := c.GetData().(*calculator)
if ok {
return cal
}

// 2.2 - set error unto the Chain instead of running error handler
//       yourself. The Chain will automatically runs the error
//       handler as the last command.
c.SetError(fmt.Errorf("missing calculator data"))

return nil
}

// main function
func Example() {
// 3. create a Chain
c := New()

// 4. set a timeout. The unit is nanosecond.
c.SetTimeout(1 * 1000 * 1000 * 1000)// t * sec * mil * nano

// 5. set your data types
c.SetData(&calculator{
a:0,
b:0,
total:0,
})

// 6. set the error handler function
c.SetErrorHandler(HandleError)

// 7. register any higher level commands for inteception
c.Register(cmdGetAlternateInput, SetInitialInput)

// 8. Run the first command. Use Wait() on for the first command
//    to wait for entire chain to complete.
c.Run(GetInput).Wait()
}

Constants

const (
	// NanoSecond is the unit multipler for chain.Timeout
	NanoSecond = uint64(1)

	// MicroSecond is the unit mulipler for chain.Timeout
	MicroSecond = 1000 * NanoSecond

	// MilliSecond is the unit multipler for chain.Timeout
	MilliSecond = 1000 * MicroSecond

	// Second is the unit multiplier for chain.Timeout
	Second = 1000 * MilliSecond

	// Minute is the unit multiplier for chain.Timeout
	Minute = 60 * Second

	// Hour is the unit multiplier for chain.Timeout
	Hour = 60 * Minute

	// DefaultTimeout is the default chain.Timeout, which is 2
	// Minutes
	DefaultTimeout = 2 * Minute
)
const (
	// ErrorTimeout is the error message for chain timeout.
	ErrorTimeout = "timeout waiting for chain completion"

	// ErrorMissingCommand is the error message for missing
	// registered command
	ErrorMissingCommand = "missing command from chain registered list"

	// ErrorExistingCommand is the error message for command
	// already exists before registering a new one with same
	// label.
	ErrorExistingCommand = "command already exists"

	// ErrorBadLabel is the error message for command label that
	// is not allowed to use.
	ErrorBadLabel = "command label cannot be 0"
)

Chain

type Chain struct {
	// contains filtered or unexported fields
}

Chain is the structure that facilitates the chain of commands executions.

Chain is using sync.mutexes to synchonize all its data variables for safe concurrent executions. As for concurrent delegation, Chain uses Goroutines where it best suited for.

This function has private elements that require initialization. Hence, use New() function to create one instead of using the conventional structure{} method.

Func New() *Chain

New is to create a new Chain object with internal elements initialized.

Func (c *Chain) CMDExists(label int) bool

CMDExists is to check a registered label has a command

It returns:

  1. true - the label has a registered command
  2. false - the label is vacant

Func (c *Chain) Delete(label int)

Delete is to delete a registered command from the chain regardlessly and safely using mutexes.

If the command does not exists, this function does nothing.

Func (c *Chain) Done()

Done is to instruct the chain to complete and close the process chains.

Func (c *Chain) GetData() interface{}

Getdata is to obtain the saved data from the Chain data system safely using mutexes

Func (c *Chain) GetError() error

GetError is to obtain the error object from the chain safely using mutexes.

It returns:

  1. nil - no error found
  2. error - error object.

Func (c *Chain) GetTimeout() uint64

GetTimeout is to get a given timeout duration from the Chain safely using mutexes.

Func (c *Chain) Intercept(label int)

Intercept is to take over the next Run(..) with a registered command safely using mutexes.

It requires a registered command’s label as the input.

If the given label does not exist, Intercept routes the next Run(…) to error handling with ErrorMissingCommand error object.

Func (c *Chain) Interrupts(cmd func(super *Chain))

Interrupts sets an interrupted process block for an existing run safely using mutexes.

Interrupts pauses the incoming Run(…) and execute the given interrupting cmd process based on First-In-First-Out (FIFO) policy. Once all interrupt commands are executed, it resumes the Run(…) back to the original process block.

While being interruptive, the interruptive command’s content must be independent and safe (e.g. thread-safe) at any given point of the chain processes. Otherwise, the chain can behaves unpredictably.

If cmd is nil, this function does nothing and return early.

Func (c *Chain) Register(label int, cmd func(super *Chain))

Register is to save a command into the chain for process interception.

If the command is already exist or the given label is 0, this function panics, prompting an immediate attention for fixing.

The label can be any integer number (both negative or positive) except 0, which is a reserved label for “no interception”.

Func (c *Chain) Reset()

Reset is to set the chain back to its initial condition for the next run.

Func (c *Chain) Run(x func(super *Chain)) *Chain

Run is to execute a given function.

If the Chain has an error value, this function will execute its available error handler and ends the chain.

Run executes commands based on pre-defined priorities:

  1. priority #1 - error handling and subsequently stop the chain
  2. priority #2 - process any pending interrupts then resumes the run
  3. priority #3 - process any pending pre-registered commands
  4. priority #4 - process the given input command

It returns:

  1. itself - for optional next functional call like Wait()

Func (c *Chain) SetData(d interface{})

SetData is to save a given data into the Chain data system safely using mutexes.

Func (c *Chain) SetError(err error)

SetError is to set an error object into the chain safely using mutexes.

Func (c *Chain) SetErrorHandler(x func(super *Chain))

SetErrorHandler is to set a given process function to handle the error safely using mutexes.

Func (c *Chain) SetTimeout(duration uint64)

SetTimeout is to save a given timeout duration into the Chain safely using mutexes.

the timeout value is for the entire chain executions. The value can be multiplied for bigger values using the available unit constants like:

             chain.Timeout = 5 * cmdchain.Seconds

Func (c *Chain) Wait()

Wait is to wait for the entire chain completion.

This function relies on the given Timeout value to exit an indefinite chain execution. If the Timeout is 0, this function sets to DefaultTimeout. To wait indefinitely, set the timeout duration to unreasonable value such as 2 hours.

This function should only be called once right after the first command’s Run(…). It is meant to instruct the “main” routine to hold and wait for all the chains to complete.

Upon timeout, it instructs the chain to perform error handling with the error message ErrorTimeout as the last process.