pax_global_header00006660000000000000000000000064142403364020014510gustar00rootroot0000000000000052 comment=8ceeaa42d215e356f6c917ffc749555b8f318400 lifecycle-1.1.4/000077500000000000000000000000001424033640200134525ustar00rootroot00000000000000lifecycle-1.1.4/.circleci/000077500000000000000000000000001424033640200153055ustar00rootroot00000000000000lifecycle-1.1.4/.circleci/config.yml000066400000000000000000000011121424033640200172700ustar00rootroot00000000000000--- version: 2.1 orbs: codecov: codecov/codecov@1.0.4 executors: golang: docker: - image: circleci/golang:latest jobs: build: executor: golang steps: - checkout - restore_cache: keys: - go-pkg-mod - run: name: Run Tests command: | go test -v -cover -race -coverprofile=coverage.txt -covermode=atomic ./... - codecov/upload - save_cache: key: go-pkg-mod paths: - "/go/pkg/mod" workflows: version: 2 build-and-test: jobs: - build lifecycle-1.1.4/.gitignore000066400000000000000000000000301424033640200154330ustar00rootroot00000000000000*.coverprofile /vendor/ lifecycle-1.1.4/LICENSE000066400000000000000000000021261424033640200144600ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2019 Joshua Rubin Copyright (c) 2018 zvelo, Inc. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. lifecycle-1.1.4/README.md000066400000000000000000000062441424033640200147370ustar00rootroot00000000000000# lifecycle [![GoDoc](https://godoc.org/github.com/joshuarubin/lifecycle?status.svg)](https://godoc.org/github.com/joshuarubin/lifecycle) [![Go Report Card](https://goreportcard.com/badge/github.com/joshuarubin/lifecycle)](https://goreportcard.com/report/github.com/joshuarubin/lifecycle) [![codecov](https://codecov.io/gh/joshuarubin/lifecycle/branch/master/graph/badge.svg)](https://codecov.io/gh/joshuarubin/lifecycle) [![CircleCI](https://circleci.com/gh/joshuarubin/lifecycle.svg?style=svg)](https://circleci.com/gh/joshuarubin/lifecycle) ## 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`: ```go // 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) } ``` lifecycle-1.1.4/example_test.go000066400000000000000000000035171424033640200165010ustar00rootroot00000000000000package lifecycle_test import ( "context" "fmt" "io" "log" "net/http" "time" "github.com/joshuarubin/lifecycle" ) func Example() { // 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 } lifecycle-1.1.4/go.mod000066400000000000000000000001571424033640200145630ustar00rootroot00000000000000module github.com/joshuarubin/lifecycle go 1.18 require golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 lifecycle-1.1.4/go.sum000066400000000000000000000003211424033640200146010ustar00rootroot00000000000000golang.org/x/sync v0.0.0-20220513210516-0976fa681c29 h1:w8s32wxx3sY+OjLlv9qltkLU5yvJzxjjgiHWLjdIcw4= golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= lifecycle-1.1.4/lifecycle.go000066400000000000000000000164361424033640200157520ustar00rootroot00000000000000package lifecycle import ( "context" "fmt" "os" "os/signal" "runtime/debug" "syscall" "time" "golang.org/x/sync/errgroup" ) var defaultSignals = []os.Signal{syscall.SIGINT, syscall.SIGTERM} type manager struct { group *errgroup.Group timeout time.Duration sigs []os.Signal ctx context.Context cancel func() gctx context.Context deferred []func() error panic chan interface{} } type contextKey struct{} func fromContext(ctx context.Context) *manager { m, ok := ctx.Value(contextKey{}).(*manager) if !ok { panic(fmt.Errorf("lifecycle: manager not in context")) } return m } // New returns a lifecycle manager with context derived from that // provided. func New(ctx context.Context, opts ...Option) context.Context { m := &manager{ deferred: []func() error{}, panic: make(chan interface{}, 1), } ctx = context.WithValue(ctx, contextKey{}, m) m.sigs = make([]os.Signal, len(defaultSignals)) copy(m.sigs, defaultSignals) m.ctx, m.cancel = context.WithCancel(ctx) m.group, m.gctx = errgroup.WithContext(context.Background()) for _, o := range opts { o(m) } return m.ctx } // Exists returns true if the context has a lifecycle manager attached func Exists(ctx context.Context) bool { _, ok := ctx.Value(contextKey{}).(*manager) return ok } func wrapCtxFunc(ctx context.Context, fn func(ctx context.Context) error) func() error { m := fromContext(ctx) return func() error { defer func() { if r := recover(); r != nil { debug.PrintStack() m.panic <- r } }() return fn(ctx) } } func wrapFunc(ctx context.Context, fn func() error) func() error { return wrapCtxFunc(ctx, func(context.Context) error { return fn() }) } // Go run a function in a new goroutine func Go(ctx context.Context, f ...func()) { m := fromContext(ctx) for _, t := range f { if t == nil { continue } fn := t m.group.Go(wrapFunc(ctx, func() error { fn() return nil })) } } // 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 GoErr(ctx context.Context, f ...func() error) { m := fromContext(ctx) for _, fn := range f { if fn == nil { continue } m.group.Go(wrapFunc(ctx, fn)) } } // 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 GoCtxErr(ctx context.Context, f ...func(ctx context.Context) error) { m := fromContext(ctx) for _, fn := range f { if fn == nil { continue } m.group.Go(wrapCtxFunc(ctx, fn)) } } // Defer adds funcs that should be called after the Go funcs complete (either // clean or with errors) or a signal is received func Defer(ctx context.Context, deferred ...func()) { m := fromContext(ctx) for _, t := range deferred { if t == nil { continue } fn := t m.deferred = append(m.deferred, wrapFunc(ctx, func() error { fn() return nil })) } } // 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 DeferErr(ctx context.Context, deferred ...func() error) { m := fromContext(ctx) for _, fn := range deferred { if fn == nil { continue } m.deferred = append(m.deferred, wrapFunc(ctx, fn)) } } // 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 DeferCtxErr(ctx context.Context, deferred ...func(context.Context) error) { m := fromContext(ctx) for _, fn := range deferred { if fn == nil { continue } m.deferred = append(m.deferred, wrapCtxFunc(ctx, fn)) } } // 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. func Wait(ctx context.Context) error { m := fromContext(ctx) err := m.runPrimaryGroup() m.cancel() if err != nil { _ = m.runDeferredGroup() // #nosec return err } return m.runDeferredGroup() } // ErrSignal is returned by Wait if the reason it returned was because a signal // was caught type ErrSignal struct { os.Signal } func (e ErrSignal) Error() string { return fmt.Sprintf("lifecycle: caught signal: %v", e.Signal) } // runPrimaryGroup waits for all registered routines to // complete, returning on an error from any of them, or from // the receipt of a registered signal, or from a context cancelation. func (m *manager) runPrimaryGroup() error { select { case sig := <-m.signalReceived(): return ErrSignal{sig} case err := <-m.runPrimaryGroupRoutines(): return err case <-m.ctx.Done(): return m.ctx.Err() case <-m.gctx.Done(): // the error from the gctx errgroup will be returned // from errgroup.Wait() later in runDeferredGroupRoutines case r := <-m.panic: panic(r) } return nil } func (m *manager) runDeferredGroup() error { ctx := context.Background() if m.timeout > 0 { var cancel context.CancelFunc ctx, cancel = context.WithTimeout(ctx, m.timeout) defer cancel() // releases resources if deferred functions return early } select { case <-ctx.Done(): return ctx.Err() case err := <-m.runDeferredGroupRoutines(): return err case r := <-m.panic: panic(r) } } // A channel that receives any os signals registered to be received. // If not configured to receive signals, it will receive nothing. func (m *manager) signalReceived() <-chan os.Signal { sigCh := make(chan os.Signal, 1) if len(m.sigs) > 0 { signal.Notify(sigCh, m.sigs...) } return sigCh } // A channel that notifies of errors caused while waiting for subroutines to finish. func (m *manager) runPrimaryGroupRoutines() <-chan error { errs := make(chan error, 1) go func() { errs <- m.group.Wait() }() return errs } func (m *manager) runDeferredGroupRoutines() <-chan error { done := make(chan struct{}) var err error go func() { defer close(done) for i := len(m.deferred) - 1; i >= 0; i-- { if derr := m.deferred[i](); err == nil { err = derr } } }() errs := make(chan error, 1) go func() { gerr := m.group.Wait() <-done if gerr != nil { errs <- gerr } else { errs <- err } }() return errs } lifecycle-1.1.4/lifecycle_test.go000066400000000000000000000210261424033640200170000ustar00rootroot00000000000000package lifecycle_test import ( "context" "errors" "fmt" "os" "runtime" "strings" "sync" "sync/atomic" "syscall" "testing" "time" "github.com/joshuarubin/lifecycle" ) func testPanic(t *testing.T, fn func()) { t.Helper() var recovered bool var wg sync.WaitGroup wg.Add(1) go func() { defer func() { if r := recover(); r != nil { recovered = true } wg.Done() }() fn() }() wg.Wait() if !recovered { t.Errorf("did not panic") } } func TestBadContext(t *testing.T) { testPanic(t, func() { lifecycle.Go(context.Background(), func() {}) }) } func TestEmptyLifecycle(t *testing.T) { ctx := lifecycle.New(context.Background()) // A lifecycle manager with no configuration should immediately return // on manager with no error and not block. err := lifecycle.Wait(ctx) if err != nil { t.Fatalf("empty lifecycle error: %v", err) } } func TestSingleRoutine(t *testing.T) { ctx := lifecycle.New(context.Background()) // A lifecycle manager with a single registered routine should immediately execute // the routine without needing to call Wait. var ran int64 lifecycle.Go(ctx, func() { atomic.StoreInt64(&ran, 1) }) runtime.Gosched() if atomic.LoadInt64(&ran) != 1 { t.Error("lifecycle manager did not immediately run registered routine.") } } func TestPrimaryError(t *testing.T) { ctx := lifecycle.New(context.Background()) // A manager with a single erroring registered routine should return that // error on Wait lifecycle.GoErr(ctx, func() error { return errors.New("errored") }) err := lifecycle.Wait(ctx) if err == nil { t.Fatal("error expected but not received.") } if err.Error() != "errored" { t.Fatalf("expected error of value \"errored\", but received: %v", err) } } func TestMultiplePrimaryErrors(t *testing.T) { ctx := lifecycle.New(context.Background()) // when multiple routines will error, the first error should be returned // without waiting for the second routine to finish. lifecycle.GoErr(ctx, func() error { return errors.New("error1") }) err := lifecycle.Wait(ctx) if err == nil { t.Fatal("error expected but none received.") } if err.Error() != "error1" { t.Fatalf("expected error of value \"error1\", but received: %v", err) } } func TestSingleDeferred(t *testing.T) { ctx := lifecycle.New(context.Background()) // A manager with no primary routines and one deferred routine should // execute the deferred routine on Wait. Deferred routines do not // run immediately, requiring that Managebe explicitly invoked. ran := false lifecycle.Defer(ctx, func() { ran = true }) err := lifecycle.Wait(ctx) if err != nil { t.Fatalf("unexpected error on Wait: %v", err) } runtime.Gosched() if !ran { t.Error("lifecycle manager did not run deferred routine upon Wait.") } } func TestSingleDeferredError(t *testing.T) { ctx := lifecycle.New(context.Background()) // A manager with no primary routines and one deferred routine should // execute the deferred routine on Wait and return its error. lifecycle.DeferErr(ctx, func() error { return errors.New("deferred error") }) err := lifecycle.Wait(ctx) if err == nil { t.Fatal("Manager with an erroring deferred expected error, but received none.") } if err.Error() != "deferred error" { t.Fatalf("expected \"deferred error\" but got: %v", err) } } func TestMultipleDeferredErrors(t *testing.T) { ctx := lifecycle.New(context.Background()) // A manager with no primary routines and multiple deferred routines // should execute the deferred routines in reverse order, and return // the first executed deferred error, which might be the last deferred // func lifecycle.DeferErr(ctx, func() error { return errors.New("deferred error1") }) lifecycle.DeferErr(ctx, func() error { time.Sleep(10 * time.Millisecond) return errors.New("deferred error2") }) err := lifecycle.Wait(ctx) if err == nil { t.Fatal("Manager with an erroring deferred expected error, but received none.") } if err.Error() != "deferred error2" { t.Fatalf("expected \"deferred error2\" but got: %v", err) } } func TestPrimaryAndSecondary(t *testing.T) { ctx := lifecycle.New(context.Background()) // A manager with both a primary and deferred routine should execute both. var primaryRan, deferredRan bool lifecycle.Go(ctx, func() { primaryRan = true }) lifecycle.Defer(ctx, func() { deferredRan = true }) err := lifecycle.Wait(ctx) if err != nil { t.Fatalf("unexpected wait error: %v", err) } if !primaryRan { t.Fatalf("primary routine did not run.") } if !deferredRan { t.Fatalf("deferred routine did not run.") } } func TestDeferredOnPrimaryError(t *testing.T) { ctx := lifecycle.New(context.Background()) // a manager with a primary error should still run deferred routines. var deferredRan bool lifecycle.GoErr(ctx, func() error { return errors.New("primary error") }) lifecycle.Defer(ctx, func() { deferredRan = true }) err := lifecycle.Wait(ctx) if err == nil { t.Fatal("manager did not return primary routine error.") } if !deferredRan { t.Fatal("deferred manager did not run on primary manager error.") } } func TestDeferredTimeout(t *testing.T) { ctx := lifecycle.New( context.Background(), lifecycle.WithTimeout(10*time.Millisecond), ) // a manager with a deferred function that takes longer than the configured // lifecycle timeout should return with a timeout error. lifecycle.Defer(ctx, func() { time.Sleep(30 * time.Second) }) err := lifecycle.Wait(ctx) if err == nil { t.Fatal("deferred timeout expected a timeout error at 10ms.") } if err != context.DeadlineExceeded { t.Fatalf("expected 'deadline exceeded' error but got: %v", err) } } func TestContextCancel(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) ctx = lifecycle.New(ctx) start := time.Now() // a manger whose context is canceled should return before subroutines // complete with a context cancelation error. lifecycle.Go(ctx, func() { select { case <-time.After(10 * time.Second): case <-ctx.Done(): } }) cancel() err := lifecycle.Wait(ctx) if err == nil { t.Fatal("canceled context, got nil error, expected canceled error.") } if err != context.Canceled { t.Fatalf("expected 'context canceled' error but got: %v", err) } if time.Since(start) > 50*time.Millisecond { t.Fatalf("Wait did not return as soon as the context was canceled") } } func TestSignalCancels(t *testing.T) { ctx := lifecycle.New(context.Background(), lifecycle.WithTimeout(1*time.Second), lifecycle.WithSignals(syscall.SIGUSR1), // SIGUSR1 plays nicely with tests ) // A long-running goroutine, when signaled, should invoke the deferred // functions and wait up to timeout before interrupting the laggard. start := time.Now() var deferredRan int64 lifecycle.Go(ctx, func() { time.Sleep(10 * time.Millisecond) }) lifecycle.Defer(ctx, func() { atomic.StoreInt64(&deferredRan, 1) }) go func() { process, _ := os.FindProcess(syscall.Getpid()) _ = process.Signal(syscall.SIGUSR1) }() err := lifecycle.Wait(ctx) if e, ok := err.(lifecycle.ErrSignal); ok { if !strings.Contains(e.Error(), "caught signal") { t.Error("unexpected error text") } } else { t.Errorf("unexpected error on signal interrupt: %v", err) } if atomic.LoadInt64(&deferredRan) != 1 { t.Error("signaled process did not run deferred func") } if dur := time.Since(start); dur > 20*time.Millisecond { t.Errorf("func ran for more than 20ms: %v", dur) } } func TestIgnoreSignals(t *testing.T) { const timeout = 5 * time.Millisecond ctx := lifecycle.New( context.Background(), lifecycle.WithTimeout(timeout), lifecycle.WithSignals(), ) lifecycle.Defer(ctx, func() { time.Sleep(100 * time.Millisecond) }) start := time.Now() go func() { process, _ := os.FindProcess(syscall.Getpid()) _ = process.Signal(syscall.SIGUSR1) }() if err := lifecycle.Wait(ctx); err != context.DeadlineExceeded { t.Fatalf("expected deadline exceeded, got: %v", err) } if time.Since(start) < timeout { t.Fatalf("did not ignore signals") } } func TestRecover(t *testing.T) { ctx := lifecycle.New(context.Background()) err := fmt.Errorf("test panic") lifecycle.Go(ctx, func() { panic(err) }) defer func() { if r := recover(); r == nil { t.Error("did not panic") } else if r != err { t.Error("unexpected panic") } }() _ = lifecycle.Wait(ctx) } func TestDeferRecover(t *testing.T) { ctx := lifecycle.New(context.Background()) err := fmt.Errorf("test panic") lifecycle.Defer(ctx, func() { panic(err) }) defer func() { if r := recover(); r == nil { t.Error("did not panic") } else if r != err { t.Error("unexpected panic") } }() _ = lifecycle.Wait(ctx) } lifecycle-1.1.4/options.go000066400000000000000000000015271424033640200155010ustar00rootroot00000000000000package lifecycle import ( "os" "time" ) // An Option is used to configure the lifecycle manager type Option func(*manager) // 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. func WithTimeout(val time.Duration) Option { return func(o *manager) { o.timeout = val } } // 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 WithSignals(val ...os.Signal) Option { return func(o *manager) { o.sigs = val } }