mediocre-go-lib/mrun/hook.go
Brian Picciano 4b446a0efc mctx: refactor so that contexts no longer carry mutable data
This change required refactoring nearly every package in this project,
but it does a lot to simplify mctx and make other code using it easier
to think about.

Other code, such as mlog and mcfg, had to be slightly modified for this
change to work as well.
2019-02-07 19:42:12 -05:00

181 lines
5.8 KiB
Go

package mrun
import (
"context"
"github.com/mediocregopher/mediocre-go-lib/mctx"
)
// Hook describes a function which can be registered to trigger on an event via
// the RegisterHook function.
type Hook func(context.Context) error
type ctxKey int
const (
ctxKeyHookEls ctxKey = iota
ctxKeyNumChildren
ctxKeyNumHooks
)
type ctxKeyWrap struct {
key ctxKey
userKey interface{}
}
// because we want Hooks to be called in the order created, taking into account
// the creation of children and their hooks as well, we create a sequence of
// elements which can either be a Hook or a child.
type hookEl struct {
hook Hook
child context.Context
}
func ctxKeys(userKey interface{}) (ctxKeyWrap, ctxKeyWrap, ctxKeyWrap) {
return ctxKeyWrap{
key: ctxKeyHookEls,
userKey: userKey,
}, ctxKeyWrap{
key: ctxKeyNumChildren,
userKey: userKey,
}, ctxKeyWrap{
key: ctxKeyNumHooks,
userKey: userKey,
}
}
// getHookEls retrieves a copy of the []hookEl in the Context and possibly
// appends more elements if more children have been added since that []hookEl
// was created.
//
// this also returns the latest numChildren and numHooks values for convenience.
func getHookEls(ctx context.Context, userKey interface{}) ([]hookEl, int, int) {
hookElsKey, numChildrenKey, numHooksKey := ctxKeys(userKey)
lastNumChildren, _ := mctx.LocalValue(ctx, numChildrenKey).(int)
lastNumHooks, _ := mctx.LocalValue(ctx, numHooksKey).(int)
lastHookEls, _ := mctx.LocalValue(ctx, hookElsKey).([]hookEl)
children := mctx.Children(ctx)
// plus 1 in case we wanna append something else outside this function
hookEls := make([]hookEl, len(lastHookEls), lastNumHooks+len(children)-lastNumChildren+1)
copy(hookEls, lastHookEls)
for _, child := range children[lastNumChildren:] {
hookEls = append(hookEls, hookEl{child: child})
}
return hookEls, len(children), lastNumHooks
}
// RegisterHook registers a Hook under a typed key. The Hook will be called when
// TriggerHooks is called with that same key. Multiple Hooks can be registered
// for the same key, and will be called sequentially when triggered.
//
// Hooks will be called with whatever Context is passed into TriggerHooks.
func RegisterHook(ctx context.Context, key interface{}, hook Hook) context.Context {
hookEls, numChildren, numHooks := getHookEls(ctx, key)
hookEls = append(hookEls, hookEl{hook: hook})
hookElsKey, numChildrenKey, numHooksKey := ctxKeys(key)
ctx = mctx.WithLocalValue(ctx, hookElsKey, hookEls)
ctx = mctx.WithLocalValue(ctx, numChildrenKey, numChildren)
ctx = mctx.WithLocalValue(ctx, numHooksKey, numHooks+1)
return ctx
}
func triggerHooks(ctx context.Context, userKey interface{}, next func([]hookEl) (hookEl, []hookEl)) error {
hookEls, _, _ := getHookEls(ctx, userKey)
var hookEl hookEl
for {
if len(hookEls) == 0 {
break
}
hookEl, hookEls = next(hookEls)
if hookEl.child != nil {
if err := triggerHooks(hookEl.child, userKey, next); err != nil {
return err
}
} else if err := hookEl.hook(ctx); err != nil {
return err
}
}
return nil
}
// TriggerHooks causes all Hooks registered with RegisterHook on the Context
// (and its predecessors) under the given key to be called in the order they
// were registered.
//
// If any Hook returns an error no further Hooks will be called and that error
// will be returned.
//
// If the Context has children (see the mctx package), and those children have
// Hooks registered under this key, then their Hooks will be called in the
// expected order. For example:
//
// // parent context has hookA registered
// ctx := context.Background()
// ctx = RegisterHook(ctx, 0, hookA)
//
// // child context has hookB registered
// childCtx := mctx.NewChild(ctx, "child")
// childCtx = RegisterHook(childCtx, 0, hookB)
// ctx = mctx.WithChild(ctx, childCtx) // needed to link childCtx to ctx
//
// // parent context has another Hook, hookC, registered
// ctx = RegisterHook(ctx, 0, hookC)
//
// // The Hooks will be triggered in the order: hookA, hookB, then hookC
// err := TriggerHooks(ctx, 0)
//
func TriggerHooks(ctx context.Context, key interface{}) error {
return triggerHooks(ctx, key, func(hookEls []hookEl) (hookEl, []hookEl) {
return hookEls[0], hookEls[1:]
})
}
// TriggerHooksReverse is the same as TriggerHooks except that registered Hooks
// are called in the reverse order in which they were registered.
func TriggerHooksReverse(ctx context.Context, key interface{}) error {
return triggerHooks(ctx, key, func(hookEls []hookEl) (hookEl, []hookEl) {
last := len(hookEls) - 1
return hookEls[last], hookEls[:last]
})
}
type builtinEvent int
const (
start builtinEvent = iota
stop
)
// OnStart registers the given Hook to run when Start is called. This is a
// special case of RegisterHook.
//
// As a convention Hooks running on the start event should block only as long as
// it takes to ensure that whatever is running can do so successfully. For
// short-lived tasks this isn't a problem, but long-lived tasks (e.g. a web
// server) will want to use the Hook only to initialize, and spawn off a
// go-routine to do their actual work. Long-lived tasks should set themselves up
// to stop on the stop event (see OnStop).
func OnStart(ctx context.Context, hook Hook) context.Context {
return RegisterHook(ctx, start, hook)
}
// Start runs all Hooks registered using OnStart. This is a special case of
// TriggerHooks.
func Start(ctx context.Context) error {
return TriggerHooks(ctx, start)
}
// OnStop registers the given Hook to run when Stop is called. This is a special
// case of RegisterHook.
func OnStop(ctx context.Context, hook Hook) context.Context {
return RegisterHook(ctx, stop, hook)
}
// Stop runs all Hooks registered using OnStop in the reverse order in which
// they were registered. This is a special case of TriggerHooks.
func Stop(ctx context.Context) error {
return TriggerHooksReverse(ctx, stop)
}