From e349d0fbd8aef8f83aa14d8cad4695a29329aaa8 Mon Sep 17 00:00:00 2001 From: Brian Picciano Date: Sun, 28 Oct 2018 19:34:26 -0400 Subject: [PATCH] mctx: moved ctx code from m and kept fleshing it out, trying to figure out how to incorporate Logger --- m/ctx.go | 73 ------------------- m/ctx_test.go | 39 ----------- m/m.go | 30 -------- mctx/ctx.go | 177 +++++++++++++++++++++++++++++++++++++++++++++++ mctx/ctx_test.go | 58 ++++++++++++++++ 5 files changed, 235 insertions(+), 142 deletions(-) delete mode 100644 m/ctx.go delete mode 100644 m/ctx_test.go create mode 100644 mctx/ctx.go create mode 100644 mctx/ctx_test.go diff --git a/m/ctx.go b/m/ctx.go deleted file mode 100644 index 279e232..0000000 --- a/m/ctx.go +++ /dev/null @@ -1,73 +0,0 @@ -package m - -import ( - "context" - "sync" -) - -type ctxKey int - -const ( - ctxStateKey ctxKey = iota -) - -type ctxState struct { - path []string - l sync.RWMutex - children map[string]context.Context -} - -func newCtxState() *ctxState { - return &ctxState{ - children: map[string]context.Context{}, - } -} - -// Ctx returns a new context which can be used as the root context for all -// purposes in this framework. -func Ctx() context.Context { - return context.WithValue(context.Background(), ctxStateKey, newCtxState()) -} - -func getCtxState(ctx context.Context) *ctxState { - s, _ := ctx.Value(ctxStateKey).(*ctxState) - if s == nil { - panic("non-conforming context used") - } - return s -} - -// Path returns the sequence of names which were used to produce this context -// via the ChildOf function. -func Path(ctx context.Context) []string { - return getCtxState(ctx).path -} - -// Children returns all children of this context which have been created by -// ChildOf, mapped by their name. -func Children(ctx context.Context) map[string]context.Context { - s := getCtxState(ctx) - out := map[string]context.Context{} - s.l.RLock() - defer s.l.RUnlock() - for name, childCtx := range s.children { - out[name] = childCtx - } - return out -} - -// ChildOf creates a child of the given context with the given name and returns -// it. The Path of the returned context will be the path of the parent with its -// name appended to it. The Children function can be called on the parent to -// retrieve all children which have been made using this function. -func ChildOf(ctx context.Context, name string) context.Context { - s, childS := getCtxState(ctx), newCtxState() - s.l.Lock() - defer s.l.Unlock() - childS.path = make([]string, 0, len(s.path)+1) - childS.path = append(childS.path, s.path...) - childS.path = append(childS.path, name) - childCtx := context.WithValue(ctx, ctxStateKey, childS) - s.children[name] = childCtx - return childCtx -} diff --git a/m/ctx_test.go b/m/ctx_test.go deleted file mode 100644 index 19b5d69..0000000 --- a/m/ctx_test.go +++ /dev/null @@ -1,39 +0,0 @@ -package m - -import ( - "context" - . "testing" - - "github.com/mediocregopher/mediocre-go-lib/mtest/massert" -) - -func TestContext(t *T) { - ctx := Ctx() - ctx1 := ChildOf(ctx, "1") - ctx1a := ChildOf(ctx1, "a") - ctx1b := ChildOf(ctx1, "b") - ctx2 := ChildOf(ctx, "2") - - massert.Fatal(t, massert.All( - massert.Len(Path(ctx), 0), - massert.Equal(Path(ctx1), []string{"1"}), - massert.Equal(Path(ctx1a), []string{"1", "a"}), - massert.Equal(Path(ctx1b), []string{"1", "b"}), - massert.Equal(Path(ctx2), []string{"2"}), - )) - - massert.Fatal(t, massert.All( - massert.Equal( - map[string]context.Context{"1": ctx1, "2": ctx2}, - Children(ctx), - ), - massert.Equal( - map[string]context.Context{"a": ctx1a, "b": ctx1b}, - Children(ctx1), - ), - massert.Equal( - map[string]context.Context{}, - Children(ctx2), - ), - )) -} diff --git a/m/m.go b/m/m.go index fbe6ef0..7e35de2 100644 --- a/m/m.go +++ b/m/m.go @@ -5,10 +5,7 @@ package m import ( - "io" - "github.com/mediocregopher/mediocre-go-lib/mcfg" - "github.com/mediocregopher/mediocre-go-lib/mlog" ) // CfgSource returns an mcfg.Source which takes in configuration info from the @@ -19,30 +16,3 @@ func CfgSource() mcfg.Source { mcfg.SourceCLI{}, } } - -// TODO this isn't going to work. At some point there will be something like -// mhttp which will have some glue code for it in here, but then something -// within it which logs (like the http server), and then there'll be an import -// cycle. -// -// Ultimately it might be worthwhile to make Logger be a field on Cfg, that -// really seems like the only other place where code like this makes sense to -// go. But then that's greatly expanding the purview of Cfg, which is -// unfortunate.... - -// Log returns a Logger which will automatically include with the log extra -// contextual information based on the Cfg and the given KVs -// -// If the cfg is nil then mlog.DefaultLogger is returned. -func Log(cfg *mcfg.Cfg, kvs ...mlog.KVer) *mlog.Logger { - fn := cfg.FullName() - l := mlog.DefaultLogger.Clone() - l.SetWriteFn(func(w io.Writer, msg mlog.Message) error { - msg.Msg = "(" + fn + ") " + msg.Msg - return mlog.DefaultWriteFn(w, msg) - }) - if len(kvs) > 0 { - l.AddKV(kvs...) - } - return l -} diff --git a/mctx/ctx.go b/mctx/ctx.go new file mode 100644 index 0000000..886b0e6 --- /dev/null +++ b/mctx/ctx.go @@ -0,0 +1,177 @@ +// Package mctx provides a framework, based around the Context type, for +// managing configuration, initialization, runtime, and shutdown of a binary. +// The framework allows components to manage these aspects individually by means +// of a heirarchical system. +// +// Each node in the hierarchy is given a name and is aware of all of its +// ancestors, and can incorporate this information into its functionality. +package mctx + +import ( + "context" + "sync" + "time" +) + +// Context is the same as the builtin type, but is used to indicate that the +// Context originally came from this package (aka New or ChildOf). +type Context context.Context + +// CancelFunc is a direct alias of the type from the context package, see its +// docs. +type CancelFunc = context.CancelFunc + +// WithValue mimics the function from the context package. +func WithValue(parent Context, key, val interface{}) Context { + return Context(context.WithValue(context.Context(parent), key, val)) +} + +// WithCancel mimics the function from the context package. +func WithCancel(parent Context) (Context, CancelFunc) { + ctx, fn := context.WithCancel(context.Context(parent)) + return Context(ctx), fn +} + +// WithDeadline mimics the function from the context package. +func WithDeadline(parent Context, t time.Time) (Context, CancelFunc) { + ctx, fn := context.WithDeadline(context.Context(parent), t) + return Context(ctx), fn +} + +// WithTimeout mimics the function from the context package. +func WithTimeout(parent Context, d time.Duration) (Context, CancelFunc) { + ctx, fn := context.WithTimeout(context.Context(parent), d) + return Context(ctx), fn +} + +//////////////////////////////////////////////////////////////////////////////// + +type ctxKey int + +type ctxState struct { + path []string + l sync.RWMutex + parent Context + children map[string]Context + + mutVals map[interface{}]interface{} +} + +func getCtxState(ctx Context) *ctxState { + s, _ := ctx.Value(ctxKey(0)).(*ctxState) + if s == nil { + panic("non-conforming context used") + } + return s +} + +func withCtxState(ctx Context, s *ctxState) Context { + return WithValue(ctx, ctxKey(0), s) +} + +// New returns a new context which can be used as the root context for all +// purposes in this framework. +func New() Context { + return withCtxState(Context(context.Background()), &ctxState{ + //logger: mlog.NewLogger(os.Stderr), + }) +} + +// Path returns the sequence of names which were used to produce this context +// via the ChildOf function. +func Path(ctx Context) []string { + return getCtxState(ctx).path +} + +// Children returns all children of this context which have been created by +// ChildOf, mapped by their name. +func Children(ctx Context) map[string]Context { + s := getCtxState(ctx) + out := map[string]Context{} + s.l.RLock() + defer s.l.RUnlock() + for name, childCtx := range s.children { + out[name] = childCtx + } + return out +} + +// Parent returns the parent Context of the given one, or nil if this is a root +// context (i.e. returned from New). +func Parent(ctx Context) Context { + return getCtxState(ctx).parent +} + +// ChildOf creates a child of the given context with the given name and returns +// it. The Path of the returned context will be the path of the parent with its +// name appended to it. The Children function can be called on the parent to +// retrieve all children which have been made using this function. +func ChildOf(ctx Context, name string) Context { + s, childS := getCtxState(ctx), new(ctxState) + + s.l.Lock() + defer s.l.Unlock() + + // set child's path field + childS.path = make([]string, 0, len(s.path)+1) + childS.path = append(childS.path, s.path...) + childS.path = append(childS.path, name) + + // set child's parent field + childS.parent = ctx + + // set up child's logger + //pathStr := "/" + //if len(childS.path) > 0 { + // pathStr += path.Join(childS.path...) + //} + //childS.logger = s.logger.Clone() + //childS.logger.SetWriteFn(func(w io.Writer, msg mlog.Message) error { + // msg.Msg = "(" + pathStr + ") " + msg.Msg + // return mlog.DefaultWriteFn(w, msg) + //}) + + // copy mutable values + if len(s.mutVals) > 0 { + childS.mutVals = make(map[interface{}]interface{}, len(s.mutVals)) + for key, val := range s.mutVals { + childS.mutVals[key] = val + } + } + + // create child's ctx and store it in parent + childCtx := withCtxState(ctx, childS) + if s.children == nil { + s.children = map[string]Context{} + } + s.children[name] = childCtx + return childCtx +} + +// TODO these might not be worth the effort, they're very subject to +// race-conditions + +// SetMutableValue is like WithMutable, except rather than leaving the original +// ctx unaffected it modifies the value in that context. Children of this +// context will inherit an independent copy of its immutable values. +func SetMutableValue(ctx Context, key, value interface{}) { + s := getCtxState(ctx) + s.l.Lock() + defer s.l.Unlock() + if s.mutVals == nil { + s.mutVals = map[interface{}]interface{}{} + } + s.mutVals[key] = value +} + +// MutableValue acts like the Value method, except that it only deals with +// keys/values set by SetMutableValue. +func MutableValue(ctx Context, key interface{}) interface{} { + s := getCtxState(ctx) + s.l.RLock() + defer s.l.RUnlock() + if s.mutVals == nil { + return nil + } + return s.mutVals[key] +} diff --git a/mctx/ctx_test.go b/mctx/ctx_test.go new file mode 100644 index 0000000..d15f7de --- /dev/null +++ b/mctx/ctx_test.go @@ -0,0 +1,58 @@ +package mctx + +import ( + . "testing" + + "github.com/mediocregopher/mediocre-go-lib/mtest/massert" +) + +func TestContext(t *T) { + ctx := New() + SetMutableValue(ctx, "one", 1) + + ctx1 := ChildOf(ctx, "1") + ctx1a := ChildOf(ctx1, "a") + SetMutableValue(ctx1, "one", 2) + ctx1b := ChildOf(ctx1, "b") + SetMutableValue(ctx1b, "one", 3) + ctx2 := ChildOf(ctx, "2") + + massert.Fatal(t, massert.All( + massert.Len(Path(ctx), 0), + massert.Equal(Path(ctx1), []string{"1"}), + massert.Equal(Path(ctx1a), []string{"1", "a"}), + massert.Equal(Path(ctx1b), []string{"1", "b"}), + massert.Equal(Path(ctx2), []string{"2"}), + )) + + massert.Fatal(t, massert.All( + massert.Equal( + map[string]Context{"1": ctx1, "2": ctx2}, + Children(ctx), + ), + massert.Equal( + map[string]Context{"a": ctx1a, "b": ctx1b}, + Children(ctx1), + ), + massert.Equal( + map[string]Context{}, + Children(ctx2), + ), + )) + + massert.Fatal(t, massert.All( + massert.Nil(Parent(ctx)), + massert.Equal(Parent(ctx1), ctx), + massert.Equal(Parent(ctx1a), ctx1), + massert.Equal(Parent(ctx1b), ctx1), + massert.Equal(Parent(ctx2), ctx), + )) + + massert.Fatal(t, massert.All( + massert.Equal(MutableValue(ctx, "one"), 1), + massert.Equal(MutableValue(ctx1, "one"), 2), + massert.Equal(MutableValue(ctx1a, "one"), 1), + massert.Equal(MutableValue(ctx1b, "one"), 3), + massert.Equal(MutableValue(ctx2, "one"), 1), + )) +}