Documentation ¶
Overview ¶
Package scope provides context objects for the sharing of scope across goroutines. This context object provides a number of utilities for coordinating concurrent work, in addition to sharing data.
Lifecycle ¶
Contexts are nodes in a tree. A context is born either by forking from an existing context (becoming a child of that node in the tree), or a new tree is started by calling New().
A context can be terminated at any time. This is usually done by calling the Terminate() or Cancel() method. Termination is associated with an error value (which may be nil if one wants to indicate success). When a node in the tree is terminated, that termination is propagated down to all its unterminated descendents.
For example, here is how one might fan out a search:
// Fan out queries. for _, q := range queries { go func() { a, err := q.Run(ctx.Fork()) if err != nil { answers <- nil } else { answers <- a } }() } // Receive answers (or failures). for answer := range answers { if answer != nil { ctx.Cancel() // tell outstanding queries to give up return answer, nil } } return nil, fmt.Errorf("all queries failed")
Contexts can be terminated at any time. You can even fork a context with a deadline:
ctx := scope.New() result, err := Search(ctx.ForkWithTimeout(5 * time.Second), queries) if err == scope.TimedOut { // one or more backends timed out, have the caller back off }
There is a termination channel, Done(), available if you want to interrupt your work when a context is terminated:
// Wait for 10 seconds or termination incurred from another goroutine, // whichever occurs first. select { case <-ctx.Done(): return ctx.Err() case <-timer.After(10*time.Second): return nil }
You can also spot-check for termination with a call to the Alive() method.
for ctx.Alive() { readChunk() }
Data Sharing ¶
Contexts provide a data store for key value pairs, shared across the entire scope. When a context is forked, the child context shares the same data map as its parent.
This data store maps blank interfaces to blank interfaces, in the exact same manner as http://www.gorillatoolkit.org/pkg/context. This means you must use type assertions at runtime. To keep this reasonably safe, it's recommended to define and use your own unexported type for all keys maintained by your package.
type myKey int const ( loggerKey myKey = iota dbKey // etc. ) func SetLogger(ctx scope.Context, logger *log.Logger) { ctx.Set(loggerKey, logger) } func GetLogger(ctx scope.Context) logger *log.Logger) { return ctx.Get(loggerKey).(*log.Logger) }
The shared data store is managed in a copy-on-write fashion as the tree branches. When a context is forked, the child maintains a pointer to the parent's data map. When Set() is called on the child, the original map is duplicated for the child, and the update is only applied to the child's map.
Common WaitGroup ¶
Each context provides a WaitGroup() method, which returns the same pointer across the entire tree. You can use this to spin off background tasks and then wait for them before you completely shut down the scope.
ctx.WaitGroup().Add(1) go func() { doSomeThing(ctx) ctx.WaitGroup().Done() }() ctx.WaitGroup().Wait()
Breakpoints ¶
Contexts provide an optional feature to facilitate unit testing, called breakpoints. A breakpoint is identified by a list of hashable values. Production code can pass this list to the Check() method to synchronize and allow for an error to be injected. Test code can register a breakpoint with Breakpoint(), which returns a channel of errors. The test can receive from this channel to synchronize with the entry of the corresponding Check() call, and then write back an error to synchronize with the exit.
func Get(ctx scope.Context, url string) (*http.Response, error) { if err := ctx.Check("http.Get", url); err != nil { return nil, err } return http.Get(url) } func TestGetError(t *testing.T) { ctx := scope.New() ctrl := ctx.Breakpoint("http.Get", "http://google.com") testErr := fmt.Errorf("test error") go func() { <-ctrl ctrl <- testErr }() if err := Get(ctx, "http://google.com"); err != testErr { t.Fail() } }
Index ¶
- Variables
- type Breakpointer
- type Context
- type ContextTree
- func (ctx *ContextTree) Alive() bool
- func (ctx *ContextTree) Breakpoint(scope ...interface{}) chan error
- func (ctx *ContextTree) Cancel()
- func (ctx *ContextTree) Check(scope ...interface{}) error
- func (ctx *ContextTree) Done() <-chan struct{}
- func (ctx *ContextTree) Err() error
- func (ctx *ContextTree) Fork() Context
- func (ctx *ContextTree) ForkWithTimeout(dur time.Duration) Context
- func (ctx *ContextTree) Get(key interface{}) interface{}
- func (ctx *ContextTree) GetOK(key interface{}) (interface{}, bool)
- func (ctx *ContextTree) Set(key, val interface{})
- func (ctx *ContextTree) Terminate(err error)
- func (ctx *ContextTree) WaitGroup() *sync.WaitGroup
Examples ¶
Constants ¶
This section is empty.
Variables ¶
Functions ¶
This section is empty.
Types ¶
type Breakpointer ¶
type Breakpointer interface { // Breakpoint returns an error channel that can be used to synchronize // with a call to Check with the exact same parameters from another // goroutine. The call to Check will send a nil value across this // channel, and then receive a value to return to its caller. Breakpoint(scope ...interface{}) chan error // Check synchronizes with a registered breakpoint to obtain an error // value to return, or immediately returns nil if no breakpoint is // registered. Check(scope ...interface{}) error }
Breakpointer provides a pair of methods for synchronizing across goroutines and injecting errors. The Check method can be used to provide a point of synchronization/injection. In normal operation, this method will quickly return nil. A unit test can then use Breakpoint, with the same parameters, to obtain a bidirectional error channel. Receiving from this channel will block until Check is called. The call to Check will block until an error value (or nil) is sent back into the channel.
Example ¶
package main import ( "fmt" "euphoria.io/scope" ) func main() { root := scope.New() // A function that returns an error, which we want to simulate. output := func(arg string) error { _, err := fmt.Println(arg) return err } // A function that we want to test the error handling of. verifyOutput := func(ctx scope.Context, arg string) error { if err := ctx.Check("output()", arg); err != nil { return err } return output(arg) } // Set a breakpoint on a particular invocation of output. ctrl := root.Breakpoint("output()", "fail") // Other invocations should proceed as normal. err := verifyOutput(root, "normal behavior") fmt.Println("verifyOutput returned", err) // Our breakpoint should allow us to inject an error. To control it // we must spin off a goroutine. go func() { <-ctrl // synchronize at beginning of verifyOutput ctrl <- fmt.Errorf("test error") }() err = verifyOutput(root, "fail") fmt.Println("verifyOutput returned", err) // We can also inject an error by terminating the context. go func() { <-ctrl root.Cancel() }() err = verifyOutput(root, "fail") fmt.Println("verifyOutput returned", err) }
Output: normal behavior verifyOutput returned <nil> verifyOutput returned test error verifyOutput returned context cancelled
type Context ¶
type Context interface { // Alive returns true if the context has not completed. Alive() bool // Done returns a receive-only channel that will be closed when this // context (or any of its ancestors) terminates. Done() <-chan struct{} // Err returns the error this context was terminated with. Err() error // Cancel terminates this context (and all its descendents) with the // Cancelled error. Cancel() // Terminate marks this context and all descendents as terminated. // This sets the error returned by Err(), closed channels returned by // Done(), and injects the given error into any pending breakpoint // checks. Terminate(error) // Fork creates and returns a new context as a child of this one. Fork() Context // ForkWithTimeout creates and returns a new context as a child of this // one. It also spins off a timer which will cancel the context after // the given duration (unless the context terminates first). ForkWithTimeout(time.Duration) Context // Get returns the value associated with the given key. If this context // has had no values set, then the lookup is made on the nearest ancestor // with data. If no value is found, an unboxed nil value is returned. Get(key interface{}) interface{} // GetOK returns the value associated with the given key, along with a // bool value indicating successful lookup. See Get for details. GetOK(key interface{}) (interface{}, bool) // Set associates the given key and value in this context's data. Set(key, val interface{}) // WaitGroup returns a wait group pointer common to the entire context // tree. WaitGroup() *sync.WaitGroup // Breakpointer provides a harness for injecting errors and coordinating // goroutines when unit testing. Breakpointer }
A Context is a handle on a node within a shared scope. This shared scope takes the form of a tree of such nodes, for sharing state across coordinating goroutines.
Example (Cancellation) ¶
package main import ( "fmt" "time" "euphoria.io/scope" ) func main() { ctx := scope.New() go func() { time.Sleep(50 * time.Millisecond) ctx.Cancel() }() loop: for { t := time.After(10 * time.Millisecond) select { case <-ctx.Done(): break loop case <-t: fmt.Println("tick") } } fmt.Println("finished with", ctx.Err()) }
Output: tick tick tick tick finished with context cancelled
type ContextTree ¶
type ContextTree struct {
// contains filtered or unexported fields
}
ContextTree is the default implementation of Context.
func (*ContextTree) Alive ¶
func (ctx *ContextTree) Alive() bool
Alive returns true if the context has not completed.
func (*ContextTree) Breakpoint ¶
func (ctx *ContextTree) Breakpoint(scope ...interface{}) chan error
Breakpoint returns an error channel that can be used to synchronize with a call to Check with the exact same parameters from another goroutine. The call to Check will send a nil value across this channel, and then receive a value to return to its caller.
func (*ContextTree) Cancel ¶
func (ctx *ContextTree) Cancel()
Cancel terminates this context (and all its descendents) with the Cancelled error.
func (*ContextTree) Check ¶
func (ctx *ContextTree) Check(scope ...interface{}) error
Check synchronizes with a registered breakpoint to obtain an error value to return, or immediately returns nil if no breakpoint is registered.
func (*ContextTree) Done ¶
func (ctx *ContextTree) Done() <-chan struct{}
Done returns a receive-only channel that will be closed when this context (or any of its ancestors) is terminated.
func (*ContextTree) Err ¶
func (ctx *ContextTree) Err() error
Err returns the error this context was terminated with.
func (*ContextTree) Fork ¶
func (ctx *ContextTree) Fork() Context
Fork creates and returns a new context as a child of this one.
func (*ContextTree) ForkWithTimeout ¶
func (ctx *ContextTree) ForkWithTimeout(dur time.Duration) Context
ForkWithTimeout creates and returns a new context as a child of this one. It also spins off a timer which will cancel the context after the given duration (unless the context terminates first).
func (*ContextTree) Get ¶
func (ctx *ContextTree) Get(key interface{}) interface{}
Get returns the value associated with the given key. If this context has had no values set, then the lookup is made on the nearest ancestor with data. If no value is found, an unboxed nil value is returned.
func (*ContextTree) GetOK ¶
func (ctx *ContextTree) GetOK(key interface{}) (interface{}, bool)
GetOK returns the value associated with the given key, along with a bool value indicating successful lookup. See Get for details.
func (*ContextTree) Set ¶
func (ctx *ContextTree) Set(key, val interface{})
Set associates the given key and value in this context's data.
func (*ContextTree) Terminate ¶
func (ctx *ContextTree) Terminate(err error)
Terminate marks this context and all descendents as terminated. This sets the error returned by Err(), closed channels returned by Done(), and injects the given error into any pending breakpoint checks.
func (*ContextTree) WaitGroup ¶
func (ctx *ContextTree) WaitGroup() *sync.WaitGroup
WaitGroup returns a wait group pointer common to the entire context tree.