scope

package module
v0.0.0-...-33b185f Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 16, 2017 License: MIT Imports: 3 Imported by: 24

README

Build Status GoDoc


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()
		}
	}

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

Examples

Constants

This section is empty.

Variables

View Source
var (
	Cancelled = errors.New("context cancelled")
	Canceled  = Cancelled

	TimedOut = errors.New("context timed out")
)

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

func New

func New() Context

New returns an empty Context with no ancestor. This serves as the root of a shared scope.

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL