mrun: refactor hooks to use Components

This commit is contained in:
Brian Picciano 2019-06-15 17:13:00 -06:00
parent dd2d601081
commit c98f154992
3 changed files with 92 additions and 129 deletions

View File

@ -3,158 +3,125 @@ package mrun
import ( import (
"context" "context"
"github.com/mediocregopher/mediocre-go-lib/mctx" "github.com/mediocregopher/mediocre-go-lib/mcmp"
) )
// Hook describes a function which can be registered to trigger on an event via // Hook describes a function which can be registered to trigger on an event via
// the WithHook function. // the WithHook function.
type Hook func(context.Context) error type Hook func(context.Context) error
type ctxKey int type hookKey struct {
key interface{}
const (
ctxKeyHookEls ctxKey = iota
ctxKeyNumHooks
)
type ctxKeyWrap struct {
key ctxKey
userKey interface{}
} }
// because we want Hooks to be called in the order created, taking into account // AddHook registers a Hook under a typed key. The Hook will be called when
// 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) {
return ctxKeyWrap{
key: ctxKeyHookEls,
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 numHooks value for convenience.
func getHookEls(ctx context.Context, userKey interface{}) ([]hookEl, int) {
hookElsKey, numHooksKey := ctxKeys(userKey)
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)+1)
copy(hookEls, lastHookEls)
lastNumChildren := len(lastHookEls) - lastNumHooks
for _, child := range children[lastNumChildren:] {
hookEls = append(hookEls, hookEl{child: child})
}
return hookEls, lastNumHooks
}
// WithHook 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 // TriggerHooks is called with that same key. Multiple Hooks can be registered
// for the same key, and will be called sequentially when triggered. // for the same key, and will be called sequentially when triggered.
// //
// Hooks will be called with whatever Context is passed into TriggerHooks. // Hooks will be called with whatever Context is passed into TriggerHooks.
func WithHook(ctx context.Context, key interface{}, hook Hook) context.Context { func AddHook(cmp *mcmp.Component, key interface{}, hook Hook) {
hookEls, numHooks := getHookEls(ctx, key) mcmp.AddSeriesValue(cmp, hookKey{key}, hook)
hookEls = append(hookEls, hookEl{hook: hook})
hookElsKey, numHooksKey := ctxKeys(key)
ctx = mctx.WithLocalValue(ctx, hookElsKey, hookEls)
ctx = mctx.WithLocalValue(ctx, numHooksKey, numHooks+1)
return ctx
} }
func triggerHooks(ctx context.Context, userKey interface{}, next func([]hookEl) (hookEl, []hookEl)) error { func triggerHooks(
hookEls, _ := getHookEls(ctx, userKey) ctx context.Context,
var hookEl hookEl cmp *mcmp.Component,
key interface{},
next func([]mcmp.SeriesElement) (mcmp.SeriesElement, []mcmp.SeriesElement),
) error {
els := mcmp.GetSeriesElements(cmp, hookKey{key})
var el mcmp.SeriesElement
for { for {
if len(hookEls) == 0 { if len(els) == 0 {
break break
} }
hookEl, hookEls = next(hookEls) el, els = next(els)
if hookEl.child != nil { if el.Child != nil {
if err := triggerHooks(hookEl.child, userKey, next); err != nil { if err := triggerHooks(ctx, el.Child, key, next); err != nil {
return err return err
} }
} else if err := hookEl.hook(ctx); err != nil { continue
}
hook := el.Value.(Hook)
if err := hook(ctx); err != nil {
return err return err
} }
} }
return nil return nil
} }
// TriggerHooks causes all Hooks registered with WithHook on the Context (and // TriggerHooks causes all Hooks registered with AddHook on the Component under
// its predecessors) under the given key to be called in the order they were // the given key to be called in the order they were registered. The given
// registered. // Context is passed into all Hooks being called.
// //
// If any Hook returns an error no further Hooks will be called and that error // If any Hook returns an error no further Hooks will be called and that error
// will be returned. // will be returned.
// //
// If the Context has children (see the mctx package), and those children have // If the Component has children (see the mcmp package), and those children have
// Hooks registered under this key, then their Hooks will be called in the // Hooks registered under this key, then their Hooks will be called in the
// expected order. See package docs for an example. // expected order. See package docs for an example.
func TriggerHooks(ctx context.Context, key interface{}) error { func TriggerHooks(
return triggerHooks(ctx, key, func(hookEls []hookEl) (hookEl, []hookEl) { ctx context.Context,
return hookEls[0], hookEls[1:] cmp *mcmp.Component,
}) key interface{},
) error {
next := func(els []mcmp.SeriesElement) (
mcmp.SeriesElement, []mcmp.SeriesElement,
) {
return els[0], els[1:]
}
return triggerHooks(ctx, cmp, key, next)
} }
// TriggerHooksReverse is the same as TriggerHooks except that registered Hooks // TriggerHooksReverse is the same as TriggerHooks except that registered Hooks
// are called in the reverse order in which they were registered. // are called in the reverse order in which they were registered.
func TriggerHooksReverse(ctx context.Context, key interface{}) error { func TriggerHooksReverse(ctx context.Context, cmp *mcmp.Component, key interface{}) error {
return triggerHooks(ctx, key, func(hookEls []hookEl) (hookEl, []hookEl) { next := func(els []mcmp.SeriesElement) (
last := len(hookEls) - 1 mcmp.SeriesElement, []mcmp.SeriesElement,
return hookEls[last], hookEls[:last] ) {
}) last := len(els) - 1
return els[last], els[:last]
}
return triggerHooks(ctx, cmp, key, next)
} }
type builtinEvent int type builtinEvent int
const ( const (
start builtinEvent = iota initEvent builtinEvent = iota
stop shutdownEvent
) )
// WithStartHook registers the given Hook to run when Start is called. This is a // InitHook registers the given Hook to run when Init is called. This is a
// special case of WithHook. // special case of AddHook.
// //
// As a convention Hooks running on the start event should block only as long as // As a convention Hooks running on the init event should block only as long as
// it takes to ensure that whatever is running can do so successfully. For // 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 // 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 // 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 // go-routine to do their actual work. Long-lived tasks should set themselves up
// to stop on the stop event (see WithStopHook). // to shutdown on the shutdown event (see ShutdownHook).
func WithStartHook(ctx context.Context, hook Hook) context.Context { func InitHook(cmp *mcmp.Component, hook Hook) {
return WithHook(ctx, start, hook) AddHook(cmp, initEvent, hook)
} }
// Start runs all Hooks registered using WithStartHook. This is a special case // Init runs all Hooks registered using InitHook. This is a special case of
// of TriggerHooks. // TriggerHooks.
func Start(ctx context.Context) error { func Init(ctx context.Context, cmp *mcmp.Component) error {
return TriggerHooks(ctx, start) return TriggerHooks(ctx, cmp, initEvent)
} }
// WithStopHook registers the given Hook to run when Stop is called. This is a // ShutdownHook registers the given Hook to run when Shutdown is called. This is
// special case of WithHook. // a special case of AddHook.
func WithStopHook(ctx context.Context, hook Hook) context.Context { //
return WithHook(ctx, stop, hook) // See InitHook for more on the relationship between Init(Hook) and
// Shutdown(Hook).
func ShutdownHook(cmp *mcmp.Component, hook Hook) {
AddHook(cmp, shutdownEvent, hook)
} }
// Stop runs all Hooks registered using WithStopHook in the reverse order in // Shutdown runs all Hooks registered using ShutdownHook in the reverse order in
// which they were registered. This is a special case of TriggerHooks. // which they were registered. This is a special case of TriggerHooks.
func Stop(ctx context.Context) error { func Shutdown(ctx context.Context, cmp *mcmp.Component) error {
return TriggerHooksReverse(ctx, stop) return TriggerHooksReverse(ctx, cmp, shutdownEvent)
} }

