state

package module
v1.1.0 Latest Latest
Warning

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

Go to latest
Published: Sep 18, 2020 License: MIT Imports: 5 Imported by: 6

README

state

The package provides a simple mechanism for managing application's background jobs, allowing easy to manage graceful shutdown, waiting for completion, error propagation and custom states creation.

Features

  • Graceful Shutdown: easily create graceful shutdown with dependencies for your application without juggling with channels and goroutines
  • Waiting: wait for completion for all waitable background jobs
  • Annotating: quickly find the cause of errors or frozen shutdowns
  • Error Group: errors propagation across the tree of states
  • Custom states

Prerequisites

Package state defines the State type, which carries errors, wait groups, shutdown signals and other values from application's background jobs.

The State type is aggregative - it contains multiple states in tree form, allowing setting dependencies for graceful shutdown between them and merging multiple independent states.

To aggregate the application's states, functions that initialize background jobs create suitable State and propagate it up in the calls stack to the layer where it will be handled, optionally merging it with other states, setting dependencies between them and annotating along the way.

State has some mechanic similarities with context package.

In the case of context, the Context type carries request-scoped deadlines, cancelation signals, and other values across API boundaries and between processes.

A common example of Context usage – application receives a user request, creates context and makes a request to external API with it:

1. o-------------------------------------->  user request
2.          o------------------->            API request

By propagating cancelable Context, calling the CancelFunc during user request cancels the parent and all its children simultaneously:

1. o------------[cancel]-x                   user request
2.          o------------x                   API request

Context is short-lived and is not supposed to be stored or reused.

State, on the other hand, is application-scoped. It provides a type similar to Context for controlling long-living background jobs. Unlike Context, State propagates bottom-top during the app initialization phase and it is ok to store it, but not reuse.

Simple example – application initializes consumer and processor to work in background:

1. o-------------------------------------->  consumer
2.          o----------------------------->  processor

If we want to shut down the application, Context canceling semantics is not applicable - a simultaneous shutdown of consumer and processor can cause a data loss. State package will handle this case gracefully: it will shut down consumer, wait until it signals Ok, and shut down processor after:

1. o-----------[close]~~~~[ok]-x             consumer
2.          o-------------[close]~~~~[ok]-x  processor

Getting started

Requirements

Go 1.13+

Installing
go get -u github.com/lefelys/state
Usage
Creation

Create a new State using one of the package functions: WithShutdown, WithWait, WithErr, WithErrorGroup or WithValue:

st := state.WithErr(errors.New("error"))
Tails

Functions WithShutdown, WithWait, and WithErrorGroup in addition to a new State returns detached tail, which must be used for signaling in a background job associated with the State. In the case of WithShutdown, it detaches ShutdownTail interface with two methods:

  • End() <-chan struct{} - returns a channel that's closed when work done on behalf of tail's State should be shut down.
  • Done() - sends a signal that the shutdown is complete.
st, tail := state.WithShutdown()

go func() {
	for {
		select {
		case <-tail.End():
			fmt.Println("shutdown job")
			tail.Done()
			return
		default:
			/*...*/
		}
	}
}()
Propagation

Created State should be propagated up in the calls stack, where it will be handled:

func StartJob() state.State {
	st, tail := state.WithShutdown()

	go func() {
		/*...*/
	}()

	return st
}

func main() {
	jobSt := StartJob()

	shutdownSig := make(chan os.Signal, 1)
	signal.Notify(shutdownSig, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)

	<-shutdownSig

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	err := jobSt.Shutdown(ctx)
	if err != nil {
		log.Fatal(err)
	}
}
Merging and annotation

To merge multiple States use function Merge. States can also be merged using function WithAnnotation - it will help to find the cause of errors or frozen shutdowns:

func StartJobs() state.State {
	st1 := startJob1()
	st2 := startJob2()

	return state.WithAnnotation("job1 and job2", st1, st2)
}

func main() {
	st := StartJobs()

	/*...*/

	err := st.Shutdown(ctx)
	if err != nil {
		log.Fatal(err) // "job1 and job2: timeout expired"
	}
}
Dependency

Dependency is used for graceful shutdown: dependent State will shut down its children first, wait until all of them are successfully shut down and then shut down itself. There are 2 ways to create a dependency between States:

  1. By passing children States to State initializer:
st1 := startJob1()
st2 := startJob2()

