From d5044ad7cbc4e6a0fe3a2609ddcc630c16c90dc3 Mon Sep 17 00:00:00 2001 From: Brian Picciano Date: Sat, 18 May 2019 12:15:35 -0600 Subject: [PATCH] mctx: add Annotated and polish documentation --- mctx/annotate.go | 18 ++++++---- mctx/ctx.go | 90 +++++++++++++++++++++++++++++++++++++----------- 2 files changed, 82 insertions(+), 26 deletions(-) diff --git a/mctx/annotate.go b/mctx/annotate.go index 1547565..58e4d90 100644 --- a/mctx/annotate.go +++ b/mctx/annotate.go @@ -63,14 +63,19 @@ func Annotate(ctx context.Context, kvs ...interface{}) context.Context { return ctx } +// Annotated is a shortcut for calling Annotate with a context.Background(). +func Annotated(kvs ...interface{}) context.Context { + return Annotate(context.Background(), kvs...) +} + // AnnotationSet describes a set of unique Annotation values which were // retrieved off a Context via the Annotations function. An AnnotationSet has a // couple methods on it to aid in post-processing. type AnnotationSet []Annotation // Annotations returns all Annotation values which have been set via Annotate on -// this Context. If a key was set twice then only the most recent value is -// included. +// this Context and its ancestors. If a key was set twice then only the most +// recent value is included. func Annotations(ctx context.Context) AnnotationSet { a, _ := ctx.Value(annotationKey(0)).(*annotation) if a == nil { @@ -160,7 +165,7 @@ func (aa AnnotationSet) StringMapByPath() map[string]map[string]string { return outM } -// StringMap formats each of the Annotations into strings using fmt.Sprint. If +// StringMap formats each of the Annotations into strings using fmt.Sprint. If // any two keys format to the same string, then path information will be // prefaced to each one. If they still format to the same string, then type // information will be prefaced to each one. @@ -272,9 +277,10 @@ func mergeAnnotations(ctxA, ctxB context.Context) context.Context { // MergeAnnotations sequentially merges the annotation data of the passed in // Contexts into the first passed in one. Data from a Context overwrites -// overlapping data on all passed in Contexts to the left of it. All other -// aspects of the first Context remain the same, and that Context is returned -// with the new set of Annotation data. +// overlapping data on all passed in Contexts to the left of it (keeping in mind +// that two Annotations must share the same Key _and_ Path to overlap). All +// other aspects of the first Context remain the same, and that Context is +// returned with the new set of Annotation data. // // NOTE this will panic if no Contexts are passed in. func MergeAnnotations(ctxs ...context.Context) context.Context { diff --git a/mctx/ctx.go b/mctx/ctx.go index ac5003e..54b5932 100644 --- a/mctx/ctx.go +++ b/mctx/ctx.go @@ -1,12 +1,62 @@ // Package mctx extends the builtin context package to organize Contexts into a -// hierarchy. Each node in the hierarchy is given a name and is aware of all of -// its ancestors. -// -// This package also provides extra functionality which allows contexts -// to be more useful when used in the hierarchy. +// hierarchy. // // All functions and methods in this package are thread-safe unless otherwise // noted. +// +// Parents and children +// +// Each node in the hierarchy is given a name and is aware of all of its +// ancestors. The sequence of ancestor's names, ending in the node's name, is +// called its "path". For example: +// +// ctx := context.Background() +// ctxA := mctx.NewChild(ctx, "A") +// ctxB := mctx.NewChild(ctx, "B") +// fmt.Printf("ctx:%#v\n", mctx.Path(ctx)) // prints "ctx:[]string(nil)" +// fmt.Printf("ctxA:%#v\n", mctx.Path(ctxA)) // prints "ctxA:[]string{"A"} +// fmt.Printf("ctxB:%#v\n", mctx.Path(ctxB)) // prints "ctxA:[]string{"A", "B"} +// +// WithChild can be used to incorporate a child into its parent, making the +// parent's children iterable on it: +// +// ctx := context.Background() +// ctxA1 := mctx.NewChild(ctx, "A1") +// ctxA2 := mctx.NewChild(ctx, "A2") +// ctx = mctx.WithChild(ctx, ctxA1) +// ctx = mctx.WithChild(ctx, ctxA2) +// for _, childCtx := range mctx.Children(ctx) { +// fmt.Printf("%q\n", mctx.Name(childCtx)) // prints "A1" then "A2" +// } +// +// Key/Value +// +// The context's key/value namespace is split into two: a space local to a node, +// not inherited from its parent or inheritable by its children +// (WithLocalValue), and the original one which comes with the builtin context +// package (context.WithValue): +// +// ctx := context.Background() +// ctx = context.WithValue(ctx, "inheritableKey", "foo") +// ctx = mctx.WithLocalValue(ctx, "localKey", "bar") +// childCtx := mctx.NewChild(ctx, "child") +// +// // ctx.Value("inheritableKey") == "foo" +// // child.Value("inheritableKey") == "foo" +// // mctx.LocalValue(ctx, "localKey") == "bar" +// // mctx.LocalValue(child, "localKey") == nil +// +// Annotations +// +// Annotations are a special case of local key/values, where the data being +// stored is specifically runtime metadata which would be useful for logging, +// error output, etc... Annotation data might include an IP address of a +// connected client, a userID the client has authenticated as, the primary key +// of a row in a database being queried, etc... +// +// Annotations are always tied to the path of the node they were set on, so that +// even when the annotations of two contexts are merged the annotation data will +// not overlap unless the contexts have the same path. package mctx import ( @@ -41,9 +91,9 @@ func Child(parent context.Context, name string) context.Context { return parent.Value(ancestryKeyChildren).([]context.Context)[i] } -// Children returns all children of this Context which have been kept by -// WithChild, mapped by their name. If this Context wasn't produced by WithChild -// then this returns nil. +// Children returns all children of this Context which have been added by +// WithChild, in the order they were added. If this Context wasn't produced by +// WithChild then this returns nil. func Children(parent context.Context) []context.Context { children, _ := parent.Value(ancestryKeyChildren).([]context.Context) return children @@ -101,8 +151,8 @@ func pathHash(path []string) string { return hex.EncodeToString(pathHash.Sum(nil)) } -// Name returns the name this Context was generated with via NewChild, or false -// if this Context was not generated via NewChild. +// Name returns the name this Context was created with via NewChild, or false if +// this Context was not created via NewChild. func Name(ctx context.Context) (string, bool) { path := Path(ctx) if len(path) == 0 { @@ -111,10 +161,10 @@ func Name(ctx context.Context) (string, bool) { return path[len(path)-1], true } -// NewChild creates a new Context based off of the parent one, and returns a new -// instance of the passed in parent and the new child. The child will have a -// path which is the parent's path with the given name appended. The parent will -// have the new child as part of its set of children (see Children function). +// NewChild creates and returns a new Context based off of the parent one. The +// child will have a path which is the parent's path appended with the given +// name. In order for the parent to "see" the child (via the Child or Children +// functions) the WithChild function must be used. // // If the parent already has a child of the given name this function panics. func NewChild(parent context.Context, name string) context.Context { @@ -147,7 +197,7 @@ func isChild(parent, child context.Context) bool { } // WithChild returns a modified parent which holds a reference to child in its -// Children set. If the child's name is already taken in the parent then this +// Children list. If the child's name is already taken in the parent then this // function panics. func WithChild(parent, child context.Context) context.Context { if !isChild(parent, child) { @@ -168,8 +218,8 @@ func WithChild(parent, child context.Context) context.Context { } // BreadthFirstVisit visits this Context and all of its children, and their -// children, in a breadth-first order. If the callback returns false then the -// function returns without visiting any more Contexts. +// children, etc... in a breadth-first order. If the callback returns false then +// the function returns without visiting any more Contexts. func BreadthFirstVisit(ctx context.Context, callback func(context.Context) bool) { queue := []context.Context{ctx} for len(queue) > 0 { @@ -193,9 +243,9 @@ type localVal struct { key, val interface{} } -// WithLocalValue is like WithValue, but the stored value will not be present -// on any children created via WithChild. Local values must be retrieved with -// the LocalValue function in this package. Local values share a different +// WithLocalValue is like context.WithValue, but the stored value will not be +// present on any children created via NewChild. Local values must be retrieved +// with the LocalValue function in this package. Local values share a different // namespace than the normal WithValue/Value values (i.e. they do not overlap). func WithLocalValue(ctx context.Context, key, val interface{}) context.Context { prev, _ := ctx.Value(localValsKey(0)).(*localVal)