├── .gitignore ├── component ├── x │ ├── grpc │ │ ├── doc.go │ │ ├── example_test.go │ │ ├── server.go │ │ └── server_test.go │ ├── go.mod │ └── go.sum ├── example_test.go ├── doc.go ├── http_server.go └── http_server_test.go ├── go.mod ├── options_test.go ├── options.go ├── utils_test.go ├── utils.go ├── doc.go ├── go.sum ├── component.go ├── .github └── workflows │ └── test.yaml ├── .golangci.yml ├── README.md ├── example_test.go ├── Makefile ├── manager.go ├── manager_test.go └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .bin/ 3 | 4 | **/coverage.* 5 | -------------------------------------------------------------------------------- /component/x/grpc/doc.go: -------------------------------------------------------------------------------- 1 | // Package grpc contains implementations of a grpc server. 2 | package grpc 3 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gojekfarm/xrun 2 | 3 | go 1.20 4 | 5 | require github.com/stretchr/testify v1.9.0 6 | 7 | require ( 8 | github.com/davecgh/go-spew v1.1.1 // indirect 9 | github.com/pmezard/go-difflib v1.0.0 // indirect 10 | gopkg.in/yaml.v3 v3.0.1 // indirect 11 | ) 12 | -------------------------------------------------------------------------------- /component/x/grpc/example_test.go: -------------------------------------------------------------------------------- 1 | package grpc_test 2 | 3 | import ( 4 | "google.golang.org/grpc" 5 | 6 | xgrpc "github.com/gojekfarm/xrun/component/x/grpc" 7 | ) 8 | 9 | func ExampleServer() { 10 | xgrpc.Server(xgrpc.Options{ 11 | Server: grpc.NewServer(), 12 | NewListener: xgrpc.NewListener(":8500"), 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /options_test.go: -------------------------------------------------------------------------------- 1 | package xrun 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestWithGracefulShutdownTimeout(t *testing.T) { 11 | expected := time.Minute 12 | 13 | m := NewManager(ShutdownTimeout(expected)) 14 | assert.Equal(t, expected, m.shutdownTimeout) 15 | } 16 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package xrun 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | const ( 8 | // NoTimeout waits indefinitely and never times out 9 | NoTimeout = time.Duration(0) 10 | ) 11 | 12 | // Option changes behaviour of Manager 13 | type Option interface { 14 | apply(*Manager) 15 | } 16 | 17 | // ShutdownTimeout allows max timeout after which Manager exits. 18 | type ShutdownTimeout time.Duration 19 | 20 | func (t ShutdownTimeout) apply(m *Manager) { m.shutdownTimeout = time.Duration(t) } 21 | -------------------------------------------------------------------------------- /utils_test.go: -------------------------------------------------------------------------------- 1 | package xrun 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestAll(t *testing.T) { 11 | r := All(NoTimeout, ComponentFunc(func(ctx context.Context) error { 12 | <-ctx.Done() 13 | return nil 14 | })) 15 | 16 | errCh := make(chan error, 1) 17 | ctx, cancel := context.WithCancel(context.Background()) 18 | go func() { 19 | errCh <- r.Run(ctx) 20 | }() 21 | 22 | cancel() 23 | assert.NoError(t, <-errCh) 24 | } 25 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package xrun 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | // All is a utility function which creates a new Manager 9 | // and adds all the components to it. Calling .Run() 10 | // on returned ComponentFunc will call Run on the Manager 11 | func All(shutdownTimeout time.Duration, components ...Component) ComponentFunc { 12 | m := NewManager(ShutdownTimeout(shutdownTimeout)) 13 | 14 | for _, c := range components { 15 | // we can ignore error as `m` is not returned 16 | // and no one can call m.Add() outside 17 | _ = m.Add(c) 18 | } 19 | 20 | return func(ctx context.Context) error { return m.Run(ctx) } 21 | } 22 | -------------------------------------------------------------------------------- /component/example_test.go: -------------------------------------------------------------------------------- 1 | package component_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "os" 8 | "os/signal" 9 | 10 | "github.com/gojekfarm/xrun/component" 11 | ) 12 | 13 | func ExampleHTTPServer() { 14 | c := component.HTTPServer(component.HTTPServerOptions{ 15 | Server: &http.Server{}, 16 | PreStart: func() { 17 | fmt.Println("starting server") 18 | }, 19 | PreStop: func() { 20 | fmt.Println("stopping server") 21 | }, 22 | }) 23 | 24 | ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill) 25 | defer stop() 26 | 27 | if err := c.Run(ctx); err != nil { 28 | os.Exit(1) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /component/x/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gojekfarm/xrun/component/x 2 | 3 | go 1.21 4 | 5 | toolchain go1.22.5 6 | 7 | require ( 8 | github.com/gojekfarm/xrun v0.4.0 9 | github.com/stretchr/testify v1.9.0 10 | golang.org/x/net v0.27.0 11 | google.golang.org/grpc v1.65.0 12 | ) 13 | 14 | require ( 15 | github.com/davecgh/go-spew v1.1.1 // indirect 16 | github.com/pmezard/go-difflib v1.0.0 // indirect 17 | github.com/stretchr/objx v0.5.2 // indirect 18 | golang.org/x/sys v0.22.0 // indirect 19 | golang.org/x/text v0.16.0 // indirect 20 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d // indirect 21 | google.golang.org/protobuf v1.34.2 // indirect 22 | gopkg.in/yaml.v3 v3.0.1 // indirect 23 | ) 24 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package xrun provides utilities around running multiple components 3 | which are long-running components, example: an HTTP server or a background worker 4 | 5 | package main 6 | 7 | import ( 8 | "net/http" 9 | "os" 10 | "os/signal" 11 | 12 | "github.com/gojekfarm/xrun" 13 | "github.com/gojekfarm/xrun/component" 14 | ) 15 | 16 | func main() { 17 | m := xrun.NewManager() 18 | server := http.Server{ 19 | Addr: ":9090", 20 | } 21 | _ = m.Add(component.HTTPServer(component.HTTPServerOptions{Server: &server})) 22 | 23 | ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt) 24 | defer stop() 25 | 26 | if err := m.Run(ctx); err != nil { 27 | os.Exit(1) 28 | } 29 | } 30 | */ 31 | package xrun 32 | -------------------------------------------------------------------------------- /component/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package component contains some commonly used implementations 3 | of long-running components like an HTTP server. 4 | 5 | package main 6 | 7 | import ( 8 | "context" 9 | "fmt" 10 | "net/http" 11 | "os" 12 | "os/signal" 13 | 14 | "github.com/gojekfarm/xrun/component" 15 | ) 16 | 17 | func main() { 18 | c := component.HTTPServer(component.HTTPServerOptions{ 19 | Server: &http.Server{}, 20 | PreStart: func() { 21 | fmt.Println("starting server") 22 | }, 23 | PreStop: func() { 24 | fmt.Println("stopping server") 25 | }, 26 | }) 27 | 28 | ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill) 29 | defer stop() 30 | 31 | if err := c.Run(ctx); err != nil { 32 | os.Exit(1) 33 | } 34 | } 35 | */ 36 | package component 37 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 6 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 7 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 9 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 10 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 11 | -------------------------------------------------------------------------------- /component.go: -------------------------------------------------------------------------------- 1 | package xrun 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | // Component allows a component to be started. 8 | // It's very important that Run blocks until 9 | // it's done running. 10 | type Component interface { 11 | // Run starts running the component. The component will stop running 12 | // when the context is closed. Run blocks until the context is closed or 13 | // an error occurs. 14 | Run(context.Context) error 15 | } 16 | 17 | // ComponentFunc is a helper to implement Component inline. 18 | // The component will stop running when the context is closed. 19 | // ComponentFunc must block until the context is closed or an error occurs. 20 | type ComponentFunc func(ctx context.Context) error 21 | 22 | // Run starts running the component. The component will stop running 23 | // when the context is closed. Run blocks until the context is closed or 24 | // an error occurs. 25 | func (f ComponentFunc) Run(ctx context.Context) error { 26 | return f(ctx) 27 | } 28 | -------------------------------------------------------------------------------- /component/http_server.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net/http" 7 | 8 | "github.com/gojekfarm/xrun" 9 | ) 10 | 11 | // HTTPServerOptions holds options for HTTPServer 12 | type HTTPServerOptions struct { 13 | Server *http.Server 14 | PreStart func() 15 | PreStop func() 16 | } 17 | 18 | // HTTPServer is a helper which returns an xrun.ComponentFunc to start an http.Server 19 | func HTTPServer(opts HTTPServerOptions) xrun.ComponentFunc { 20 | srv := opts.Server 21 | ps := opts.PreStart 22 | pst := opts.PreStop 23 | 24 | return func(ctx context.Context) error { 25 | errCh := make(chan error, 1) 26 | 27 | go func() { 28 | if ps != nil { 29 | ps() 30 | } 31 | 32 | if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { 33 | errCh <- err 34 | } 35 | }() 36 | 37 | select { 38 | case <-ctx.Done(): 39 | case err := <-errCh: 40 | return err 41 | } 42 | 43 | shutdownCtx, cancel := context.WithCancel(context.Background()) 44 | defer cancel() 45 | 46 | if pst != nil { 47 | pst() 48 | } 49 | 50 | return srv.Shutdown(shutdownCtx) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | tags: [v\d+.\d+.\d+] 6 | branches: [main] 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | go-version: [1.21.x, 1.22.x] 15 | steps: 16 | - name: Install Go 17 | uses: actions/setup-go@v4 18 | with: 19 | go-version: ${{ matrix.go-version }} 20 | - name: Checkout code 21 | uses: actions/checkout@v3 22 | - uses: actions/cache@v3 23 | with: 24 | path: ~/go/pkg/mod 25 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 26 | restore-keys: | 27 | ${{ runner.os }}-go- 28 | - name: Tools bin cache 29 | uses: actions/cache@v3 30 | with: 31 | path: .bin 32 | key: ${{ runner.os }}-${{ hashFiles('Makefile') }} 33 | - name: Install jq 34 | uses: dcarbone/install-jq-action@v1.0.1 35 | - name: Test 36 | run: make ci 37 | - name: Upload coverage to Codecov 38 | uses: codecov/codecov-action@v3 39 | with: 40 | token: ${{ secrets.CODECOV_TOKEN }} 41 | files: ./coverage.xml 42 | fail_ci_if_error: true 43 | verbose: true 44 | -------------------------------------------------------------------------------- /component/x/grpc/server.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net" 7 | 8 | "google.golang.org/grpc" 9 | 10 | "github.com/gojekfarm/xrun" 11 | ) 12 | 13 | // Options holds options for Server 14 | type Options struct { 15 | Server *grpc.Server 16 | NewListener func() (net.Listener, error) 17 | PreStart func() 18 | PreStop func() 19 | PostStop func() 20 | } 21 | 22 | // Server is a helper which returns a xrun.ComponentFunc to start a grpc.Server 23 | func Server(opts Options) xrun.ComponentFunc { 24 | srv := opts.Server 25 | nl := opts.NewListener 26 | ps := opts.PreStart 27 | pst := opts.PreStop 28 | pstp := opts.PostStop 29 | 30 | return func(ctx context.Context) error { 31 | l, err := nl() 32 | if err != nil { 33 | return err 34 | } 35 | 36 | errCh := make(chan error, 1) 37 | 38 | go func(errCh chan error) { 39 | if ps != nil { 40 | ps() 41 | } 42 | 43 | if err := srv.Serve(l); err != nil && !errors.Is(err, grpc.ErrServerStopped) { 44 | errCh <- err 45 | } 46 | }(errCh) 47 | 48 | select { 49 | case <-ctx.Done(): 50 | case err := <-errCh: 51 | return err 52 | } 53 | 54 | if pst != nil { 55 | pst() 56 | } 57 | 58 | srv.GracefulStop() 59 | 60 | if pstp != nil { 61 | pstp() 62 | } 63 | 64 | return nil 65 | } 66 | } 67 | 68 | func NewListener(address string) func() (net.Listener, error) { 69 | return func() (net.Listener, error) { 70 | return net.Listen("tcp", address) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | disable-all: true 3 | enable: 4 | - errcheck 5 | - staticcheck 6 | - unused 7 | - gosimple 8 | - ineffassign 9 | - stylecheck 10 | - typecheck 11 | - unconvert 12 | - bodyclose 13 | - dupl 14 | - goconst 15 | - gocyclo 16 | - gofmt 17 | - lll 18 | - misspell 19 | - nakedret 20 | - exportloopref 21 | - funlen 22 | - nestif 23 | - nlreturn 24 | - prealloc 25 | - rowserrcheck 26 | - unconvert 27 | - unparam 28 | - whitespace 29 | - wsl 30 | run: 31 | skip-dirs: 32 | - bin 33 | skip-files: 34 | - .*mock.*\.go$ 35 | - version.go 36 | - example_test.go 37 | modules-download-mode: readonly 38 | linters-settings: 39 | govet: 40 | check-shadowing: true 41 | enable-all: true 42 | disable: 43 | - asmdecl 44 | - assign 45 | errcheck: 46 | check-type-assertions: true 47 | misspell: 48 | locale: UK 49 | ignore-words: 50 | - initialized 51 | funlen: 52 | lines: 80 53 | statements: 40 54 | 55 | issues: 56 | exclude-use-default: false 57 | exclude: 58 | - declaration of "(err|ctx)" shadows declaration at 59 | exclude-rules: 60 | - text: "^SA1019: .* is deprecated:" 61 | linters: 62 | - staticcheck 63 | - path: _test\.go 64 | linters: 65 | - dupl 66 | - gosec 67 | - wsl 68 | - lll 69 | - funlen 70 | - nlreturn 71 | - unused 72 | - path: _test\.go 73 | text: ^Error return value is not checked$ 74 | linters: 75 | - errcheck 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # xrun 2 | 3 | [![test][github-workflow-badge]][github-workflow] 4 | [![codecov][coverage-badge]][codecov] 5 | [![PkgGoDev][pkg-go-dev-xrun-badge]][pkg-go-dev-xrun] 6 | [![Go Report Card][go-report-card-badge]][go-report-card] 7 | 8 | > Utilities around running multiple components 9 | > which are long-running components, example: 10 | > an HTTP server or a background worker 11 | 12 | ## Install 13 | 14 | ``` 15 | $ go get github.com/gojekfarm/xrun 16 | ``` 17 | 18 | ## Usage 19 | 20 | > Minimum Required Go Version: 1.20.x 21 | 22 | - [API reference][api-docs] 23 | - [Blog post explaining motivation behind xrun][blog-link] 24 | - [Reddit post][reddit-link] 25 | 26 | ###### Credits 27 | 28 | Manager source modified 29 | from [sigs.k8s.io/controller-runtime](https://github.com/kubernetes-sigs/controller-runtime/tree/a1e2ea2/pkg/manager) 30 | 31 | [github-workflow-badge]: 32 | https://github.com/gojekfarm/xrun/workflows/test/badge.svg 33 | [github-workflow]: 34 | https://github.com/gojekfarm/xrun/actions?query=workflow%3Atest 35 | [coverage-badge]: https://codecov.io/gh/gojekfarm/xrun/branch/main/graph/badge.svg?token=QPLV2ZDE84 36 | [codecov]: https://codecov.io/gh/gojekfarm/xrun 37 | [pkg-go-dev-xrun-badge]: https://pkg.go.dev/badge/github.com/gojekfarm/xrun 38 | [pkg-go-dev-xrun]: https://pkg.go.dev/mod/github.com/gojekfarm/xrun?tab=packages 39 | [go-report-card-badge]: https://goreportcard.com/badge/github.com/gojekfarm/xrun 40 | [go-report-card]: https://goreportcard.com/report/github.com/gojekfarm/xrun 41 | [api-docs]: https://pkg.go.dev/github.com/gojekfarm/xrun 42 | [blog-link]: https://ajatprabha.in/2023/05/24/intro-xrun-package-managing-component-lifecycle-go 43 | [reddit-link]: https://www.reddit.com/r/golang/comments/13r91gt/introducing_xrun_a_flexible_package_for_managing 44 | 45 | -------------------------------------------------------------------------------- /component/x/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/gojekfarm/xrun v0.4.0 h1:bp1I4PA7yRBuXY2oWZIJwBxBLnMEsQGNSwC9UlOdLW8= 4 | github.com/gojekfarm/xrun v0.4.0/go.mod h1:pJjvSfU0Th/dRsxjXZ1kHpRuF505DBTUhzr5DhRP8gI= 5 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 6 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 7 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 8 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 9 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 10 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 11 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 12 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 13 | golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= 14 | golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= 15 | golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= 16 | golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 17 | golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= 18 | golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= 19 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d h1:JU0iKnSg02Gmb5ZdV8nYsKEKsP6o/FGVWTrw4i1DA9A= 20 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240711142825-46eb208f015d/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= 21 | google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= 22 | google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= 23 | google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= 24 | google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 25 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 26 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 27 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 28 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 29 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package xrun_test 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "os" 7 | "os/signal" 8 | 9 | "github.com/gojekfarm/xrun" 10 | "github.com/gojekfarm/xrun/component" 11 | ) 12 | 13 | func ExampleNewManager() { 14 | m := xrun.NewManager(xrun.ShutdownTimeout(xrun.NoTimeout)) 15 | 16 | if err := m.Add(component.HTTPServer(component.HTTPServerOptions{Server: &http.Server{}})); err != nil { 17 | panic(err) 18 | } 19 | 20 | if err := m.Add(xrun.ComponentFunc(func(ctx context.Context) error { 21 | // Start something here in a blocking way and continue on ctx.Done 22 | <-ctx.Done() 23 | // Call Stop on component if cleanup is required 24 | return nil 25 | })); err != nil { 26 | panic(err) 27 | } 28 | 29 | // ctx is marked done (its Done channel is closed) when one of the listed signals arrives 30 | ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill) 31 | defer stop() 32 | 33 | if err := m.Run(ctx); err != nil { 34 | os.Exit(1) 35 | } 36 | } 37 | 38 | func ExampleNewManager_nested() { 39 | m1 := xrun.NewManager() 40 | if err := m1.Add(component.HTTPServer(component.HTTPServerOptions{Server: &http.Server{}})); err != nil { 41 | panic(err) 42 | } 43 | 44 | m2 := xrun.NewManager() 45 | if err := m2.Add(xrun.ComponentFunc(func(ctx context.Context) error { 46 | // Start something here in a blocking way and continue on ctx.Done 47 | <-ctx.Done() 48 | // Call Stop on component if cleanup is required 49 | return nil 50 | })); err != nil { 51 | panic(err) 52 | } 53 | 54 | gm := xrun.NewManager() 55 | if err := gm.Add(m1); err != nil { 56 | panic(err) 57 | } 58 | if err := gm.Add(m2); err != nil { 59 | panic(err) 60 | } 61 | 62 | // ctx is marked done (its Done channel is closed) when one of the listed signals arrives 63 | ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill) 64 | defer stop() 65 | 66 | // Run will start m1 and m2 simultaneously 67 | if err := gm.Run(ctx); err != nil { 68 | os.Exit(1) 69 | } 70 | } 71 | 72 | func ExampleAll() { 73 | // ctx is marked done (its Done channel is closed) when one of the listed signals arrives 74 | ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill) 75 | defer stop() 76 | 77 | if err := xrun.All(xrun.NoTimeout, 78 | component.HTTPServer(component.HTTPServerOptions{Server: &http.Server{}}), 79 | xrun.ComponentFunc(func(ctx context.Context) error { 80 | // Start something here in a blocking way and continue on ctx.Done 81 | <-ctx.Done() 82 | // Call Stop on component if cleanup is required 83 | return nil 84 | }), 85 | ).Run(ctx); err != nil { 86 | os.Exit(1) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ALL_GO_MOD_DIRS := $(shell find . -type f -name 'go.mod' -exec dirname {} \; | sort) 2 | PROJECT_DIR := $(shell dirname $(abspath $(lastword $(MAKEFILE_LIST)))) 3 | LOCAL_GO_BIN_DIR := $(PROJECT_DIR)/.bin 4 | BIN_DIR := $(if $(LOCAL_GO_BIN_DIR),$(LOCAL_GO_BIN_DIR),$(GOPATH)/bin) 5 | 6 | fmt: 7 | @$(call run-go-mod-dir,go fmt ./...,"go fmt") 8 | 9 | vet: 10 | @$(call run-go-mod-dir,go vet ./...,"go vet") 11 | 12 | lint: golangci-lint 13 | @$(call run-go-mod-dir,$(GOLANGCI_LINT) run --timeout=10m -v,".bin/golangci-lint") 14 | 15 | imports: gci 16 | @$(call run-go-mod-dir,$(GCI_BIN) write --skip-generated -s standard -s default -s "prefix(github.com/gojekfarm)" . | { grep -v -e 'skip file .*' || true; },".bin/gci") 17 | 18 | .PHONY: check 19 | check: fmt vet lint imports 20 | @git diff --quiet || test $$(git diff --name-only | grep -v -e 'go.mod$$' -e 'go.sum$$' | wc -l) -eq 0 || ( echo "The following changes (result of code generators and code checks) have been detected:" && git --no-pager diff && false ) # fail if Git working tree is dirty 21 | 22 | .PHONY: gomod.tidy 23 | gomod.tidy: 24 | @$(call run-go-mod-dir,go mod tidy,"go mod tidy") 25 | 26 | .PHONY: test 27 | test: check test-run 28 | 29 | .PHONY: ci 30 | ci: test test-cov test-xml 31 | 32 | test-run: 33 | @$(call run-go-mod-dir,go test -race -covermode=atomic -coverprofile=coverage.out ./...,"go test") 34 | 35 | test-cov: gocov 36 | @$(call run-go-mod-dir,$(GOCOV) convert coverage.out > coverage.json) 37 | @$(call run-go-mod-dir,$(GOCOV) convert coverage.out | $(GOCOV) report) 38 | 39 | test-xml: test-cov gocov-xml 40 | @jq -n '{ Packages: [ inputs.Packages ] | add }' $(shell find . -type f -name 'coverage.json' | sort) | $(GOCOVXML) > coverage.xml 41 | 42 | # ========= Helpers =========== 43 | 44 | golangci-lint: 45 | $(call install-if-needed,GOLANGCI_LINT,github.com/golangci/golangci-lint/cmd/golangci-lint,v1.59.1) 46 | 47 | gci: 48 | $(call install-if-needed,GCI_BIN,github.com/daixiang0/gci,v0.13.4) 49 | 50 | gocov: 51 | $(call install-if-needed,GOCOV,github.com/axw/gocov/gocov,v1.1.0) 52 | 53 | gocov-xml: 54 | $(call install-if-needed,GOCOVXML,github.com/AlekSi/gocov-xml,v1.1.0) 55 | 56 | is-available = $(if $(wildcard $(LOCAL_GO_BIN_DIR)/$(1)),$(LOCAL_GO_BIN_DIR)/$(1),$(if $(shell command -v $(1) 2> /dev/null),yes,no)) 57 | 58 | define install-if-needed 59 | @if [ ! -f "$(BIN_DIR)/$(notdir $(2))" ]; then \ 60 | echo "Installing $(2)@$(3) in $(BIN_DIR)" ;\ 61 | set -e ;\ 62 | TMP_DIR=$$(mktemp -d) ;\ 63 | cd $$TMP_DIR ;\ 64 | go mod init tmp ;\ 65 | go get $(2)@$(3) ;\ 66 | go build -o $(BIN_DIR)/$(notdir $(2)) $(2);\ 67 | rm -rf $$TMP_DIR ;\ 68 | fi 69 | $(eval $1 := $(BIN_DIR)/$(notdir $(2))) 70 | endef 71 | 72 | # run-go-mod-dir runs the given $1 command in all the directories with 73 | # a go.mod file 74 | define run-go-mod-dir 75 | set -e; \ 76 | for dir in $(ALL_GO_MOD_DIRS); do \ 77 | [ -z $(2) ] || echo "$(2) $${dir}/..."; \ 78 | cd "$(PROJECT_DIR)/$${dir}" && $(1); \ 79 | done; 80 | endef 81 | -------------------------------------------------------------------------------- /component/http_server_test.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "net/http" 7 | "testing" 8 | "time" 9 | 10 | "github.com/stretchr/testify/suite" 11 | 12 | "github.com/gojekfarm/xrun" 13 | ) 14 | 15 | type HTTPServerSuite struct { 16 | suite.Suite 17 | } 18 | 19 | func TestHTTPServerSuite(t *testing.T) { 20 | suite.Run(t, new(HTTPServerSuite)) 21 | } 22 | 23 | func (s *HTTPServerSuite) TestHTTPServer() { 24 | mux := http.NewServeMux() 25 | mux.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) { 26 | _, _ = w.Write([]byte("pong")) 27 | }) 28 | 29 | testcases := []struct { 30 | name string 31 | server *http.Server 32 | testFunc func(s *suite.Suite) func() bool 33 | wantErr bool 34 | wantShutdownTimeout bool 35 | }{ 36 | { 37 | name: "SuccessfulStart", 38 | server: &http.Server{ 39 | Addr: ":8888", 40 | Handler: mux, 41 | }, 42 | testFunc: func(s *suite.Suite) func() bool { 43 | return func() bool { 44 | resp, err := http.Get("http://localhost:8888/ping") 45 | s.NoError(err) 46 | defer func() { 47 | s.NoError(resp.Body.Close()) 48 | }() 49 | if d, err := io.ReadAll(resp.Body); err == nil { 50 | return string(d) == "pong" 51 | } 52 | return false 53 | } 54 | }, 55 | }, 56 | { 57 | name: "FailedStart", 58 | server: &http.Server{ 59 | Addr: ":-9090", 60 | Handler: mux, 61 | }, 62 | testFunc: func(s *suite.Suite) func() bool { 63 | i := 0 64 | return func() bool { 65 | time.Sleep(100 * time.Millisecond) 66 | i++ 67 | return i > 3 68 | } 69 | }, 70 | wantErr: true, 71 | }, 72 | { 73 | name: "UnlimitedShutdownWait", 74 | server: &http.Server{ 75 | Addr: ":9999", 76 | Handler: mux, 77 | }, 78 | }, 79 | { 80 | name: "ShutdownTimeout", 81 | server: &http.Server{ 82 | Addr: ":9999", 83 | Handler: mux, 84 | }, 85 | wantShutdownTimeout: true, 86 | wantErr: true, 87 | }, 88 | } 89 | 90 | for _, t := range testcases { 91 | s.Run(t.name, func() { 92 | var opts []xrun.Option 93 | if t.wantShutdownTimeout { 94 | opts = append(opts, xrun.ShutdownTimeout(time.Nanosecond)) 95 | } 96 | 97 | m := xrun.NewManager(opts...) 98 | st := s.T() 99 | 100 | s.NoError(m.Add(HTTPServer( 101 | HTTPServerOptions{ 102 | Server: t.server, 103 | PreStart: func() { st.Log("PreStart called") }, 104 | PreStop: func() { st.Log("PreStop called") }, 105 | }, 106 | ))) 107 | 108 | errCh := make(chan error, 1) 109 | ctx, cancel := context.WithCancel(context.Background()) 110 | go func() { 111 | errCh <- m.Run(ctx) 112 | }() 113 | 114 | time.Sleep(50 * time.Millisecond) 115 | 116 | if t.testFunc != nil { 117 | s.Eventually(t.testFunc(&s.Suite), 10*time.Second, 100*time.Millisecond) 118 | } 119 | 120 | cancel() 121 | if t.wantErr { 122 | s.Error(<-errCh) 123 | } else { 124 | s.NoError(<-errCh) 125 | } 126 | 127 | time.Sleep(50 * time.Millisecond) // for goroutine to exit 128 | }) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /component/x/grpc/server_test.go: -------------------------------------------------------------------------------- 1 | package grpc 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net" 7 | "testing" 8 | "time" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/mock" 12 | "github.com/stretchr/testify/suite" 13 | "golang.org/x/net/nettest" 14 | "google.golang.org/grpc" 15 | 16 | "github.com/gojekfarm/xrun" 17 | ) 18 | 19 | type ServerTestSuite struct { 20 | suite.Suite 21 | } 22 | 23 | func TestServerSuite(t *testing.T) { 24 | suite.Run(t, new(ServerTestSuite)) 25 | } 26 | 27 | func (s *ServerTestSuite) TestServer() { 28 | testcases := []struct { 29 | name string 30 | wantErr bool 31 | newListener func() (net.Listener, error) 32 | wantShutdownTimeout bool 33 | }{ 34 | { 35 | name: "SuccessfulStart", 36 | newListener: func() (net.Listener, error) { 37 | l, _ := nettest.NewLocalListener("tcp") 38 | return l, nil 39 | }, 40 | }, 41 | { 42 | name: "BadListener", 43 | newListener: func() (net.Listener, error) { 44 | ml := &mockListener{} 45 | ml.On("Accept").Return(nil, errors.New("unknown listen error")) 46 | ml.On("Close").Return(nil) 47 | ml.On("Addr").Return(&net.UnixAddr{ 48 | Net: "unix", 49 | Name: "test.sock", 50 | }) 51 | return ml, nil 52 | }, 53 | wantErr: true, 54 | }, 55 | { 56 | name: "GracefulShutdownError", 57 | newListener: func() (net.Listener, error) { 58 | l, _ := nettest.NewLocalListener("tcp") 59 | return l, nil 60 | }, 61 | wantShutdownTimeout: true, 62 | wantErr: true, 63 | }, 64 | { 65 | name: "ListenerCreateError", 66 | newListener: func() (net.Listener, error) { 67 | return nil, errors.New("cannot create a listener") 68 | }, 69 | wantErr: true, 70 | }, 71 | } 72 | 73 | for _, t := range testcases { 74 | s.Run(t.name, func() { 75 | var opts []xrun.Option 76 | if t.wantShutdownTimeout { 77 | opts = append(opts, xrun.ShutdownTimeout(time.Nanosecond)) 78 | } 79 | m := xrun.NewManager(opts...) 80 | srv := grpc.NewServer() 81 | 82 | l, err := t.newListener() 83 | st := s.T() 84 | 85 | s.NoError(m.Add(Server(Options{ 86 | Server: srv, 87 | NewListener: func() (net.Listener, error) { 88 | return l, err 89 | }, 90 | PreStart: func() { st.Log("PreStart called") }, 91 | PreStop: func() { st.Log("PreStop called") }, 92 | PostStop: func() { st.Log("PostStop called") }, 93 | }))) 94 | 95 | errCh := make(chan error, 1) 96 | ctx, cancel := context.WithCancel(context.Background()) 97 | go func() { 98 | errCh <- m.Run(ctx) 99 | }() 100 | 101 | time.Sleep(50 * time.Millisecond) 102 | 103 | cancel() 104 | if t.wantErr { 105 | s.Error(<-errCh) 106 | } else { 107 | s.NoError(<-errCh) 108 | } 109 | 110 | time.Sleep(50 * time.Millisecond) // wait for goroutine to finish 111 | 112 | if ml, ok := l.(*mockListener); ok { 113 | ml.AssertExpectations(s.T()) 114 | } 115 | }) 116 | } 117 | } 118 | 119 | type mockListener struct { 120 | mock.Mock 121 | } 122 | 123 | func (m *mockListener) Accept() (net.Conn, error) { 124 | args := m.Called() 125 | if err := args.Error(1); err != nil { 126 | return nil, err 127 | } 128 | return args.Get(0).(net.Conn), nil 129 | } 130 | 131 | func (m *mockListener) Close() error { 132 | return m.Called().Error(0) 133 | } 134 | 135 | func (m *mockListener) Addr() net.Addr { 136 | return m.Called().Get(0).(net.Addr) 137 | } 138 | 139 | func TestNewListener(t *testing.T) { 140 | f := NewListener(":0") 141 | l, err := f() 142 | 143 | assert.NoError(t, err) 144 | assert.NotNil(t, l) 145 | } 146 | -------------------------------------------------------------------------------- /manager.go: -------------------------------------------------------------------------------- 1 | package xrun 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "sync" 8 | "time" 9 | ) 10 | 11 | // NewManager creates a Manager and applies provided Option 12 | func NewManager(opts ...Option) *Manager { 13 | m := &Manager{shutdownTimeout: NoTimeout} 14 | 15 | for _, o := range opts { 16 | o.apply(m) 17 | } 18 | 19 | return m 20 | } 21 | 22 | // Manager helps to run multiple components 23 | // and waits for them to complete 24 | type Manager struct { 25 | mu sync.Mutex 26 | 27 | internalCtx context.Context 28 | internalCancel context.CancelFunc 29 | 30 | components []Component 31 | wg sync.WaitGroup 32 | 33 | started bool 34 | stopping bool 35 | shutdownTimeout time.Duration 36 | shutdownCtx context.Context 37 | errChan chan error 38 | } 39 | 40 | // Add will enqueue the Component to run it, 41 | // last added component will be started first 42 | func (m *Manager) Add(c Component) error { 43 | m.mu.Lock() 44 | defer m.mu.Unlock() 45 | 46 | if m.stopping { 47 | return errors.New("can't accept new component as stop procedure is already engaged") 48 | } 49 | 50 | if m.started { 51 | return errors.New("can't accept new component as manager has already started") 52 | } 53 | 54 | m.components = append(m.components, c) 55 | 56 | return nil 57 | } 58 | 59 | // Run starts running the registered components. The components will stop running 60 | // when the context is closed. Run blocks until the context is closed or 61 | // an error occurs. 62 | func (m *Manager) Run(ctx context.Context) (err error) { 63 | m.internalCtx, m.internalCancel = context.WithCancel(ctx) 64 | 65 | defer func() { 66 | if stopErr := m.engageStopProcedure(); stopErr != nil { 67 | err = stopErr 68 | } 69 | }() 70 | 71 | m.errChan = make(chan error) 72 | 73 | go m.start() 74 | 75 | select { 76 | case <-ctx.Done(): 77 | return 78 | case err := <-m.errChan: 79 | return err 80 | } 81 | } 82 | 83 | func (m *Manager) start() { 84 | m.mu.Lock() 85 | defer m.mu.Unlock() 86 | m.started = true 87 | 88 | for _, c := range m.components { 89 | if c != nil { 90 | m.startComponent(c) 91 | } 92 | } 93 | } 94 | 95 | func (m *Manager) startComponent(c Component) { 96 | m.wg.Add(1) 97 | 98 | go func() { 99 | defer m.wg.Done() 100 | 101 | if err := c.Run(m.internalCtx); err != nil && !errors.Is(err, context.Canceled) { 102 | m.errChan <- err 103 | } 104 | }() 105 | } 106 | 107 | func (m *Manager) engageStopProcedure() error { 108 | shutdownCancel := m.cancelFunc() 109 | defer shutdownCancel() 110 | 111 | m.internalCancel() 112 | 113 | m.mu.Lock() 114 | defer m.mu.Unlock() 115 | m.stopping = true 116 | 117 | var retErr error 118 | 119 | retErrCh := make(chan error, 1) 120 | 121 | go m.aggregateErrors(retErrCh) 122 | go func() { 123 | m.wg.Wait() 124 | close(m.errChan) 125 | 126 | retErr = <-retErrCh 127 | 128 | shutdownCancel() 129 | }() 130 | 131 | <-m.shutdownCtx.Done() 132 | 133 | if err := m.shutdownCtx.Err(); err != nil && !errors.Is(err, context.Canceled) { 134 | return fmt.Errorf("not all components were shutdown completely within grace period(%s): %w", m.shutdownTimeout, err) 135 | } 136 | 137 | return retErr 138 | } 139 | 140 | func (m *Manager) cancelFunc() context.CancelFunc { 141 | var shutdownCancel context.CancelFunc 142 | if m.shutdownTimeout > 0 { 143 | m.shutdownCtx, shutdownCancel = context.WithTimeout(context.Background(), m.shutdownTimeout) 144 | } else { 145 | m.shutdownCtx, shutdownCancel = context.WithCancel(context.Background()) 146 | } 147 | 148 | return shutdownCancel 149 | } 150 | 151 | func (m *Manager) aggregateErrors(ch chan<- error) { 152 | var r error 153 | for err := range m.errChan { 154 | r = errors.Join(r, err) 155 | } 156 | ch <- r 157 | } 158 | -------------------------------------------------------------------------------- /manager_test.go: -------------------------------------------------------------------------------- 1 | package xrun 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/suite" 11 | ) 12 | 13 | type ManagerSuite struct { 14 | suite.Suite 15 | } 16 | 17 | func TestManagerSuite(t *testing.T) { 18 | suite.Run(t, new(ManagerSuite)) 19 | } 20 | 21 | func (s *ManagerSuite) TestNewManager() { 22 | testcases := []struct { 23 | name string 24 | wantErr assert.ErrorAssertionFunc 25 | wantAddErr bool 26 | components []Component 27 | options []Option 28 | }{ 29 | { 30 | name: "WithZeroComponents", 31 | wantErr: assert.NoError, 32 | }, 33 | { 34 | name: "WithOneComponent", 35 | wantErr: assert.NoError, 36 | components: []Component{ 37 | ComponentFunc(func(ctx context.Context) error { 38 | time.Sleep(300 * time.Millisecond) 39 | <-ctx.Done() 40 | return nil 41 | }), 42 | }, 43 | }, 44 | { 45 | name: "WithErrorOnComponentStart", 46 | wantErr: assert.Error, 47 | components: []Component{ 48 | ComponentFunc(func(ctx context.Context) error { 49 | return errors.New("start error") 50 | }), 51 | }, 52 | }, 53 | { 54 | name: "WithGracefulShutdownErrorOnOneComponent", 55 | options: []Option{ShutdownTimeout(time.Second)}, 56 | wantErr: assert.Error, 57 | components: []Component{ 58 | ComponentFunc(func(ctx context.Context) error { 59 | time.Sleep(100 * time.Millisecond) 60 | <-ctx.Done() 61 | time.Sleep(100 * time.Millisecond) 62 | return nil 63 | }), 64 | ComponentFunc(func(ctx context.Context) error { 65 | <-ctx.Done() 66 | time.Sleep(time.Minute) 67 | return nil 68 | }), 69 | }, 70 | }, 71 | { 72 | name: "WithGracefulShutdownForTwoLongRunningComponents", 73 | options: []Option{ShutdownTimeout(time.Minute)}, 74 | wantErr: assert.NoError, 75 | components: []Component{ 76 | ComponentFunc(func(ctx context.Context) error { 77 | time.Sleep(500 * time.Millisecond) 78 | <-ctx.Done() 79 | time.Sleep(500 * time.Millisecond) 80 | return nil 81 | }), 82 | ComponentFunc(func(ctx context.Context) error { 83 | time.Sleep(100 * time.Millisecond) 84 | <-ctx.Done() 85 | time.Sleep(time.Second) 86 | return nil 87 | }), 88 | }, 89 | }, 90 | { 91 | name: "UndefinedGracefulShutdown", 92 | wantErr: assert.NoError, 93 | components: []Component{ 94 | ComponentFunc(func(ctx context.Context) error { 95 | <-ctx.Done() 96 | time.Sleep(2 * time.Second) 97 | return nil 98 | }), 99 | }, 100 | }, 101 | { 102 | name: "ShutdownWhenComponentReturnsContextErrorAsItIs", 103 | wantErr: assert.NoError, 104 | components: []Component{ 105 | ComponentFunc(func(ctx context.Context) error { 106 | time.Sleep(100 * time.Millisecond) 107 | <-ctx.Done() 108 | time.Sleep(200 * time.Millisecond) 109 | return nil 110 | }), 111 | ComponentFunc(func(ctx context.Context) error { 112 | time.Sleep(100 * time.Millisecond) 113 | <-ctx.Done() 114 | time.Sleep(100 * time.Millisecond) 115 | return ctx.Err() 116 | }), 117 | }, 118 | }, 119 | { 120 | name: "ShutdownWhenOneComponentReturnsErrorOnExit", 121 | wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { 122 | return assert.EqualError(t, err, "shutdown error", i...) 123 | }, 124 | components: []Component{ 125 | ComponentFunc(func(ctx context.Context) error { 126 | time.Sleep(100 * time.Millisecond) 127 | <-ctx.Done() 128 | time.Sleep(200 * time.Millisecond) 129 | return nil 130 | }), 131 | ComponentFunc(func(ctx context.Context) error { 132 | time.Sleep(100 * time.Millisecond) 133 | <-ctx.Done() 134 | time.Sleep(100 * time.Millisecond) 135 | return errors.New("shutdown error") 136 | }), 137 | }, 138 | }, 139 | { 140 | name: "ShutdownWhenMoreThanOneComponentReturnsErrorOnExit", 141 | wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { 142 | return assert.EqualError(t, err, `shutdown error 2 143 | shutdown error 1`, i...) 144 | }, 145 | components: []Component{ 146 | ComponentFunc(func(ctx context.Context) error { 147 | <-ctx.Done() 148 | time.Sleep(200 * time.Millisecond) 149 | return nil 150 | }), 151 | ComponentFunc(func(ctx context.Context) error { 152 | <-ctx.Done() 153 | time.Sleep(300 * time.Millisecond) 154 | return errors.New("shutdown error 1") 155 | }), 156 | ComponentFunc(func(ctx context.Context) error { 157 | <-ctx.Done() 158 | time.Sleep(200 * time.Millisecond) 159 | return errors.New("shutdown error 2") 160 | }), 161 | }, 162 | }, 163 | } 164 | 165 | for _, t := range testcases { 166 | s.Run(t.name, func() { 167 | m := NewManager(t.options...) 168 | 169 | for _, r := range t.components { 170 | s.NoError(m.Add(r)) 171 | } 172 | 173 | ctx, cancel := context.WithCancel(context.Background()) 174 | 175 | errCh := make(chan error, 1) 176 | go func() { 177 | errCh <- m.Run(ctx) 178 | }() 179 | 180 | time.Sleep(300 * time.Millisecond) 181 | cancel() 182 | 183 | t.wantErr(s.T(), <-errCh) 184 | }) 185 | } 186 | } 187 | 188 | func (s *ManagerSuite) TestAddNewComponentAfterStop() { 189 | m := NewManager() 190 | 191 | ctx, cancel := context.WithCancel(context.Background()) 192 | 193 | errCh := make(chan error, 1) 194 | go func() { 195 | errCh <- m.Run(ctx) 196 | }() 197 | 198 | time.Sleep(100 * time.Millisecond) 199 | cancel() 200 | 201 | s.NoError(<-errCh) 202 | 203 | s.EqualError(m.Add(ComponentFunc(func(ctx context.Context) error { 204 | return nil 205 | })), "can't accept new component as stop procedure is already engaged") 206 | } 207 | 208 | func (s *ManagerSuite) TestAddNewComponentAfterStart() { 209 | m := NewManager() 210 | 211 | ctx, cancel := context.WithCancel(context.Background()) 212 | 213 | errCh := make(chan error, 1) 214 | go func() { 215 | errCh <- m.Run(ctx) 216 | }() 217 | 218 | time.Sleep(100 * time.Millisecond) 219 | 220 | s.EqualError(m.Add(ComponentFunc(func(ctx context.Context) error { 221 | return nil 222 | })), "can't accept new component as manager has already started") 223 | cancel() 224 | 225 | s.NoError(<-errCh) 226 | } 227 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [2021] [Ajat Prabha] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------