// st3 depends on st1 and st2
st3, tail := state.WithShutdown(st1, st2)
  1. By calling an existing State's DependsOn method, which returns a new State with set dependencies:
st1 := StartJob1()
st2 := startJob2()
st3 := startJob3()

// st is the merged st1, st2 and st3 with st3 dependency set on st1 and st2
st := st3.DependsOn(st1, st2)
Recommendations

Programs that use State should follow these rules to keep interfaces consistent:

  1. All functions that initialize application-scoped background jobs should return State as its last return value.
  2. If an error can occur during initialization it is still should be returned as State using function WithError.
  3. Never return nil State - return Empty() instead, or do not return State at all if it is not needed.
  4. Every background job should be shutdownable and/or waitable.

There might be special cases, when returning state as the last return value is not possible, for example - when using dependency injection packages. To handle this case, embed State into dependency's return value:

package app

import "github.com/lefelys/state"

type Server struct {
	state.State

	server *http.Server
}

type Updater interface {
	state.State

	Update() error
}

func NewApp(server *Server, updater Updater) state.State {
	 st := server.DependsOn(updater)

	 /*...*/
}

Examples

See examples

Documentation

Overview

The state package provides simple state management primitives for go applications.

The package defines the State type, which carries errors, wait groups, shutdown signals and other values from application's background jobs.

The State type is aggregative - it contains multiple states in tree form, allowing setting dependencies for graceful shutdown between them and merging multiple independent states.

To aggregate the application's states, functions that initialize background jobs create suitable State and propagate it up in the calls stack to the layer where it will be handled, optionally merging it with other states, setting dependencies between them and annotating along the way.

Programs that use State should follow these rules to keep interfaces consistent:

1. All functions that initialize application-scoped background jobs should return State as its last return value.

There might be special cases, when returning state as the last return value is not possible, for example - when using dependency injection packages. To handle this case, embed State into dependency's return value:

type Server struct {
	state.State

	server *http.Server
}

type Updater interface {
	state.State

	Update() error
}

func NewApp(server *Server, updater Updater) state.State {
	 st := server.DependsOn(updater)

	 /*...*/
}

2. If an error can occur during initialization it is still should be returned as State using function WithError.

3. Never return nil State - return Empty() instead, or do not return State at all if it is not needed.

4. Every background job should be shutdownable and/or waitable.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrTimeout is the error returned by State.Shudown when shutdown's
	// timeout is expired
	ErrTimeout = errors.New("timeout expired")
)

Functions

func WithErrorGroup

func WithErrorGroup(children ...State) (State, ErrTail)

WithErrorGroup returns new state with merged children that can store an error.

The returned ErrTail is used to assign error to the state.

Example
st := func() State {
	st, tail := WithErrorGroup()

	go func() {
		tail.Error(errors.New("error"))
	}()

	return st
}()

time.Sleep(100 * time.Millisecond)

if err := st.Err(); err != nil {
	fmt.Println(err)
}
Output:

error

func WithReadiness added in v1.1.0

func WithReadiness(children ...State) (State, ReadinessTail)

func WithShutdown

func WithShutdown(children ...State) (State, ShutdownTail)

WithShutdown returns a new shutdownable State that depends on children.

The returned ShutdownTail's End channel is closed when State's Shutdown method is called or by its parent during graceful shutdown.

The ShutdownTail's Done call sends a signal that the shutdown is complete, which causes State's Shutdown method to return nil, or allow its parent to shut down itself during graceful shutdown.

Example
st := func() State {
	st, tail := WithShutdown()
	ticker := time.NewTicker(1 * time.Second)

	go func() {
		for {
			select {
			// receive a shutdown signal
			case <-tail.End():
				ticker.Stop()

				// send a signal that the shutdown is complete
				tail.Done()

				return
			case <-ticker.C:
				// some job
			}
		}
	}()

	return st
}()

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

err := st.Shutdown(ctx)
if err != nil {
	log.Fatal(err)
}
Output:

Example (Dependency)
runJob := func(name string) State {
	st, tail := WithShutdown()

	go func() {
		<-tail.End()

		fmt.Println("shutdown " + name)

		tail.Done()
	}()

	return st
}

st1 := runJob("job 1")
st2 := runJob("job 2")
st3 := runJob("job 3")

// st3 will be shut down first, then st2, then st1
st := st1.DependsOn(st2).DependsOn(st3)

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

