lifecycle

package module
v1.1.4 Latest Latest
Warning

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

Go to latest
Published: May 16, 2022 License: MIT Imports: 8 Imported by: 5

README

lifecycle

GoDoc Go Report Card codecov CircleCI

Overview

lifecycle helps manage goroutines at the application level. context.Context has been great for propagating cancellation signals, but not for getting any feedback about when goroutines actually finish.

This package works with context.Context to ensure that applications don't quit before their goroutines do.

The semantics work similarly to the go (lifecycle.Go) and defer (lifecycle.Defer) keywords as well as sync.WaitGroup.Wait (lifecycle.Wait). Additionally, there are lifecycle.GoErr and lifecycle.DeferErr which only differ in that they take funcs that return errors.

lifecycle.Wait will block until one of the following happens:

  • all funcs registered with Go complete successfully then all funcs registered with Defer complete successfully
  • a func registered with Go returns an error, immediately canceling ctx and triggering Defer funcs to run. Once all Go and Defer funcs complete, Wait will return the error
  • a signal (by default SIGINT and SIGTERM, but configurable with WithSignals) is received, immediately canceling ctx and triggering Defer funcs to run. Once all Go and Defer funcs complete, Wait will return ErrSignal
  • a func registered with Go or Defer panics. the panic will be propagated to the goroutine that Wait runs in. there is no attempt, in case of a panic, to manage the state within the lifecycle package.

Example

Here is an example that shows how lifecycle could work with an http.Server:

// At the top of your application
ctx := lifecycle.New(
    context.Background(),
    lifecycle.WithTimeout(30*time.Second), // optional
)

helloHandler := func(w http.ResponseWriter, req *http.Request) {
    _, _ = io.WriteString(w, "Hello, world!\n")
}

mux := http.NewServeMux()
mux.HandleFunc("/hello", helloHandler)

srv := &http.Server{
    Addr:    ":8080",
    Handler: mux,
}

lifecycle.GoErr(ctx, srv.ListenAndServe)

lifecycle.DeferErr(ctx, func() error {
    // use a background context because we already have a timeout and when
    // Defer funcs run, ctx is necessarily canceled.
    return srv.Shutdown(context.Background())
})

// Any panics in Go or Defer funcs will be passed to the goroutine that Wait
// runs in, so it is possible to handle them like this
defer func() {
    if r := recover(); r != nil {
        panic(r) // example, you probably want to do something else
    }
}()

// Then at the end of main(), or run() or however your application operates
//
// The returned err is the first non-nil error returned by any func
// registered with Go or Defer, otherwise nil.
if err := lifecycle.Wait(ctx); err != nil {
    log.Fatal(err)
}

Documentation

Overview

Example
package main

import (
	"context"
	"fmt"
	"io"
	"log"
	"net/http"
	"time"

	"github.com/joshuarubin/lifecycle"
)

func main() {
	// This is only to ensure that the example completes
	const timeout = 10 * time.Millisecond
	ctx, cancel := context.WithTimeout(context.Background(), timeout)
	defer cancel()

	// At the top of your application
	ctx = lifecycle.New(
		ctx,
		lifecycle.WithTimeout(30*time.Second), // optional
	)

	helloHandler := func(w http.ResponseWriter, req *http.Request) {
		_, _ = io.WriteString(w, "Hello, world!\n")
	}

	mux := http.NewServeMux()
	mux.HandleFunc("/hello", helloHandler)

	srv := &http.Server{
		Addr:    ":8080",
		Handler: mux,
	}

	var start, finish time.Time

	lifecycle.GoErr(ctx, func() error {
		start = time.Now()
		return srv.ListenAndServe()
	})

	lifecycle.DeferErr(ctx, func() error {
		finish = time.Now()
		fmt.Println("shutting down http server")
		// use a background context because we already have a timeout and when
		// Defer funcs run, ctx is necessarily canceled.
		return srv.Shutdown(context.Background())
	})

	// Any panics in Go or Defer funcs will be passed to the goroutine that Wait
	// runs in, so it is possible to handle them like this
	defer func() {
		if r := recover(); r != nil {
			panic(r) // example, you probably want to do something else
		}
	}()

	// Then at the end of main(), or run() or however your application operates
	//
	// The returned err is the first non-nil error returned by any func
	// registered with Go or Defer, otherwise nil.
	if err := lifecycle.Wait(ctx); err != nil && err != context.DeadlineExceeded {
		log.Fatal(err)
	}

	// This is just to show that the server will run for at least `timeout`
	// before shutting down
	if finish.Sub(start) < timeout {
		log.Fatal("didn't wait long enough to shutdown")
	}

}
Output:

shutting down http server

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Defer

func Defer(ctx context.Context, deferred ...func())

Defer adds funcs that should be called after the Go funcs complete (either clean or with errors) or a signal is received

func DeferCtxErr added in v1.1.0

func DeferCtxErr(ctx context.Context, deferred ...func(context.Context) error)

DeferCtxErr adds funcs, that take a context and return an error, that should be called after the Go funcs complete (either clean or with errors) or a signal is received. If any GoErr, GoCtxErr, DeferErr or DeferCtxErr func returns an error, only the first one will be returned by Wait()

func DeferErr added in v0.0.2

func DeferErr(ctx context.Context, deferred ...func() error)

DeferErr adds funcs, that return errors, that should be called after the Go funcs complete (either clean or with errors) or a signal is received. If any GoErr, GoCtxErr, DeferErr or DeferCtxErr func returns an error, only the first one will be returned by Wait()

func Exists added in v1.0.0

func Exists(ctx context.Context) bool

Exists returns true if the context has a lifecycle manager attached

func Go

func Go(ctx context.Context, f ...func())

Go run a function in a new goroutine

func GoCtxErr added in v1.1.0

func GoCtxErr(ctx context.Context, f ...func(ctx context.Context) error)

GoCtxErr runs a function that takes a context and returns an error in a new goroutine. If any GoErr, GoCtxErr, DeferErr or DeferCtxErr func returns an error, only the first one will be returned by Wait()

func GoErr added in v0.0.2

func GoErr(ctx context.Context, f ...func() error)

GoErr runs a function that returns an error in a new goroutine. If any GoErr, GoCtxErr, DeferErr or DeferCtxErr func returns an error, only the first one will be returned by Wait()

func New

func New(ctx context.Context, opts ...Option) context.Context

New returns a lifecycle manager with context derived from that provided.

func Wait

func Wait(ctx context.Context) error

Wait blocks until all go routines have been completed.

All funcs registered with Go and Defer _will_ complete under every circumstance except a panic

Funcs passed to Defer begin (and the context returned by New() is canceled) when any of:

  • All funcs registered with Go complete successfully
  • Any func registered with Go returns an error
  • A signal is received (by default SIGINT or SIGTERM, but can be changed by WithSignals

Funcs registered with Go should stop and clean up when the context returned by New() is canceled. If the func accepts a context argument, it will be passed the context returned by New().

WithTimeout() can be used to set a maximum amount of time, starting with the context returned by New() is canceled, that Wait will wait before returning.

The returned err is the first non-nil error returned by any func registered with Go or Defer, otherwise nil.

Types

type ErrSignal

type ErrSignal struct {
	os.Signal
}

ErrSignal is returned by Wait if the reason it returned was because a signal was caught

func (ErrSignal) Error

func (e ErrSignal) Error() string

type Option

type Option func(*manager)

An Option is used to configure the lifecycle manager

func WithSignals

func WithSignals(val ...os.Signal) Option

WithSignals causes Handle to wait for Go funcs to finish, if WhenDone was used or until a signal is received. The signals it will wait for can be defined with WithSigs or will default to syscall.SIGINT and syscall.SIGTERM

func WithTimeout

func WithTimeout(val time.Duration) Option

WithTimeout sets an upper limit for how much time Handle will wait to return. After the Go funcs finish, if WhenDone was used, or after a signal is received if WhenSignaled was used, this timer starts. From that point, Handle will return if any Go or Defer function takes longer than this value.

Jump to

Keyboard shortcuts

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