Don't use this. Use golang.org/x/sync/errgroup
instead.
Simple barrier logic for asynchronous non-cancellable tasks.
taskchain is a very simple implementation of what's known as a barrier. TL;DR: a barrier is a mechanism that aggregates a group of asynchronous tasks in such a way that they all must complete before the next barrier can begin.
The task chain consists of one to many contiguous task groups. Within each task group is one to many tasks. A task is a function with this signature:
type Task func(*TaskGroup) error
We'll see why you would want to take in the task group as a parameter in a moment. What's important to know here is that if your task returns an error, the task group will not continue to the next task group. More specifically, task group is a struct:
type TaskGroup struct {
Next *TaskGroup
ErrorHandler func(*TaskGroup, error)
}
You add tasks to a task group with the Add
function:
func (t *TaskGroup) Add(task Task) {...}
func myTask (t *TaskGroup) error {
// do stuff
return nil
}
tg := &TaskGroup{}
tg.Add(myTask)
TaskGroup
tasks are dispatched with the Exec
function:
func (t *TaskGroup) Exec() error {...}
tg := &TaskGroup{}
tg.Add(func(t *TaskGroup) error {
// do stuff
return nil
})
if err := tg.Exec(); err != nil {
// handle error
}
If any of the tasks within the task group fail, then Exec
returns the first error returned by any of the tasks. However, you can listen for any errors that occur when the task group executes by setting its error handler:
func errHandler(t *TaskGroup, err error) {
// this error handler will be called for ANY
// errors returned while the task group is executing
}
tg := &TaskGroup{}
tg.ErrorHandler = errHandler
You can inject dependencies into a task group by accessing its bag. The bag is an internal map that is mutexed during storage and retrieval operations. You are responsible for the goroutine safety of whatever you put in the bag, however.
You add things to the bag like so:
tg := &TaskGroup{}
tg.Set("foo", "bar")
And then access it within your task like so:
func myTask(t *TaskGroup) error {
fooThing := t.Get("foo", "bazz").(string)
}
Note that second argument in t.Get
. If there is no item in the bag with key foo
, it will return a default value (bazz
in this case). If there is no item matching the key, or if the item matching the key is nil
, then the default will be used (this is a straight ripoff of Python's get
dictionary function). If you want nil
to be returned if the key is not found, set that second parameter to nil
.
Also note that Get
returns an interface{}
, so you need to cast it to the appropriate type.
One nice thing about the bag is that it gets passed from parent task group to child task group. Well, it's not so much passed along as it is overlayed atop the child group's bag. If a child task group has an item in its bag with the same key as its parent, the child's value will not be overwritten. For example:
tgParent := &TaskGroup{}
tgParent.Set("foo", "bar")
tgParent.Set("fizz", "buzz")
// add tasks to parent ...
tgChild := &TaskGroup{}
tgChild.Set("foo", "bazz")
// add tasks to child ...
tgParent.Next = tgChild
_ = tgParent.Exec()
// this will be 'bazz', not 'bar'
v1 := tgChild.Get("foo", "").(string)
// this will be 'buzz' inherited from the parent task group
v2 := tgChild.Get("fizz", "").(string)
The same forwarding logic applies to error handlers. If the parent task group has an error handler and the child task group does not, then the child task group will inherit the parent's handler. If the child group has its own error handler, that will be used.