A package for graceful shutdowns (inspired by tomb)
A Killable
represents a group of goroutines. It goes through 3 stages:
- Alive - The goroutines are running
- Dying - The goroutines are being signaled to terminate
- Dead - All managed goroutines have terminated
There are two ways a Killable
can enter the dying state.
- One of the managed goroutines returns a non
nil
error
- The
Kill(error)
method is invoked on theKillable
.
Goroutines managed by the Killable
are started with killable.Go
k := killable.New()
go func() {
<-k.Dying()
fmt.Println("Dying")
<-k.Dead()
fmt.Println("Dead")
}()
killable.Go(k, func() error {
time.Sleep(5 * time.Second)
fmt.Println("Finished sleeping, i'll be dead soon")
return nil
})
k.Kill(fmt.Errorf("it's time to die!"))
- A
Killable
is not dead until all managed goroutines have returned. - If the goroutine returns a non
nil
error
, theKillable
starts dying. - If the
Killable
is already dying when theGo
method is invoked, it does not run.
Defer
is similar to the defer
keyword.
func Connect(k killable.Killable) (*sql.DB, error) {
db, err := sql.Open("foo", "bar")
if err != nil {
return nil, err
}
// clean up resources near instantiation
killable.Defer(k, func() {
db.Close()
})
return db, nil
}
- Deferred methods are called once the killable is dead.
- Deferred methods are invoked in the opposite order they were defined (lifo).
Killable
s can be linked to eachother in a parent/child relationship.
- If a child is killed, the parent is also killed.
- If the parent is killed, it kills all the children.
- If the
reason
isErrKillLocal
, the parent ignores it. - The parent doesn't die until all the children are dead
func makeChild(d time.Duration) killable.Killable {
k := killable.New()
killable.Go(k, func() {
// Sleep will immediately return ErrDying if the Killable
// enters the dying state during the sleep
// (see source to see how to implement similar methods)
if err := killable.Sleep(k, d); err != nil {
return err
}
return killable.ErrKill
})
return k
}
var (
// children
k1 = makeChild(4 * time.Second)
k2 = makeChild(3 * time.Second)
k3 = makeChild(2 * time.Second)
// parent
k4 = killable.New(k1, k2, k3)
)
killable.Defer(k4, func() {
fmt.Println("All children are dead!")
})
go func() {
<-k4.Dying()
fmt.Println("Killing all children")
}()
See examples/
directory.
The methods like Defer
, Go
, Do
, etc ... have been placed in the packages because the Killable
type is meant to be embedded. The interface the Killable
type exposes makes sense without understanding the killable
package.