err := st.Shutdown(ctx)
if err != nil {
	log.Fatal(err)
}
Output:

shutdown job 3
shutdown job 2
shutdown job 1
Example (DependencyWrap)
st1 := func() State {
	st, tail := WithShutdown()

	go func() {
		<-tail.End()
		fmt.Println("shutdown job 1")
		tail.Done()
	}()

	return st
}()

// st1 will be shut down first, then st2
st2, tail := WithShutdown(st1)

go func() {
	<-tail.End()
	fmt.Println("shutdown job 2")
	tail.Done()
}()

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

err := st2.Shutdown(ctx)
if err != nil {
	log.Fatal(err)
}
Output:

shutdown job 1
shutdown job 2

func WithWait

func WithWait(children ...State) (State, WaitTail)

WithWait returns new waitable State with merged children.

The returned WaitTail is used to increment and decrement State's WaitGroup counter.

Example
st := func() State {
	st, tail := WithWait()

	for i := 1; i <= 3; i++ {
		tail.Add(1)

		go func(i int) {
			<-time.After(time.Duration(i) * 50 * time.Millisecond)
			fmt.Printf("job %d ended\n", i)

			tail.Done()
		}(i)
	}

	return st
}()

// blocks until state's WaitGroup counter is zero
st.Wait()
Output:

job 1 ended
job 2 ended
job 3 ended

Types

type ErrTail

type ErrTail interface {
	// Error assigns err to associated state.
	// If the state already has an error - does nothing.
	Error(err error)

	// Errorf formats according to a format specifier and assigns
	// the string to associated state as a value that satisfies error.
	// If the state already has an error - does nothing.
	Errorf(format string, a ...interface{})
}

ErrTail detaches after error group state initialization. The tail is supposed to stay in a background job associated with created state and used to assign error to the state.

type ReadinessTail added in v1.1.0

type ReadinessTail interface {
	// Ok sends a signal that background job is ready.
	// Not calling Ok will block all parents readiness and cause
	// the channel from State's Ready call to block forever.
	// After the first call, subsequent calls do nothing.
	Ok()
}

ReadinessTail detaches after readiness state initialization. The tail is supposed to stay in a background job associated with created State as it carries readiness signal.

type ShutdownTail

type ShutdownTail interface {
	// End returns a channel that's closed when work done on behalf
	// of tail's State should be shut down.
	// Successive calls to End return the same value.
	End() <-chan struct{}

	// Done sends a signal that a shutdown is complete.
	// Not calling Done will block all parents closing and cause
	// the State's Shutdown call to return ErrTimeout or block forever.
	// After the first call, subsequent calls do nothing.
	Done()
}

ShutdownTail detaches after shutdownable state initialization. The tail is supposed to stay in a background job associated with created State as it carries shutdown and finish signals.

type State

type State interface {
	// Err returns the first encountered error in this state.
	// While error is propagated from bottom to top, it is being annotated
	// by annotation states in a chain. Annotation uses introduced in
	// go 1.13 errors wrapping.
	//
	// Successive calls to Err may not return the same value, but it will
	// never return nil after the first error occurred.
	Err() error

	// Wait blocks until all counters of WaitGroups in this state are zero.
	// It uses sync.Waitgroup under the hood and shares all its mechanics.
	Wait()

	// Shutdown gracefully shuts down this state.
	// Ths shutdown occurs from bottom to top: parents shut down their
	// children, wait until all of them are successfully shut down and
	// then shut down themselves.
	//
	// If ctx expires before the shutdown is complete, Shutdown tries
	// to find the first full path of unclosed children to accumulate
	// annotations and returns ErrTimeout wrapped in them.
	// There is a chance that the shutdown will complete during that check -
	// in this case, it is considered as fully completed and returns nil.
	Shutdown(ctx context.Context) error

	// Ready returns a channel that signals that all states in tree are
	// ready. If there is no readiness states in the tree - state is considered
	// as ready by default.
	//
	// If some readiness state in the tree didn't send Ok signal -
	// returned channel blocks forever. It is caller's responsibility to
	// handle possible block.
	Ready() <-chan struct{}

	// Value returns the first found value in this state for key,
	// or nil if no value is associated with key. The tree is searched
	// from top to bottom and from left to right.
	//
	// It is possible to have multiple values associated with the same key,
	// but Value call will always return the topmost and the leftmost.
	//
	// Use state values only for data that represents custom states, not
	// for returning optional values from functions.
	//
	// Other rules for working with Value is the same as in the standard
	// package context:
	//
	// 1. Functions that wish to store values in State typically allocate
	// a key in a global variable then use that key as the argument to
	// state.WithValue and State.Value.
	// 2. A key can be any type that supports equality and can not be nil.
	// 3. Packages should define keys as an unexported type to avoid
	// collisions.
	// 4. Packages that define a State key should provide type-safe accessors
	// for the values stored using that key (see examples).
	Value(key interface{}) (value interface{})

	// DependsOn creates a new state from the original and children.
	// The new state ensures that during shutdown it will shut down children
	// first, wait until all of them are successfully shut down and then shut
	// down the original state.
	DependsOn(children ...State) State
	// contains filtered or unexported methods
}

State carries errors, wait groups, shutdown signals and other values from application's background jobs in tree form.

State is not reusable.

State's methods may be called by multiple goroutines simultaneously.

func Empty

func Empty() State

Empty returns new empty State

func Merge

func Merge(states ...State) State

Merge returns new State with merged children.

Example
runJob := func(name string, duration time.Duration) State {
	st, tail := WithShutdown()

	go func() {
		<-tail.End()

		<-time.After(duration)
		fmt.Println("shutdown " + name)

		tail.Done()
	}()

	return st
}

st1 := runJob("job 1", 50*time.Millisecond)
st2 := runJob("job 2", 100*time.Millisecond)
st3 := runJob("job 3", 150*time.Millisecond)

st := Merge(st1, st2, st3)

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

err := st.Shutdown(ctx)
if err != nil {
	log.Fatal(err)
}
Output:

shutdown job 1
shutdown job 2
shutdown job 3

func WithAnnotation

func WithAnnotation(message string, children ...State) State

WithAnnotation returns new state with merged children and assigned annotation to it.

Example
st := func() State {
	st, tail := WithErrorGroup()

	go func() {
		tail.Error(errors.New("error"))
	}()

	return WithAnnotation("my job", st)
}()

time.Sleep(100 * time.Millisecond)

if err := st.Err(); err != nil {
	fmt.Println(err)
}
Output:

my job: error
Example (Shutdown)
st := func() State {
	st, tail := WithShutdown()

	go func() {
		<-tail.End()
		<-time.After(1 * time.Second)
		tail.Done()
	}()

	return WithAnnotation("my job", st)
}()

ctx, cancel := context.WithTimeout(context.Background(), 0)
defer cancel()

err := st.Shutdown(ctx)
if err != nil {
	fmt.Println(err)
}
Output:

my job: timeout expired

func WithError

func WithError(err error, children ...State) State

WithError returns new State with merged children and assigned err to it.

Example
st := func() State {
	return WithError(errors.New("error"))
}()

if err := st.Err(); err != nil {
	fmt.Println(err)
}
Output:

error

func WithValue

func WithValue(key, value interface{}, children ...State) State

WithValue returns new State with merged children and value assigned to key.

Use state values only for data that represents custom states, not for returning optional values from functions.

Other rules for working with Value is the same as in the standard package context:

1. Functions that wish to store values in State typically allocate a key in a global variable then use that key as the argument to state.WithValue and State.Value.

2. A key can be any type that supports equality and can not be nil.

3. Packages should define keys as an unexported type to avoid collisions.

4. Packages that define a State key should provide type-safe accessors for the values stored using that key (see examples).

Example
type key int

var greetingKey key

getGreeting := func(st State) (c chan string, ok bool) {
	value := st.Value(greetingKey)
	if value != nil {
		return value.(chan string), true
	}

	return
}

st := func() State {
	c := make(chan string)
	st := WithValue(greetingKey, c)

	go func() {
		c <- "hi"
	}()

	return st
}()

c, ok := getGreeting(st)
if ok {
	fmt.Println(<-c)
}
Output:

hi

type WaitTail

type WaitTail interface {
	// Done calls sync.WaitGroup's Done method
	Done()

	// Add calls sync.WaitGroup's Add method
	Add(i int)
}

WaitTail detaches after waitable state initialization. The tail is supposed to stay in a background job associated with created State.

WaitTail uses sync.WaitGroup and shares all its mechanics.

Directories

Path Synopsis
examples

Jump to

Keyboard shortcuts

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