View File

@ -4,7 +4,7 @@ import (
"context" "context"
. "testing" . "testing"
"github.com/mediocregopher/mediocre-go-lib/mctx" "github.com/mediocregopher/mediocre-go-lib/mcmp"
"github.com/mediocregopher/mediocre-go-lib/mtest/massert" "github.com/mediocregopher/mediocre-go-lib/mtest/massert"
) )
@ -17,34 +17,31 @@ func TestHooks(t *T) {
} }
} }
ctx := context.Background() cmp := new(mcmp.Component)
ctx = WithHook(ctx, 0, mkHook(1)) AddHook(cmp, 0, mkHook(1))
ctx = WithHook(ctx, 0, mkHook(2)) AddHook(cmp, 0, mkHook(2))
ctxA := mctx.NewChild(ctx, "a") cmpA := cmp.Child("a")
ctxA = WithHook(ctxA, 0, mkHook(3)) AddHook(cmpA, 0, mkHook(3))
ctxA = WithHook(ctxA, 999, mkHook(999)) // different key AddHook(cmpA, 999, mkHook(999)) // different key
ctx = mctx.WithChild(ctx, ctxA)
ctx = WithHook(ctx, 0, mkHook(4)) AddHook(cmp, 0, mkHook(4))
ctxB := mctx.NewChild(ctx, "b") cmpB := cmp.Child("b")
ctxB = WithHook(ctxB, 0, mkHook(5)) AddHook(cmpB, 0, mkHook(5))
ctxB1 := mctx.NewChild(ctxB, "1") cmpB1 := cmpB.Child("1")
ctxB1 = WithHook(ctxB1, 0, mkHook(6)) AddHook(cmpB1, 0, mkHook(6))
ctxB = mctx.WithChild(ctxB, ctxB1)
ctx = mctx.WithChild(ctx, ctxB)
ctx = WithHook(ctx, 0, mkHook(7)) AddHook(cmp, 0, mkHook(7))
massert.Require(t, massert.Require(t,
massert.Nil(TriggerHooks(ctx, 0)), massert.Nil(TriggerHooks(context.Background(), cmp, 0)),
massert.Equal([]int{1, 2, 3, 4, 5, 6, 7}, out), massert.Equal([]int{1, 2, 3, 4, 5, 6, 7}, out),
) )
out = nil out = nil
massert.Require(t, massert.Require(t,
massert.Nil(TriggerHooksReverse(ctx, 0)), massert.Nil(TriggerHooksReverse(context.Background(), cmp, 0)),
massert.Equal([]int{7, 6, 5, 4, 3, 2, 1}, out), massert.Equal([]int{7, 6, 5, 4, 3, 2, 1}, out),
) )
} }

View File

@ -1,12 +1,12 @@
// Package mrun provides the ability to register callback hooks on contexts, as // Package mrun provides the ability to register callback hooks on Components,
// well as some convenience functions which allow using a context as a // as well as some convenience functions which allow using a context as a
// wait-group. // wait-group.
// //
// Hooks // Hooks
// //
// Hooks are registered onto contexts and later called in bulk. mrun will take // Hooks are registered onto Components and later called in bulk. mrun will take
// into account the order Hooks are registered, including Hooks within a // into account the order Hooks are registered, including Hooks within a
// context's children (see mctx package), and execute them in the same order // Component's children (see mcmp package), and execute them in the same order
// they were registered. For example: // they were registered. For example:
// //
// newHook := func(i int) mrun.Hook { // newHook := func(i int) mrun.Hook {
@ -16,15 +16,14 @@
// } // }
// } // }
// //
// ctx := context.Background() // cmp := new(mcmp.Component)
// ctx = mrun.WithStartHook(ctx, newHook(0)) // mrun.InitHook(cmp, newHook(0))
// //
// child := mctx.NewChild(ctx, "child") // cmpChild := cmp.Child("child")
// child = mrun.WithStartHook(child, newHook(1)) // mrun.InitHook(cmpChild, newHook(1))
// ctx = mctx.WithChild(ctx, child)
// //
// ctx = mrun.WithStartHook(ctx, newHook(2)) // mrun.InitHook(cmp, newHook(2))
// mrun.Start(ctx) // prints "0", "1", then "2" // mrun.Init(context.Background(), cmp) // prints "0", "1", then "2"
// //
package mrun package mrun