mediocre-go-lib/mrun/hook.go

181 lines
5.8 KiB
Go
Raw Normal View History

2019-01-13 01:14:02 +00:00
package mrun
import (
"context"
2019-01-13 01:14:02 +00:00
"github.com/mediocregopher/mediocre-go-lib/mctx"
)
2019-01-13 01:14:02 +00:00
// 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
}
2019-01-13 01:14:02 +00:00
// 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})
2019-01-13 01:14:02 +00:00
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
}
2019-01-13 01:14:02 +00:00
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
2019-01-13 01:14:02 +00:00
}
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
2019-01-13 01:14:02 +00:00
}
}
return nil
2019-01-13 01:14:02 +00:00
}
// 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.
2019-01-13 01:14:02 +00:00
//
// 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:]
2019-01-13 01:14:02 +00:00
})
}
// 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]
2019-01-13 01:14:02 +00:00
})
}
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)
2019-01-13 01:14:02 +00:00
}
// Start runs all Hooks registered using OnStart. This is a special case of
// TriggerHooks.
func Start(ctx context.Context) error {
2019-01-13 01:14:02 +00:00
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)
2019-01-13 01:14:02 +00:00
}
// 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 {
2019-01-13 01:14:02 +00:00
return TriggerHooksReverse(ctx, stop)
}