├── .gitignore ├── go.mod ├── README.md ├── go.sum ├── .github └── workflows │ └── ci.yaml ├── pkg ├── flag │ └── flag.go ├── test │ ├── irqhandler_test.go │ └── irqhandler.go ├── version │ ├── version_test.go │ └── version.go ├── signal │ ├── handler.go │ └── handler_test.go └── log │ └── logging.go ├── lifecycle.go ├── lifecycle_test.go ├── example_group_test.go ├── LICENSE ├── group_test.go └── group.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | .DS_Store 4 | *.swp 5 | *.code-workspace 6 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tetratelabs/run 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/logrusorgru/aurora v2.0.3+incompatible 7 | github.com/spf13/pflag v1.0.5 8 | github.com/tetratelabs/telemetry v0.7.1 9 | ) 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # run 2 | 3 | This package contains a universal mechanism to manage goroutine lifecycles. It 4 | implements an actor-runner with deterministic teardown. It uses the 5 | https://github.com/oklog/run/ package as its basis and enhances it with 6 | configuration registration and validation as well as pre-run phase logic. 7 | 8 | See godoc for information how to use 9 | [run.Group](https://pkg.go.dev/github.com/tetratelabs/run) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8= 2 | github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= 3 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 4 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 5 | github.com/tetratelabs/telemetry v0.7.1 h1:IiDiiZgShKlHjPFgCAE6ZD4URH3r8yj7SDAEN/ImHYA= 6 | github.com/tetratelabs/telemetry v0.7.1/go.mod h1:jDUcf1A2u4F5V1io5RdipM/bKz/hFCsx/RAgGopC37s= 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | jobs: 10 | test: 11 | strategy: 12 | matrix: 13 | go-version: 14 | - 1.20.x 15 | - 1.21.x 16 | - 1.22.x 17 | os: 18 | - ubuntu-latest 19 | runs-on: ${{ matrix.os }} 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: actions/setup-go@v5 23 | with: 24 | go-version: ${{ matrix.go-version }} 25 | - run: go test ./... 26 | env: 27 | GOMAXPROCS: 4 28 | -------------------------------------------------------------------------------- /pkg/flag/flag.go: -------------------------------------------------------------------------------- 1 | package flag 2 | 3 | import "fmt" 4 | 5 | // ValidationError provides the ability to create constant errors for run.Group 6 | // validation errors, e.g. incorrect flag values. 7 | type ValidationError string 8 | 9 | // Error implements the built-in error interface. 10 | func (v ValidationError) Error() string { return string(v) } 11 | 12 | // NewValidationError provides a convenient helper function to create flag 13 | // validation errors usable by run.Config implementations. 14 | func NewValidationError(flag string, reason error) error { 15 | return fmt.Errorf(FlagErr, flag, reason) 16 | } 17 | 18 | const ( 19 | // FlagErr can be used as formatting string for flag related validation 20 | // errors where the first variable lists the flag name and the second 21 | // variable is the actual error. 22 | FlagErr = "--%s error: %w" 23 | 24 | // ErrRequired is returned when required config options are not provided. 25 | ErrRequired ValidationError = "required" 26 | 27 | // ErrInvalidPath is returned when a path config option is invalid. 28 | ErrInvalidPath ValidationError = "invalid path" 29 | 30 | // ErrInvalidVal is returned when the value passed into a flag argument is invalid. 31 | ErrInvalidVal ValidationError = "invalid value" 32 | ) 33 | -------------------------------------------------------------------------------- /lifecycle.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tetrate, Inc 2021. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package run 16 | 17 | import ( 18 | "context" 19 | ) 20 | 21 | // Lifecycle tracks application lifecycle. 22 | // And allows anyone to attach to it by exposing a `context.Context` that will 23 | // end at the shutdown phase. 24 | type Lifecycle interface { 25 | Unit 26 | 27 | // Context returns a context that gets cancelled when application 28 | // is stopped. 29 | Context() context.Context 30 | } 31 | 32 | // NewLifecycle returns a new application lifecycle tracker. 33 | func NewLifecycle() Lifecycle { 34 | ctx, cancel := context.WithCancel(context.Background()) 35 | return &lifecycle{ 36 | ctx: ctx, 37 | cancel: cancel, 38 | } 39 | } 40 | 41 | type lifecycle struct { 42 | ctx context.Context 43 | cancel context.CancelFunc 44 | } 45 | 46 | var _ Service = (*lifecycle)(nil) 47 | 48 | // Name implements Unit. 49 | func (l *lifecycle) Name() string { 50 | return "lifecycle-tracker" 51 | } 52 | 53 | // Serve implements Server. 54 | func (l *lifecycle) Serve() error { 55 | <-l.ctx.Done() 56 | return nil 57 | } 58 | 59 | // GracefulStop implements Server. 60 | func (l *lifecycle) GracefulStop() { 61 | l.cancel() 62 | } 63 | 64 | func (l *lifecycle) Context() context.Context { 65 | return l.ctx 66 | } 67 | -------------------------------------------------------------------------------- /pkg/test/irqhandler_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tetrate, Inc 2022. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package test_test 16 | 17 | import ( 18 | "runtime" 19 | "sync" 20 | "testing" 21 | 22 | "github.com/tetratelabs/run" 23 | "github.com/tetratelabs/run/pkg/test" 24 | "github.com/tetratelabs/telemetry" 25 | ) 26 | 27 | // TestIRQService test if irqs returns a valid error for deliberate termination. 28 | func TestIRQService(t *testing.T) { 29 | if ps := runtime.GOMAXPROCS(runtime.NumCPU()); ps < 3 { 30 | t.Skipf("GOMAXPROCS not sufficient for test: %d", ps) 31 | } 32 | var ( 33 | g = &run.Group{Name: "test", Logger: telemetry.NoopLogger()} 34 | irqs = test.NewIRQService(func() {}) 35 | ) 36 | 37 | g.Register(irqs) 38 | if err := g.RunConfig(); err != nil { 39 | t.Fatalf("configuring run.Group: %v", err) 40 | } 41 | 42 | wg := sync.WaitGroup{} 43 | wg.Add(2) 44 | 45 | t.Run("primary thread", func(t *testing.T) { 46 | t.Parallel() 47 | defer wg.Done() 48 | 49 | if err := g.Run(); err != nil { 50 | t.Fatalf("server exit: %v", err) 51 | } 52 | }) 53 | 54 | // Try to close the run group on primary thread from the secondary thread. 55 | t.Run("secondary thread", func(t *testing.T) { 56 | t.Parallel() 57 | defer func() { 58 | irqs.Close() 59 | wg.Done() 60 | }() 61 | }) 62 | 63 | t.Run("waiter thread", func(t *testing.T) { 64 | t.Parallel() 65 | wg.Wait() 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /lifecycle_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tetrate, Inc 2021. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package run_test 16 | 17 | import ( 18 | "context" 19 | "reflect" 20 | "testing" 21 | "time" 22 | 23 | "github.com/tetratelabs/run" 24 | ) 25 | 26 | func TestLifecycle(t *testing.T) { 27 | l := run.NewLifecycle() 28 | 29 | if want, have := "lifecycle-tracker", l.Name(); want != have { 30 | t.Errorf("unexpected unit name: want %q, have %q", want, have) 31 | } 32 | 33 | errCh := make(chan error, 1) 34 | go func() { 35 | defer close(errCh) 36 | 37 | errCh <- (l.(run.Service)).Serve() 38 | }() 39 | 40 | waitFor := time.Now().Add(25 * time.Millisecond) 41 | for { 42 | err := l.Context().Err() 43 | if err != nil { 44 | t.Fatalf("unexpected context error: %+v", err) 45 | } 46 | if time.Now().After(waitFor) { 47 | break 48 | } 49 | time.Sleep(5 * time.Millisecond) 50 | } 51 | 52 | (l.(run.Service)).GracefulStop() 53 | 54 | ctx := l.Context() 55 | 56 | if want, have := context.Canceled, ctx.Err(); want != have { 57 | t.Errorf("unexpected error: want %v, have %v", want, have) 58 | } 59 | 60 | if !isChannelClosed(ctx.Done()) { 61 | t.Errorf("expected context.Done() to be closed") 62 | } 63 | } 64 | 65 | func isChannelClosed(val interface{}) bool { 66 | channelValue := reflect.ValueOf(val) 67 | winnerIndex, _, open := reflect.Select([]reflect.SelectCase{ 68 | {Dir: reflect.SelectRecv, Chan: channelValue}, 69 | {Dir: reflect.SelectDefault}, 70 | }) 71 | var closed bool 72 | if winnerIndex == 0 { 73 | closed = !open 74 | } else if winnerIndex == 1 { 75 | closed = false 76 | } 77 | return closed 78 | } 79 | -------------------------------------------------------------------------------- /pkg/version/version_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tetrate, Inc 2021. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package version 16 | 17 | import ( 18 | "testing" 19 | ) 20 | 21 | func TestParse(t *testing.T) { 22 | type versionStringTest struct { 23 | input string 24 | want string 25 | } 26 | // versionString syntax: 27 | // --- 28 | tests := []versionStringTest{ 29 | {input: "0.6.6-0-g12345678-master", want: "v0.6.6"}, 30 | {input: "0.6.6-0-g12345678-main", want: "v0.6.6"}, 31 | {input: "0.6.6-0-g12345678-HEAD", want: "v0.6.6"}, 32 | {input: "0.6.6-0-g87654321-custom", want: "v0.6.6-custom"}, 33 | {input: "0.6.6-2-gabcdef01-master", want: "v0.6.6-master (abcdef01, +2)"}, 34 | {input: "0.6.6-1-g123456ab-custom", want: "v0.6.6-custom (123456ab, +1)"}, 35 | {input: "0.6.6-rc1-0-g12345678-master", want: "v0.6.6-rc1"}, 36 | {input: "0.6.6-internal-rc1-0-g12345678-master", want: "v0.6.6-internal-rc1"}, 37 | {input: "0.6.6-internal-rc1-0-g12345678-main", want: "v0.6.6-internal-rc1"}, 38 | {input: "0.6.6-internal-rc1-0-g12345678-HEAD", want: "v0.6.6-internal-rc1"}, 39 | {input: "0.6.6-rc1-g12345678-master", want: "v0.0.0-unofficial"}, // unparseable: no commits present 40 | {input: "", want: "v0.0.0-unofficial"}, 41 | {input: "0.6.6-rc1-15-g12345678-want-more-branch", want: "v0.6.6-rc1-want-more-branch (12345678, +15)"}, // branch name with hypens should be captured. 42 | {input: "v0.6.6-rc1-15-g12345678-want-more-branch", want: "v0.6.6-rc1-want-more-branch (12345678, +15)"}, 43 | } 44 | for _, test := range tests { 45 | t.Run(test.input, func(t *testing.T) { 46 | build = test.input 47 | if have := Parse(); test.want != have { 48 | t.Errorf("want: %s, have: %s", test.want, have) 49 | } 50 | }) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /pkg/signal/handler.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tetrate, Inc 2021. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package signal implements a run.GroupService handling incoming unix signals. 16 | package signal 17 | 18 | import ( 19 | "context" 20 | "fmt" 21 | "os" 22 | "os/signal" 23 | "syscall" 24 | 25 | "github.com/tetratelabs/run" 26 | ) 27 | 28 | // Handler implements a unix signal handler as run.GroupService. 29 | type Handler struct { 30 | // RefreshCallback is called when a syscall.SIGHUP is received. 31 | // If the callback returns an error, the signal handler is stopped. In a 32 | // run.Group environment this means the entire run.Group is requested to 33 | // stop. 34 | RefreshCallback func() error 35 | 36 | signal chan os.Signal 37 | } 38 | 39 | // Name implements run.Unit. 40 | func (h *Handler) Name() string { 41 | return "signal" 42 | } 43 | 44 | // PreRun implements run.PreRunner to initialize the handler. 45 | func (h *Handler) PreRun() error { 46 | // Notify uses a non-blocking channel send. If handling a HUP and receiving 47 | // an INT shortly after, it might get lost if we don't use a buffered 48 | // channel here. 49 | // E.g. https://gist.github.com/basvanbeek/c0e2ef60b73c8a5d5028ee0cf1afb576 50 | h.signal = make(chan os.Signal, 2) 51 | signal.Notify(h.signal, 52 | syscall.SIGHUP, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM) 53 | return nil 54 | } 55 | 56 | // ServeContext implements run.ServiceContext and listens for incoming unix 57 | // signals. 58 | // If a callback handler was registered it will be executed if a "SIGHUP" is 59 | // received. If the callback handler returns an error it will exit in error and 60 | // initiate Group shutdown if used in a run.Group environment. 61 | func (h *Handler) ServeContext(ctx context.Context) error { 62 | for { 63 | select { 64 | case sig := <-h.signal: 65 | switch sig { 66 | case syscall.SIGHUP: 67 | if h.RefreshCallback != nil { 68 | if err := h.RefreshCallback(); err != nil { 69 | return fmt.Errorf("error on signal %s: %w", sig, err) 70 | } 71 | } 72 | case syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM: 73 | return fmt.Errorf("%s %w", sig, run.ErrRequestedShutdown) 74 | } 75 | case <-ctx.Done(): 76 | signal.Stop(h.signal) 77 | close(h.signal) 78 | return nil 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /pkg/log/logging.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tetrate, Inc 2021. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package log 16 | 17 | import ( 18 | "context" 19 | "log" 20 | "time" 21 | 22 | "github.com/tetratelabs/telemetry" 23 | ) 24 | 25 | // Logger holds a very bare bones minimal implementation of telemetry.Logging. 26 | // It is used by run.Group when not wired up with an explicit Logging 27 | // implementation. 28 | type Logger struct { 29 | args []interface{} 30 | } 31 | 32 | func (l *Logger) Debug(msg string, keyValuePairs ...interface{}) { 33 | args := []interface{}{ 34 | time.Now().Format("2006-01-02 15:04:05.000000 "), 35 | "msg", msg, "level", "debug", 36 | } 37 | args = append(args, l.args...) 38 | args = append(args, keyValuePairs...) 39 | log.Println(args...) 40 | } 41 | 42 | func (l *Logger) Info(msg string, keyValuePairs ...interface{}) { 43 | args := []interface{}{ 44 | time.Now().Format("2006-01-02 15:04:05.000000 "), 45 | "msg", msg, "level", "info", 46 | } 47 | args = append(args, l.args...) 48 | args = append(args, keyValuePairs...) 49 | log.Println(args...) 50 | } 51 | 52 | func (l *Logger) Error(msg string, err error, keyValuePairs ...interface{}) { 53 | args := []interface{}{ 54 | time.Now().Format("2006-01-02 15:04:05.000000 "), 55 | "msg", msg, "level", "error", "error", err.Error(), 56 | } 57 | args = append(args, l.args...) 58 | args = append(args, keyValuePairs...) 59 | log.Println(args...) 60 | } 61 | 62 | func (l Logger) With(keyValuePairs ...interface{}) telemetry.Logger { 63 | newLogger := l.Clone().(*Logger) 64 | newLogger.args = append(newLogger.args, keyValuePairs...) 65 | return newLogger 66 | } 67 | 68 | func (l *Logger) Clone() telemetry.Logger { 69 | return &Logger{ 70 | args: append(([]interface{})(nil), l.args...), 71 | } 72 | } 73 | 74 | func (l *Logger) Level() telemetry.Level { 75 | // not used by run.Group 76 | return telemetry.LevelNone 77 | } 78 | 79 | func (l *Logger) SetLevel(telemetry.Level) { 80 | // not used by run.Group 81 | } 82 | 83 | func (l *Logger) KeyValuesToContext(ctx context.Context, _ ...interface{}) context.Context { 84 | // not used by run.Group 85 | return ctx 86 | } 87 | 88 | func (l *Logger) Context(_ context.Context) telemetry.Logger { 89 | // not used by run.Group 90 | return l 91 | } 92 | 93 | func (l *Logger) Metric(_ telemetry.Metric) telemetry.Logger { 94 | // not used by run.Group 95 | return l 96 | } 97 | 98 | var _ telemetry.Logger = (*Logger)(nil) 99 | -------------------------------------------------------------------------------- /pkg/signal/handler_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tetrate, Inc 2021. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package signal 16 | 17 | import ( 18 | "errors" 19 | "syscall" 20 | "testing" 21 | "time" 22 | 23 | "github.com/tetratelabs/run" 24 | "github.com/tetratelabs/run/pkg/test" 25 | ) 26 | 27 | var ( 28 | errClose = errors.New("requested close") 29 | errIRQ = errors.New("interrupt") 30 | ) 31 | 32 | func TestSignalHandlerStop(t *testing.T) { 33 | var ( 34 | g = run.Group{} 35 | s Handler 36 | ) 37 | 38 | // add our signal handler to Group 39 | g.Register(&s) 40 | 41 | // add our interrupter 42 | g.Register(&test.TestSvc{ 43 | SvcName: "irqsvc", 44 | Execute: func() error { return errClose }, 45 | }) 46 | 47 | // start group 48 | res := make(chan error) 49 | go func() { res <- g.Run() }() 50 | 51 | select { 52 | case err := <-res: 53 | if !errors.Is(err, errClose) { 54 | t.Errorf("expected clean shutdown, got %v", err) 55 | } 56 | case <-time.After(100 * time.Millisecond): 57 | t.Errorf("timeout") 58 | } 59 | } 60 | 61 | func TestSignalHandlerSignals(t *testing.T) { 62 | var ( 63 | s Handler 64 | errHUP = errors.New("sigHUP called") 65 | ) 66 | 67 | tests := []struct { 68 | action func() 69 | err error 70 | }{ 71 | {action: s.sendHUP, err: errHUP}, 72 | {action: s.sendQUIT, err: nil}, 73 | } 74 | for idx, tt := range tests { 75 | var ( 76 | g = run.Group{} 77 | irq = make(chan error) 78 | ) 79 | 80 | s.RefreshCallback = func() error { 81 | return errHUP 82 | } 83 | 84 | // add our signal handler to Group 85 | g.Register(&s) 86 | 87 | // add our interrupter 88 | g.Register(&test.TestSvc{ 89 | SvcName: "irqsvc", 90 | Execute: func() error { 91 | tt.action() 92 | return <-irq 93 | }, 94 | Interrupt: func() { irq <- errIRQ }, 95 | }) 96 | 97 | // start group 98 | res := make(chan error) 99 | go func() { res <- g.Run() }() 100 | 101 | select { 102 | case err := <-res: 103 | if !errors.Is(err, tt.err) { 104 | t.Errorf("[%d] expected %v, got %v", idx, tt.err, err) 105 | } 106 | case <-time.After(100 * time.Millisecond): 107 | t.Errorf("[%d] timeout", idx) 108 | } 109 | 110 | } 111 | } 112 | 113 | // sendHUP is for test purposes 114 | func (h *Handler) sendHUP() { 115 | h.signal <- syscall.SIGHUP 116 | } 117 | 118 | // sendQUIT is for test purposes 119 | func (h *Handler) sendQUIT() { 120 | h.signal <- syscall.SIGQUIT 121 | } 122 | -------------------------------------------------------------------------------- /pkg/test/irqhandler.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tetrate, Inc 2021. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package test adds helper utilities for testing run.Group enabled services. 16 | package test 17 | 18 | import ( 19 | "errors" 20 | "io" 21 | "sync" 22 | 23 | "github.com/tetratelabs/run" 24 | ) 25 | 26 | // IRQService is a run.Service and io.Closer implementation. It can be 27 | // registered with run.Group and calling Close will initiate shutdown of the 28 | // run.Group. 29 | // This is primarily used for unit tests of run.Group enabled services and 30 | // handlers. 31 | type IRQService interface { 32 | run.Service 33 | io.Closer 34 | } 35 | 36 | // NewIRQService returns a IRQService for usage in run.Group tests. 37 | // Use the Close() method to shutdown a run.Group. 38 | // The GracefulStop() method will be called automatically when run.Group is 39 | // shutting down. Both Serve() and GracefulStop() should not be called outside 40 | // of the internal run.Group logic as run.Service is to be managed by run.Group 41 | func NewIRQService(cleanup func()) IRQService { 42 | return &irqSvc{ 43 | irq: make(chan error), 44 | cfn: cleanup, 45 | } 46 | } 47 | 48 | type irqSvc struct { 49 | irq chan error 50 | cfn func() 51 | mu sync.Mutex 52 | } 53 | 54 | func (i *irqSvc) Name() string { 55 | return "irqsvc" 56 | } 57 | 58 | func (i *irqSvc) Serve() error { 59 | return <-i.irq 60 | } 61 | 62 | // GracefulStop is managed by run.Group. Do not call directly. 63 | func (i *irqSvc) GracefulStop() { 64 | i.cfn() 65 | 66 | i.mu.Lock() 67 | defer i.mu.Unlock() 68 | 69 | if i.irq != nil { 70 | close(i.irq) 71 | i.irq = nil 72 | } 73 | } 74 | 75 | // Close signals the IRQService to shutdown and run.Group is responsible for 76 | // cleaning up and calling GracefulStop() on all registered units. 77 | func (i *irqSvc) Close() error { 78 | i.mu.Lock() 79 | defer i.mu.Unlock() 80 | 81 | if i.irq != nil { 82 | i.irq <- run.ErrRequestedShutdown 83 | } 84 | return nil 85 | } 86 | 87 | // TestSvc allows one to quickly bootstrap a run.GroupService from simple 88 | // functions. This is especially useful for unit tests. 89 | type TestSvc struct { 90 | SvcName string 91 | Execute func() error 92 | Interrupt func() 93 | } 94 | 95 | // Name implements run.Unit. 96 | func (t TestSvc) Name() string { 97 | return t.SvcName 98 | } 99 | 100 | // Serve implements run.Service 101 | func (t TestSvc) Serve() error { 102 | if t.Execute == nil { 103 | return errors.New("missing execute function") 104 | } 105 | return t.Execute() 106 | } 107 | 108 | // GracefulStop implements run.Service 109 | func (t TestSvc) GracefulStop() { 110 | if t.Interrupt == nil { 111 | return 112 | } 113 | t.Interrupt() 114 | } 115 | -------------------------------------------------------------------------------- /example_group_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tetrate, Inc 2021. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package run_test 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | "os" 21 | 22 | "github.com/spf13/pflag" 23 | "github.com/tetratelabs/run" 24 | "github.com/tetratelabs/run/pkg/signal" 25 | ) 26 | 27 | func Example() { 28 | var ( 29 | g run.Group 30 | p PersonService 31 | s signal.Handler 32 | ) 33 | 34 | // add our PersonService 35 | g.Register(&p) 36 | 37 | // add a SignalHandler service 38 | g.Register(&s) 39 | 40 | // Start our services and block until error or exit request. 41 | // If sending a SIGINT to the process, a graceful shutdown of the 42 | // application will occur. 43 | if err := g.Run(); err != nil { 44 | fmt.Printf("Unexpected exit: %v\n", err) 45 | os.Exit(-1) 46 | } 47 | } 48 | 49 | // PersonService implements run.Config, run.PreRunner and run.GroupService to 50 | // show a fully managed service lifecycle. 51 | type PersonService struct { 52 | name string 53 | age int 54 | 55 | closer chan error 56 | } 57 | 58 | func (p PersonService) Name() string { 59 | return "person" 60 | } 61 | 62 | // FlagSet implements run.Config and thus its configuration and flag handling is 63 | // automatically registered when adding the service to Group. 64 | func (p *PersonService) FlagSet() *pflag.FlagSet { 65 | flags := pflag.NewFlagSet("PersonService's flags", pflag.ContinueOnError) 66 | 67 | flags.StringVarP(&p.name, "name", "-n", "john doe", "name of person") 68 | flags.IntVarP(&p.age, "age", "a", 42, "age of person") 69 | 70 | return flags 71 | } 72 | 73 | // Validate implements run.Config and thus its configuration and flag handling 74 | // is automatically registered when adding the service to Group. 75 | func (p PersonService) Validate() error { 76 | var errs []error 77 | if p.name == "" { 78 | errs = append(errs, errors.New("invalid name provided")) 79 | } 80 | if p.age < 18 { 81 | errs = append(errs, errors.New("invalid age provided, we don't serve minors")) 82 | } 83 | if p.age > 110 { 84 | errs = append(errs, errors.New("faking it? or life expectancy assumptions surpassed by future healthcare system")) 85 | } 86 | return errors.Join(errs...) 87 | } 88 | 89 | // PreRun implements run.PreRunner and thus this method is run at the pre-run 90 | // stage of Group before starting any of the services. 91 | func (p *PersonService) PreRun() error { 92 | p.closer = make(chan error) 93 | return nil 94 | } 95 | 96 | // Serve implements run.GroupService and is executed at the service run phase of 97 | // Group in order of registration. All Serve methods must block until requested 98 | // to Stop or needing to fatally error. 99 | func (p PersonService) Serve() error { 100 | <-p.closer 101 | return nil 102 | } 103 | 104 | // GracefulStop implements run.GroupService and is executed at the shutdown 105 | // phase of Group. 106 | func (p PersonService) GracefulStop() { 107 | close(p.closer) 108 | } 109 | -------------------------------------------------------------------------------- /pkg/version/version.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tetrate, Inc 2021. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package version can be used to implement embedding versioning details from 16 | // git branches and tags into the binary importing this package. 17 | package version 18 | 19 | import ( 20 | "fmt" 21 | "regexp" 22 | "strconv" 23 | "strings" 24 | ) 25 | 26 | // gitDescribeHashIndexPattern matches the git describe hash index pattern in the version string. 27 | // The version string should be in the format: 28 | // 29 | // --g- 30 | // 31 | // As an example: 0.6.6-rc1-15-g12345678-want-more-branch, the "g" prefix stands for "git" 32 | // (see: https://git-scm.com/docs/git-describe). 33 | var gitDescribeHashIndexPattern = regexp.MustCompile(`-[0-9]+(-g+)+`) 34 | 35 | // gitCommitsAheadPattern captures the commits ahead pattern in the version substring (that should 36 | // be an integer). 37 | var gitCommitsAheadPattern = regexp.MustCompile(`[0-9]+`) 38 | 39 | // build is to be populated at build time using -ldflags -X. 40 | // 41 | // Example: 42 | // 43 | // VERSION_PATH := github.com/tetratelabs/run/pkg/version 44 | // VERSION_STRING := $(shell git describe --tags --long) 45 | // GIT_BRANCH_NAME := $(shell git rev-parse --abbrev-ref HEAD) 46 | // GO_LINK_VERSION := -X ${VERSION_PATH}.build=${VERSION_STRING}-${GIT_BRANCH_NAME} 47 | // go build -ldflags '${GO_LINK_VERSION}' 48 | var build string 49 | 50 | // mainBranches is a list of (sorted) main branches/revisions. 51 | var mainBranches = []string{"HEAD", "main", "master"} 52 | 53 | // Show the service's version information 54 | func Show(serviceName string) { 55 | fmt.Println(serviceName + " " + Parse()) 56 | } 57 | 58 | // Parse returns the parsed service's version information. (from raw git label) 59 | func Parse() string { 60 | return parseGit(build).String() 61 | } 62 | 63 | // Git contains the version information extracted from a Git SHA. 64 | type Git struct { 65 | ClosestTag string 66 | CommitsAhead int 67 | Sha string 68 | Branch string 69 | } 70 | 71 | func (g Git) String() string { 72 | switch { 73 | case g == Git{}: 74 | // unofficial version built without using the make tooling 75 | return "v0.0.0-unofficial" 76 | case g.CommitsAhead != 0: 77 | // built from a non release commit point 78 | // In the version string, the commit tag is prefixed with "-g" (which stands for "git"). 79 | // When printing the version string, remove that prefix to just show the real commit hash. 80 | return fmt.Sprintf("%s-%s (%s, +%d)", g.ClosestTag, g.Branch, g.Sha, g.CommitsAhead) 81 | case !isMainBranch(g.Branch): 82 | // specific branch release build 83 | return fmt.Sprintf("%s-%s", g.ClosestTag, g.Branch) 84 | default: 85 | return g.ClosestTag 86 | } 87 | } 88 | 89 | // parseGit the given version string into a version object. The input version string 90 | // is in the format: 91 | // 92 | // --g- 93 | func parseGit(v string) Git { 94 | // Here we try to find the "--g"-part. 95 | found := gitDescribeHashIndexPattern.FindStringIndex(v) 96 | if found == nil { 97 | return Git{} 98 | } 99 | 100 | idx := strings.Index(v[found[1]:], "-") 101 | if idx == -1 { 102 | return Git{} 103 | } 104 | branch := v[found[1]:][idx+1:] // branch name is the part after the "-g-". 105 | sha := v[found[1]:][:idx] 106 | 107 | commits, err := strconv.Atoi(gitCommitsAheadPattern.FindString(v[found[0]+1:])) 108 | if err != nil { // extra safety but should never happen. 109 | return Git{} 110 | } 111 | 112 | // prefix v on semantic versioning tags omitting it 113 | // Go module tags should include the 'v' 114 | closestTagIndex := 0 115 | if strings.ToLower(v)[0] != 'v' { 116 | v = "v" + v 117 | closestTagIndex = 1 118 | } 119 | 120 | return Git{ 121 | ClosestTag: v[0 : found[0]+closestTagIndex], 122 | CommitsAhead: commits, 123 | Sha: sha, 124 | Branch: branch, 125 | } 126 | } 127 | 128 | // isMainBranch returns true if the given branch name is a main branch. 129 | func isMainBranch(branch string) bool { 130 | for _, b := range mainBranches { 131 | if b == branch { 132 | return true 133 | } 134 | } 135 | return false 136 | } 137 | -------------------------------------------------------------------------------- /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 [yyyy] [name of copyright owner] 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 | -------------------------------------------------------------------------------- /group_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tetrate, Inc 2021. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package run_test 16 | 17 | import ( 18 | "context" 19 | "errors" 20 | "fmt" 21 | "strings" 22 | "testing" 23 | "time" 24 | 25 | "github.com/tetratelabs/run" 26 | "github.com/tetratelabs/run/pkg/test" 27 | ) 28 | 29 | var ( 30 | errFlags = errors.New("flagset error") 31 | errClose = errors.New("requested close") 32 | errIRQ = errors.New("interrupt") 33 | ) 34 | 35 | func TestRunGroupSvcLifeCycle(t *testing.T) { 36 | var ( 37 | g = run.Group{} 38 | s service 39 | sc serviceContext 40 | irq = make(chan error) 41 | hasName bool 42 | ) 43 | 44 | // add our service to Group 45 | g.Register(&s) 46 | // add our context aware service to Group 47 | g.Register(&sc) 48 | 49 | // add our interrupter 50 | g.Register(&test.TestSvc{ 51 | SvcName: "testsvc", 52 | Execute: func() error { 53 | // wait until the service has started to signal termination so that 54 | // we can properly assert the full lifecycle has been executed. Otherwise, the service 55 | // GracefulStop may be called before it starts 56 | <-s.started 57 | hasName = len(g.Name) > 0 58 | return errIRQ 59 | }, 60 | }) 61 | 62 | // start Group 63 | go func() { irq <- g.Run("./myService", "-f", "1") }() 64 | 65 | select { 66 | case err := <-irq: 67 | if err != errIRQ { 68 | t.Errorf("Expected proper close, got %v", err) 69 | } 70 | if s.groupName != g.Name { 71 | t.Error("Expected namer logic to run") 72 | } 73 | if s.initializer < 1 { 74 | t.Error("Expected initializer logic to run") 75 | } 76 | if !s.flagSet { 77 | t.Error("Expected flagSet logic to run") 78 | } 79 | if !s.validated { 80 | t.Error("Expected validation logic to run") 81 | } 82 | if s.configItem != 1 { 83 | t.Errorf("Expected flag value to be %d, got %d", 1, s.configItem) 84 | } 85 | if !s.preRun { 86 | t.Errorf("Expected preRun logic to run") 87 | } 88 | if !s.serve { 89 | t.Errorf("Expected serve logic to run: service") 90 | } 91 | if !sc.serve { 92 | t.Errorf("Expected serve logic to run: serviceContext") 93 | } 94 | if !s.gracefulStop { 95 | t.Errorf("Expected graceful stop logic to run") 96 | } 97 | if !sc.contextDone { 98 | t.Errorf("Expected context cancellation to be received") 99 | } 100 | if !hasName { 101 | t.Errorf("Expected valid name from env") 102 | } 103 | case <-time.After(100 * time.Millisecond): 104 | t.Errorf("timeout") 105 | } 106 | } 107 | 108 | func TestRunGroupMultiErrorHandling(t *testing.T) { 109 | var ( 110 | g = run.Group{Name: "MyService"} 111 | 112 | err1 = errors.New("cfg1 failed") 113 | err2 = errors.New("cfg2 failed") 114 | err3 = errors.New("cfg3 failed") 115 | mErr = fmt.Errorf("3 errors occured:\n%w", errors.Join(err1, err2, err3)) 116 | 117 | cfg1 = failingConfig{e: err1} 118 | cfg2 = failingConfig{e: err2} 119 | cfg3 = failingConfig{e: err3} 120 | ) 121 | 122 | g.Register(cfg1, cfg2, cfg3) 123 | 124 | if want, have := mErr.Error(), g.Run().Error(); want != have { 125 | t.Errorf("invalid error payload returned:\nwant:\n%+v\nhave:\n%+v\n", want, have) 126 | } 127 | } 128 | 129 | func TestRunGroupEarlyBailFlags(t *testing.T) { 130 | var irq = make(chan error) 131 | 132 | for idx, tt := range []struct { 133 | flag string 134 | hasErr bool 135 | }{ 136 | {flag: "-v"}, 137 | {flag: "-h"}, 138 | {flag: "--version"}, 139 | {flag: "--help"}, 140 | {flag: "--non-existent", hasErr: true}, 141 | } { 142 | g := run.Group{HelpText: "placeholder"} 143 | 144 | // start Group 145 | go func() { irq <- g.Run("./myService", tt.flag) }() 146 | 147 | select { 148 | case err := <-irq: 149 | if !tt.hasErr && err != nil { 150 | t.Errorf("[%d] Expected proper close, got %v", idx, err) 151 | } 152 | if tt.hasErr && err == nil { 153 | t.Errorf("[%d] Expected early bail with error, got nil", idx) 154 | } 155 | case <-time.After(100 * time.Millisecond): 156 | t.Errorf("timeout") 157 | } 158 | } 159 | } 160 | 161 | func TestRunPreRunFailure(t *testing.T) { 162 | var ( 163 | e = errors.New("preRun failed") 164 | irq = make(chan error) 165 | pr = failingPreRun{e: e} 166 | g = run.Group{Name: "PreRunFail"} 167 | ) 168 | 169 | g.Register(pr) 170 | 171 | go func() { irq <- g.Run() }() 172 | 173 | select { 174 | case err := <-irq: 175 | if !errors.Is(err, e) { 176 | t.Errorf("Expected %v, got %v", e, err) 177 | } 178 | case <-time.After(100 * time.Millisecond): 179 | t.Errorf("timeout") 180 | } 181 | } 182 | 183 | func TestDuplicateFlag(t *testing.T) { 184 | var ( 185 | g = run.Group{} 186 | flag1 flagTestConfig 187 | flag2 flagTestConfig 188 | irq = make(chan error) 189 | ) 190 | 191 | // add our flags 192 | g.Register(&flag1, &flag2) 193 | 194 | // add our interrupter 195 | g.Register(&test.TestSvc{ 196 | SvcName: "irqsvc", 197 | Execute: func() error { return errIRQ }, 198 | }) 199 | 200 | // start Group 201 | go func() { irq <- g.Run("./myService", "-f", "3") }() 202 | 203 | select { 204 | case err := <-irq: 205 | if err != errIRQ { 206 | t.Errorf("Expected proper close, got %v", err) 207 | } 208 | if flag1.value != 3 { 209 | t.Errorf("Expected flag1 = %d, got %d", 3, flag1.value) 210 | } 211 | if flag2.value != 10 { 212 | t.Errorf("Expected flag2 = %d, got %d", 10, flag2.value) 213 | } 214 | case <-time.After(100 * time.Millisecond): 215 | t.Errorf("timeout") 216 | } 217 | } 218 | 219 | func TestRuntimeDeregister(t *testing.T) { 220 | for _, svcs := range [][]string{ 221 | {"--s1-disable"}, 222 | {"--s2-disable"}, 223 | {"--s1-disable", "--s2-disable"}, 224 | } { 225 | for _, phase := range []string{"config", "preRunner", "service"} { 226 | var ( 227 | g = run.Group{} 228 | s1, s2, s3 service 229 | d1, d2 bool 230 | disabler disablerService 231 | irq = make(chan error) 232 | idx = fmt.Sprintf("%s(%s)", phase, strings.Join(svcs, ",")) 233 | ) 234 | 235 | s1.customFlags = run.NewFlagSet("s1-disabler") 236 | s1.customFlags.BoolVar(&d1, "s1-disable", false, "disable service 1") 237 | s1.configItem = 1 238 | s2.customFlags = run.NewFlagSet("s2-disabler") 239 | s2.customFlags.BoolVar(&d2, "s2-disable", false, "disable service 2") 240 | s2.configItem = 1 241 | 242 | g.Register(&disabler, &s1, &s2, &s3) 243 | g.Deregister(&s3) // make sure we also handle deregister before calling Run 244 | 245 | switch phase { 246 | case "config": 247 | disabler.config = func() { 248 | if d1 { 249 | if dereg := g.Deregister(&s1); dereg[0] == false { 250 | t.Errorf("%s: deregister want: true, have: %t", idx, dereg[0]) 251 | } 252 | s1.disabled.config = true 253 | s1.disabled.preRun = true 254 | s1.disabled.serve = true 255 | } 256 | if d2 { 257 | if dereg := g.Deregister(&s2); dereg[0] == false { 258 | t.Errorf("%s: deregister want: true, have: %t", idx, dereg[0]) 259 | } 260 | s2.disabled.config = true 261 | s2.disabled.preRun = true 262 | s2.disabled.serve = true 263 | } 264 | } 265 | case "preRunner": 266 | disabler.preRunner = func() { 267 | if d1 { 268 | if dereg := g.Deregister(&s1); dereg[0] == false { 269 | t.Errorf("%s: deregister want: true, have: %t", idx, dereg[0]) 270 | } 271 | s1.disabled.preRun = true 272 | s1.disabled.serve = true 273 | } 274 | if d2 { 275 | if dereg := g.Deregister(&s2); dereg[0] == false { 276 | t.Errorf("%s: deregister want: true, have: %t", idx, dereg[0]) 277 | } 278 | s2.disabled.preRun = true 279 | s2.disabled.serve = true 280 | } 281 | } 282 | case "service": 283 | g.Register(run.NewPreRunner("service-disabler", func() error { 284 | if d1 { 285 | if dereg := g.Deregister(&s1); dereg[0] == false { 286 | t.Errorf("%s: deregister want: true, have: %t", idx, dereg[0]) 287 | } 288 | s1.disabled.serve = true 289 | } 290 | if d2 { 291 | if dereg := g.Deregister(&s2); dereg[0] == false { 292 | t.Errorf("%s: deregister want: true, have: %t", idx, dereg[0]) 293 | } 294 | s2.disabled.serve = true 295 | } 296 | return nil 297 | })) 298 | } 299 | 300 | g.Register(&test.TestSvc{ 301 | SvcName: "testsvc", 302 | Execute: func() error { 303 | // wait until the service has started to signal termination so that 304 | // we can properly assert the full lifecycle has been executed. Otherwise, the service 305 | // GracefulStop may be called before it starts 306 | if !d1 { 307 | <-s1.started 308 | } 309 | if !d2 { 310 | <-s2.started 311 | } 312 | return errIRQ 313 | }, 314 | }) 315 | 316 | // start Group 317 | go func() { irq <- g.Run(append([]string{"./myService"}, svcs...)...) }() 318 | 319 | select { 320 | case err := <-irq: 321 | if err != errIRQ { 322 | t.Errorf("Expected proper close, got %v", err) 323 | } 324 | 325 | if want, have := !s1.disabled.config, s1.validated; want != have { 326 | t.Errorf("%s: s1 config want: %t, have: %t", idx, want, have) 327 | } 328 | if want, have := !s1.disabled.preRun, s1.preRun; want != have { 329 | t.Errorf("%s: s1 prerun want: %t, have: %t", idx, want, have) 330 | } 331 | if want, have := !s1.disabled.serve, s1.serve && s1.gracefulStop; want != have { 332 | t.Errorf("%s: s1 serve want: %t, have: %t", idx, want, have) 333 | } 334 | if want, have := !s2.disabled.config, s2.validated; want != have { 335 | t.Errorf("%s: s2 config want: %t, have: %t", idx, want, have) 336 | } 337 | if want, have := !s2.disabled.preRun, s2.preRun; want != have { 338 | t.Errorf("%s: s2 prerun want: %t, have: %t", idx, want, have) 339 | } 340 | if want, have := !s2.disabled.serve, s2.serve && s2.gracefulStop; want != have { 341 | t.Errorf("%s: s2 serve want: %t, have: %t", idx, want, have) 342 | } 343 | 344 | case <-time.After(100 * time.Millisecond): 345 | t.Errorf("timeout") 346 | } 347 | 348 | } 349 | } 350 | } 351 | 352 | type flagTestConfig struct { 353 | value int 354 | } 355 | 356 | func (f flagTestConfig) Name() string { 357 | return fmt.Sprintf("flagtest%d", f.value) 358 | } 359 | 360 | func (f *flagTestConfig) FlagSet() *run.FlagSet { 361 | flags := run.NewFlagSet("flag test config") 362 | flags.IntVarP(&f.value, "flagtest", "f", 10, "flagtester") 363 | return flags 364 | } 365 | 366 | func (f flagTestConfig) Validate() error { return nil } 367 | 368 | type failingConfig struct { 369 | e error 370 | } 371 | 372 | func (f failingConfig) Name() string { 373 | return f.e.Error() 374 | } 375 | 376 | func (f failingConfig) FlagSet() *run.FlagSet { return nil } 377 | 378 | func (f failingConfig) Validate() error { return f.e } 379 | 380 | type failingPreRun struct { 381 | e error 382 | } 383 | 384 | func (f failingPreRun) Name() string { return f.e.Error() } 385 | func (f failingPreRun) PreRun() error { return f.e } 386 | 387 | var ( 388 | _ run.Unit = (*service)(nil) 389 | _ run.Initializer = (*service)(nil) 390 | _ run.Namer = (*service)(nil) 391 | _ run.Config = (*service)(nil) 392 | _ run.PreRunner = (*service)(nil) 393 | _ run.Service = (*service)(nil) 394 | ) 395 | 396 | type service struct { 397 | configItem int 398 | groupName string 399 | initializer int 400 | flagSet bool 401 | validated bool 402 | preRun bool 403 | serve bool 404 | gracefulStop bool 405 | disabled struct { 406 | config bool 407 | preRun bool 408 | serve bool 409 | } 410 | closer chan error 411 | started chan struct{} 412 | customFlags *run.FlagSet 413 | } 414 | 415 | func (s *service) Name() string { 416 | return "testsvc" 417 | } 418 | 419 | func (s *service) GroupName(name string) { 420 | s.groupName = name 421 | } 422 | 423 | func (s *service) Initialize() { 424 | s.initializer++ 425 | } 426 | 427 | func (s *service) FlagSet() *run.FlagSet { 428 | s.flagSet = true 429 | if s.customFlags != nil { 430 | return s.customFlags 431 | } 432 | flags := run.NewFlagSet("dummy flagset") 433 | flags.IntVarP(&s.configItem, "flagtest", "f", 5, "rungroup flagset test") 434 | return flags 435 | } 436 | 437 | func (s *service) Validate() error { 438 | s.validated = true 439 | if s.configItem != 1 { 440 | return errFlags 441 | } 442 | return nil 443 | } 444 | 445 | func (s *service) PreRun() error { 446 | s.preRun = true 447 | s.closer = make(chan error, 5) 448 | s.started = make(chan struct{}) 449 | return nil 450 | } 451 | 452 | func (s *service) Serve() error { 453 | s.serve = true 454 | close(s.started) // signal the Serve method has been called 455 | err := <-s.closer 456 | if err == errClose { 457 | s.gracefulStop = true 458 | } 459 | close(s.closer) 460 | return err 461 | } 462 | 463 | func (s *service) GracefulStop() { 464 | s.closer <- errClose 465 | } 466 | 467 | var ( 468 | _ run.Unit = (*disablerService)(nil) 469 | _ run.Config = (*disablerService)(nil) 470 | _ run.PreRunner = (*disablerService)(nil) 471 | ) 472 | 473 | type disablerService struct { 474 | q chan error 475 | config func() 476 | preRunner func() 477 | } 478 | 479 | func (d disablerService) Name() string { 480 | return "disablerService" 481 | } 482 | 483 | func (d disablerService) FlagSet() *run.FlagSet { 484 | return run.NewFlagSet("dummy flagset") 485 | } 486 | 487 | func (d *disablerService) Validate() error { 488 | d.q = make(chan error) 489 | if d.config != nil { 490 | d.config() 491 | } 492 | return nil 493 | } 494 | 495 | func (d *disablerService) PreRun() error { 496 | if d.preRunner != nil { 497 | d.preRunner() 498 | } 499 | return nil 500 | } 501 | 502 | var ( 503 | _ run.ServiceContext = (*serviceContext)(nil) 504 | ) 505 | 506 | type serviceContext struct { 507 | serve bool 508 | contextDone bool 509 | } 510 | 511 | func (s serviceContext) Name() string { 512 | return "svc-context" 513 | } 514 | 515 | func (s *serviceContext) ServeContext(ctx context.Context) error { 516 | s.serve = true 517 | <-ctx.Done() 518 | s.contextDone = true 519 | return nil 520 | } 521 | -------------------------------------------------------------------------------- /group.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Tetrate, Inc 2021. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package run implements an actor-runner with deterministic teardown. 16 | // It uses the concepts found in the https://github.com/oklog/run/ package as 17 | // its basis and enhances it with configuration registration and validation as 18 | // well as pre-run phase logic. 19 | package run 20 | 21 | import ( 22 | "context" 23 | "errors" 24 | "fmt" 25 | "os" 26 | "path" 27 | "strings" 28 | "sync/atomic" 29 | 30 | color "github.com/logrusorgru/aurora" 31 | "github.com/spf13/pflag" 32 | "github.com/tetratelabs/telemetry" 33 | 34 | "github.com/tetratelabs/run/pkg/log" 35 | "github.com/tetratelabs/run/pkg/version" 36 | ) 37 | 38 | // BinaryName holds the template variable that will be replaced by the Group 39 | // name in HelpText strings. 40 | const BinaryName = "{{.Name}}" 41 | 42 | // Error allows for creating constant errors instead of sentinel ones. 43 | type Error string 44 | 45 | // Error implements error. 46 | func (e Error) Error() string { return string(e) } 47 | 48 | // ErrBailEarlyRequest is returned when a call to RunConfig was successful but 49 | // signals that the application should exit in success immediately. 50 | // It is typically returned on --version and --help requests that have been 51 | // served. It can and should be used for custom config phase situations where 52 | // the job of the application is done. 53 | const ErrBailEarlyRequest Error = "exit request from flag handler" 54 | 55 | // ErrRequestedShutdown can be used by Service implementations to gracefully 56 | // request a shutdown of the application. Group will then exit without errors. 57 | const ErrRequestedShutdown Error = "shutdown requested" 58 | 59 | // FlagSet holds a pflag.FlagSet as well as an exported Name variable for 60 | // allowing improved help usage information. 61 | type FlagSet struct { 62 | *pflag.FlagSet 63 | Name string 64 | } 65 | 66 | // NewFlagSet returns a new FlagSet for usage in Config objects. 67 | func NewFlagSet(name string) *FlagSet { 68 | return &FlagSet{ 69 | FlagSet: pflag.NewFlagSet(name, pflag.ContinueOnError), 70 | Name: name, 71 | } 72 | } 73 | 74 | // Unit is the default interface an object needs to implement for it to be able 75 | // to register with a Group. 76 | // Name should return a short but good identifier of the Unit. 77 | type Unit interface { 78 | Name() string 79 | } 80 | 81 | // Initializer is an extension interface that Units can implement if they need 82 | // to have certain properties initialized after creation but before any of the 83 | // other lifecycle phases such as Config, PreRunner and/or Serve are run. 84 | // Note, since an Initializer is a public function, make sure it is safe to be 85 | // called multiple times. 86 | type Initializer interface { 87 | // Unit is embedded for Group registration and identification 88 | Unit 89 | Initialize() 90 | } 91 | 92 | // Namer is an extension interface that Units can implement if they need to know 93 | // or want to use the Group.Name. Since Group's name can be updated at runtime 94 | // by the -n flag, Group first parses its own FlagSet the know if its Name needs 95 | // to be updated and then runs the Name method on all Units implementing the 96 | // Namer interface before handling the Units that implement Config. This allows 97 | // these units to have the Name method be used to adjust the default values for 98 | // flags or any other logic that uses the Group name to make decisions. 99 | type Namer interface { 100 | GroupName(string) 101 | } 102 | 103 | // Config interface should be implemented by Group Unit objects that manage 104 | // their own configuration through the use of flags. 105 | // If a Unit's Validate returns an error it will stop the Group immediately. 106 | type Config interface { 107 | // Unit is embedded for Group registration and identification 108 | Unit 109 | // FlagSet returns an object's FlagSet 110 | FlagSet() *FlagSet 111 | // Validate checks an object's stored values 112 | Validate() error 113 | } 114 | 115 | // PreRunner interface should be implemented by Group Unit objects that need 116 | // a pre run stage before starting the Group Services. 117 | // If a Unit's PreRun returns an error it will stop the Group immediately. 118 | type PreRunner interface { 119 | // Unit is embedded for Group registration and identification 120 | Unit 121 | PreRun() error 122 | } 123 | 124 | // NewPreRunner takes a name and a standalone pre runner compatible function 125 | // and turns them into a Group compatible PreRunner, ready for registration. 126 | func NewPreRunner(name string, fn func() error) PreRunner { 127 | return preRunner{name: name, fn: fn} 128 | } 129 | 130 | type preRunner struct { 131 | name string 132 | fn func() error 133 | } 134 | 135 | func (p preRunner) Name() string { 136 | return p.name 137 | } 138 | 139 | func (p preRunner) PreRun() error { 140 | return p.fn() 141 | } 142 | 143 | // Service interface should be implemented by Group Unit objects that need 144 | // to run a blocking service until an error occurs or a shutdown request is 145 | // made. 146 | // The Serve method must be blocking and return an error on unexpected shutdown. 147 | // Recoverable errors need to be handled inside the service itself. 148 | // GracefulStop must gracefully stop the service and make the Serve call return. 149 | // 150 | // Since Service is managed by Group, it is considered a design flaw to call any 151 | // of the Service methods directly in application code. 152 | // 153 | // An alternative to implementing Service can be found in the ServiceContext 154 | // interface which allows the Group Unit to listen for the cancellation signal 155 | // from the Group provided context.Context. 156 | // 157 | // Important: Service and ServiceContext are mutually exclusive and should never 158 | // be implemented in the same Unit. 159 | type Service interface { 160 | // Unit is embedded for Group registration and identification 161 | Unit 162 | // Serve starts the GroupService and blocks. 163 | Serve() error 164 | // GracefulStop shuts down and cleans up the GroupService. 165 | GracefulStop() 166 | } 167 | 168 | // ServiceContext interface should be implemented by Group Unit objects that 169 | // need to run a blocking service until an error occurs or the by Group provided 170 | // context.Context sends a cancellation signal. 171 | // 172 | // An alternative to implementing ServiceContext can be found in the Service 173 | // interface which has specific Serve and GracefulStop methods. 174 | // 175 | // Important: Service and ServiceContext are mutually exclusive and should never 176 | // be implemented in the same Unit. 177 | type ServiceContext interface { 178 | // Unit is embedded for Group registration and identification 179 | Unit 180 | // ServeContext starts the GroupService and blocks until the provided 181 | // context is cancelled. 182 | ServeContext(ctx context.Context) error 183 | } 184 | 185 | // Group builds on concepts from https://github.com/oklog/run to provide a 186 | // deterministic way to manage service lifecycles. It allows for easy 187 | // composition of elegant monoliths as well as adding signal handlers, metrics 188 | // services, etc. 189 | type Group struct { 190 | // Name of the Group managed service. If omitted, the binary name will be 191 | // used as found at runtime. 192 | Name string 193 | // HelpText is optional and allows to provide some additional help context 194 | // when --help is requested. 195 | HelpText string 196 | Logger telemetry.Logger 197 | 198 | f *FlagSet 199 | i []Initializer 200 | n []Namer 201 | c []Config 202 | p []PreRunner 203 | s []Service 204 | x []ServiceContext 205 | 206 | configured bool 207 | } 208 | 209 | // Register will inspect the provided objects implementing the Unit interface to 210 | // see if it needs to register the objects for any of the Group bootstrap 211 | // phases. If a Unit doesn't satisfy any of the bootstrap phases it is ignored 212 | // by Group. 213 | // The returned array of booleans is of the same size as the amount of provided 214 | // Units, signalling for each provided Unit if it successfully registered with 215 | // Group for at least one of the bootstrap phases or if it was ignored. 216 | // 217 | // Important: It is a design flaw for a Unit implementation to adhere to both 218 | // the Service and ServiceContext interfaces. Passing along such a Unit will 219 | // cause Register to throw a panic! 220 | func (g *Group) Register(units ...Unit) []bool { 221 | type ambiguousService interface { 222 | Service 223 | ServiceContext 224 | } 225 | hasRegistered := make([]bool, len(units)) 226 | for idx := range units { 227 | if i, ok := units[idx].(Initializer); ok { 228 | g.i = append(g.i, i) 229 | hasRegistered[idx] = true 230 | } 231 | if !g.configured { 232 | // if RunConfig has been called we can no longer register Config 233 | // phases of Units 234 | if n, ok := units[idx].(Namer); ok { 235 | g.n = append(g.n, n) 236 | hasRegistered[idx] = true 237 | } 238 | if c, ok := units[idx].(Config); ok { 239 | g.c = append(g.c, c) 240 | hasRegistered[idx] = true 241 | } 242 | } 243 | if p, ok := units[idx].(PreRunner); ok { 244 | g.p = append(g.p, p) 245 | hasRegistered[idx] = true 246 | } 247 | if svc, ok := units[idx].(ambiguousService); ok { 248 | panic("ambiguous service " + svc.Name() + " encountered: " + 249 | "a Unit MUST NOT implement both Service and ServiceContext") 250 | } 251 | if s, ok := units[idx].(Service); ok { 252 | g.s = append(g.s, s) 253 | hasRegistered[idx] = true 254 | } 255 | if x, ok := units[idx].(ServiceContext); ok { 256 | g.x = append(g.x, x) 257 | hasRegistered[idx] = true 258 | } 259 | } 260 | return hasRegistered 261 | } 262 | 263 | // Deregister will inspect the provided objects implementing the Unit interface 264 | // to see if it needs to de-register the objects for any of the Group bootstrap 265 | // phases. 266 | // The returned array of booleans is of the same size as the amount of provided 267 | // Units, signalling for each provided Unit if it successfully de-registered 268 | // with Group for at least one of the bootstrap phases or if it was ignored. 269 | // It is generally safe to use Deregister at any bootstrap phase except at Serve 270 | // time (when it will have no effect). 271 | // WARNING: Dependencies between Units can cause a crash as a dependent Unit 272 | // might expect the other Unit to gone through all the needed bootstrapping 273 | // phases. 274 | func (g *Group) Deregister(units ...Unit) []bool { 275 | hasDeregistered := make([]bool, len(units)) 276 | for idx := range units { 277 | for i := range g.i { 278 | if g.i[i] != nil && g.i[i].(Unit) == units[idx] { 279 | g.i[i] = nil // can't resize slice during Run, so nil 280 | hasDeregistered[idx] = true 281 | } 282 | } 283 | for i := range g.n { 284 | if g.n[i] != nil && g.n[i].(Unit) == units[idx] { 285 | g.n[i] = nil // can't resize slice during Run, so nil 286 | hasDeregistered[idx] = true 287 | } 288 | } 289 | for i := range g.c { 290 | if g.c[i] != nil && g.c[i].(Unit) == units[idx] { 291 | g.c[i] = nil // can't resize slice during Run, so nil 292 | hasDeregistered[idx] = true 293 | } 294 | } 295 | for i := range g.p { 296 | if g.p[i] != nil && g.p[i].(Unit) == units[idx] { 297 | g.p[i] = nil // can't resize slice during Run, so nil 298 | hasDeregistered[idx] = true 299 | } 300 | } 301 | for i := range g.s { 302 | if g.s[i] != nil && g.s[i].(Unit) == units[idx] { 303 | g.s[i] = nil // can't resize slice during Run, so nil 304 | hasDeregistered[idx] = true 305 | } 306 | } 307 | for i := range g.x { 308 | if g.x[i] != nil && g.x[i].(Unit) == units[idx] { 309 | g.x[i] = nil // can't resize slice during Run, so nil 310 | hasDeregistered[idx] = true 311 | } 312 | } 313 | } 314 | return hasDeregistered 315 | } 316 | 317 | // RunConfig runs the Config phase of all registered Config aware Units. 318 | // Only use this function if needing to add additional wiring between config 319 | // and (pre)run phases and a separate PreRunner phase is not an option. 320 | // In most cases it is best to use the Run method directly as it will run the 321 | // Config phase prior to executing the PreRunner and Service phases. 322 | // If an error is returned the application must shut down as it is considered 323 | // fatal. In case the error is an ErrBailEarlyRequest the application 324 | // should clean up and exit without an error code as an ErrBailEarlyRequest 325 | // is not an actual error but a request for Help, Version or other task that has 326 | // been finished and there is no more work left to handle. 327 | func (g *Group) RunConfig(args ...string) (err error) { 328 | g.configured = true 329 | if g.Logger == nil { 330 | g.Logger = &log.Logger{} 331 | } 332 | 333 | if g.Name == "" { 334 | // use the binary name if custom name has not been provided 335 | g.Name = path.Base(os.Args[0]) 336 | } 337 | 338 | g.HelpText = strings.ReplaceAll(g.HelpText, BinaryName, os.Args[0]) 339 | 340 | defer func() { 341 | if err != nil && !errors.Is(err, ErrBailEarlyRequest) { 342 | g.Logger.Error("unexpected exit", err) 343 | } 344 | }() 345 | 346 | // run configuration stage 347 | g.f = NewFlagSet(g.Name) 348 | g.f.SortFlags = false // keep order of flag registration 349 | g.f.Usage = func() { 350 | fmt.Printf("Usage of %s:\n", g.Name) 351 | if g.HelpText != "" { 352 | fmt.Printf("%s\n", g.HelpText) 353 | } 354 | fmt.Printf("Flags:\n") 355 | g.f.PrintDefaults() 356 | } 357 | 358 | // register default rungroup flags 359 | var ( 360 | name string 361 | showHelp bool 362 | showVersion bool 363 | showRunGroup bool 364 | ) 365 | 366 | gFS := NewFlagSet("Common Service options") 367 | gFS.SortFlags = false 368 | gFS.StringVarP(&name, "name", "n", g.Name, `name of this service`) 369 | gFS.BoolVarP(&showVersion, "version", "v", false, 370 | "show version information and exit.") 371 | gFS.BoolVarP(&showHelp, "help", "h", false, 372 | "show this help information and exit.") 373 | gFS.BoolVar(&showRunGroup, "show-rungroup-units", false, "show run group units") 374 | _ = gFS.MarkHidden("show-rungroup-units") 375 | g.f.AddFlagSet(gFS.FlagSet) 376 | 377 | // default to os.Args if args parameter was omitted 378 | if len(args) == 0 { 379 | args = os.Args[1:] 380 | } 381 | 382 | // parse our run group flags only (not the plugin ones) 383 | _ = gFS.Parse(args) 384 | if name != "" { 385 | g.Name = name 386 | } 387 | 388 | // initialize all Units implementing Initializer 389 | for idx, i := range g.i { 390 | // an Initializer might have been de-registered 391 | if i != nil { 392 | i.Initialize() 393 | // don't call in Run phase again 394 | g.i[idx] = nil 395 | } 396 | } 397 | 398 | // inform all Units implementing Namer of the parsed Group name 399 | for _, n := range g.n { 400 | // a Namer might have been de-registered 401 | if n != nil { 402 | n.GroupName(g.Name) 403 | } 404 | } 405 | 406 | // register flags from attached Config objects 407 | fs := make([]*FlagSet, len(g.c)) 408 | for idx := range g.c { 409 | // a Config might have been de-registered 410 | if g.c[idx] == nil { 411 | g.Logger.Debug("flagset", 412 | "name", "--deregistered--", 413 | "item", fmt.Sprintf("(%d/%d)", idx+1, len(g.c)), 414 | ) 415 | continue 416 | } 417 | g.Logger.Debug("flagset", 418 | "name", g.c[idx].Name(), 419 | "item", fmt.Sprintf("(%d/%d)", idx+1, len(g.c)), 420 | ) 421 | fs[idx] = g.c[idx].FlagSet() 422 | if fs[idx] == nil { 423 | // no FlagSet returned 424 | g.Logger.Debug("config object did not return a flagset", "index", idx) 425 | continue 426 | } 427 | fs[idx].VisitAll(func(f *pflag.Flag) { 428 | if g.f.Lookup(f.Name) != nil { 429 | g.Logger.Debug("ignoring duplicate flag", "name", f.Name, "index", idx) 430 | return 431 | } 432 | g.f.AddFlag(f) 433 | }) 434 | } 435 | 436 | // parse FlagSet and exit on error 437 | if err = g.f.Parse(args); err != nil { 438 | return err 439 | } 440 | 441 | // bail early on help or version requests 442 | switch { 443 | case showHelp: 444 | fmt.Println(color.Cyan(color.Bold(fmt.Sprintf("Usage of %s:", g.Name)))) 445 | if g.HelpText != "" { 446 | fmt.Printf("%s\n", g.HelpText) 447 | } 448 | fmt.Printf("%s\n\n", color.Cyan(color.Bold("Flags:"))) 449 | fmt.Printf("%s\n%s\n", color.Cyan("* "+gFS.Name), gFS.FlagUsages()) 450 | for _, f := range fs { 451 | if f != nil { 452 | fmt.Printf("%s\n%s\n", color.Cyan("* "+f.Name), f.FlagUsages()) 453 | } 454 | } 455 | return ErrBailEarlyRequest 456 | case showVersion: 457 | version.Show(g.Name) 458 | return ErrBailEarlyRequest 459 | case showRunGroup: 460 | fmt.Println(g.ListUnits()) 461 | return ErrBailEarlyRequest 462 | } 463 | 464 | // Validate Config inputs 465 | var errs []error 466 | for idx, cfg := range g.c { 467 | func(itemNr int, cfg Config) { 468 | // a Config might have been de-registered during Run 469 | if cfg == nil { 470 | g.Logger.Debug("validate-skip", 471 | "name", "--deregistered--", 472 | "item", fmt.Sprintf("(%d/%d)", itemNr, len(g.c)), 473 | ) 474 | return 475 | } 476 | var vErr error 477 | l := g.Logger.With( 478 | "name", cfg.Name(), 479 | "item", fmt.Sprintf("(%d/%d)", itemNr, len(g.c))) 480 | l.Debug("validate") 481 | defer l.Debug("validate-exit", debugLogError(vErr)...) 482 | vErr = cfg.Validate() 483 | if vErr != nil { 484 | errs = append(errs, vErr) 485 | } 486 | }(idx+1, cfg) 487 | } 488 | 489 | // exit on at least one Validate error 490 | if len(errs) > 0 { 491 | return fmt.Errorf("%d errors occured:\n%w", len(errs), errors.Join(errs...)) 492 | } 493 | 494 | // log binary name and version 495 | g.Logger.Info(g.Name + " " + version.Parse() + " started") 496 | 497 | return nil 498 | } 499 | 500 | // Run will execute all phases of all registered Units and block until an error 501 | // occurs. 502 | // If RunConfig has been called prior to Run, the Group's Config phase will be 503 | // skipped and Run continues with the PreRunner and Service phases. 504 | // 505 | // The following phases are executed in the following sequence: 506 | // 507 | // Initialization phase (serially, in order of Unit registration) 508 | // - Initialize() Initialize Unit's supporting this interface. 509 | // 510 | // Config phase (serially, in order of Unit registration) 511 | // - FlagSet() Get & register all FlagSets from Config Units. 512 | // - Flag Parsing Using the provided args (os.Args if empty). 513 | // - Validate() Validate Config Units. Exit on first error. 514 | // 515 | // PreRunner phase (serially, in order of Unit registration) 516 | // - PreRun() Execute PreRunner Units. Exit on first error. 517 | // 518 | // Service and ServiceContext phase (concurrently) 519 | // - Serve() Execute all Service Units in separate Go routines. 520 | // ServeContext() Execute all ServiceContext Units. 521 | // - Wait Block until one of the Serve() or ServeContext() 522 | // methods returns. 523 | // - GracefulStop() Call interrupt handlers of all Service Units and 524 | // cancel the context.Context provided to all the 525 | // ServiceContext units registered. 526 | // 527 | // Run will return with the originating error on: 528 | // - first Config.Validate() returning an error 529 | // - first PreRunner.PreRun() returning an error 530 | // - first Service.Serve() or ServiceContext.ServeContext() returning 531 | // 532 | // Note: it is perfectly acceptable to use Group without Service and 533 | // ServiceContext units. In this case Run will just return immediately after 534 | // having handled the Config and PreRunner phases of the registered Units. This 535 | // is particularly convenient if using the common pkg middlewares in a CLI, 536 | // script, or other ephemeral environment. 537 | func (g *Group) Run(args ...string) (err error) { 538 | if !g.configured { 539 | // run config registration and flag parsing stages 540 | if err = g.RunConfig(args...); err != nil { 541 | if errors.Is(err, ErrBailEarlyRequest) { 542 | return nil 543 | } 544 | return err 545 | } 546 | } 547 | 548 | var hasServices bool 549 | 550 | defer func() { 551 | if err == nil { 552 | // Registered services should never initiate an exit without an 553 | // error. Services allowing intended shutdowns must use the 554 | // ErrRequestShutdown error (or wrap it) to signal intent. 555 | // If Group is used without services (e.g. PreRunner scripts) this 556 | // is fine. 557 | if hasServices { 558 | err = errors.New("run terminated without explicit error condition") 559 | g.Logger.Error("unexpected exit", err) 560 | return 561 | } 562 | g.Logger.Info("done") 563 | return 564 | } 565 | // test if this is a requested / expected shutdown... 566 | if errors.Is(err, ErrRequestedShutdown) { 567 | g.Logger.Info("received shutdown request", "details", err) 568 | err = nil 569 | return 570 | } 571 | // actual fatal error 572 | g.Logger.Error("unexpected exit", err) 573 | }() 574 | 575 | // call our Initializer (again) 576 | // In case a Unit was registered for PreRun and/or Serve phase after Config 577 | // phase was completed, we still want to run the Initializer if existent. 578 | for _, i := range g.i { 579 | // an Initializer might have been de-registered 580 | if i != nil { 581 | i.Initialize() 582 | } 583 | } 584 | 585 | // execute pre run stage and exit on error 586 | for idx := range g.p { 587 | if err = func(itemNr int, pr PreRunner) error { 588 | // a PreRunner might have been de-registered during Run 589 | if pr == nil { 590 | g.Logger.Debug("pre-run-skip", 591 | "name", "--deregistered--", 592 | "item", fmt.Sprintf("(%d/%d)", itemNr, len(g.p)), 593 | ) 594 | return nil 595 | } 596 | var err error 597 | l := g.Logger.With( 598 | "name", pr.Name(), 599 | "item", fmt.Sprintf("(%d/%d)", itemNr, len(g.p))) 600 | l.Debug("pre-run") 601 | defer l.Debug("pre-run-exit", debugLogError(err)...) 602 | err = pr.PreRun() 603 | if err != nil { 604 | return fmt.Errorf("pre-run %s: %w", pr.Name(), err) 605 | } 606 | return nil 607 | }(idx+1, g.p[idx]); err != nil { 608 | return err 609 | } 610 | } 611 | 612 | var ( 613 | s []Service 614 | x []ServiceContext 615 | ) 616 | for idx := range g.s { 617 | // a Service might have been de-registered during Run 618 | if g.s[idx] != nil { 619 | s = append(s, g.s[idx]) 620 | } 621 | } 622 | for idx := range g.x { 623 | // a ServiceContext might have been de-registered during Run 624 | if g.x[idx] != nil { 625 | x = append(x, g.x[idx]) 626 | } 627 | } 628 | if len(s)+len(x) == 0 { 629 | // we have no Service or ServiceContext to run. 630 | return nil 631 | } 632 | 633 | // setup our cancellable context and error channel 634 | ctx, cancel := context.WithCancel(context.Background()) 635 | errs := make(chan error, len(s)+len(x)) 636 | hasServices = true 637 | var stopped int32 638 | 639 | // run each Service 640 | for idx, svc := range s { 641 | go func(itemNr int, svc Service) { 642 | var err error 643 | l := g.Logger.With( 644 | "name", svc.Name(), 645 | "item", fmt.Sprintf("(%d/%d)", itemNr, len(s))) 646 | l.Debug("serve") 647 | defer l.Debug("serve-exit", debugLogError(err)...) 648 | // do not start Serve if other services signaled termination, to prevent 649 | // a race where stop may have been called for this unit already as that would leave 650 | // the unit running forever 651 | if atomic.LoadInt32(&stopped) == 0 { 652 | err = svc.Serve() 653 | } 654 | errs <- err 655 | }(idx+1, svc) 656 | } 657 | // run each ServiceContext 658 | for idx, svc := range x { 659 | go func(itemNr int, svc ServiceContext) { 660 | var err error 661 | l := g.Logger.With( 662 | "name", svc.Name(), 663 | "item", fmt.Sprintf("(%d/%d)", itemNr, len(x))) 664 | l.Debug("serve-context") 665 | defer l.Debug("serve-context-exit", debugLogError(err)...) 666 | // do not start Serve if other services signaled termination, to prevent 667 | // a race where stop may have been called for this unit already as that would leave 668 | // the unit running forever 669 | if atomic.LoadInt32(&stopped) == 0 { 670 | err = svc.ServeContext(ctx) 671 | } 672 | errs <- err 673 | }(idx+1, svc) 674 | } 675 | 676 | // wait for the first Service or ServiceContext to stop and special case 677 | // its error as the originator 678 | err = <-errs 679 | atomic.SwapInt32(&stopped, 1) 680 | 681 | // signal all Service and ServiceContext Units to stop 682 | cancel() 683 | for idx, svc := range s { 684 | go func(itemNr int, svc Service) { 685 | l := g.Logger.With( 686 | "name", svc.Name(), 687 | "item", fmt.Sprintf("(%d/%d)", itemNr, len(s))) 688 | l.Debug("graceful-stop") 689 | defer l.Debug("graceful-stop-exit") 690 | svc.GracefulStop() 691 | }(idx+1, svc) 692 | } 693 | 694 | // wait for all Service and ServiceContext Units to have returned 695 | for i := 1; i < cap(errs); i++ { 696 | <-errs 697 | } 698 | 699 | // return the originating error 700 | return err 701 | } 702 | 703 | // ListUnits returns a list of all Group phases and the Units registered to each 704 | // of them. 705 | func (g Group) ListUnits() string { 706 | var ( 707 | s string 708 | t = "cli" 709 | ) 710 | 711 | if len(g.i) > 0 { 712 | s += "\n - initialize: " 713 | for _, u := range g.i { 714 | if u != nil { 715 | s += u.Name() + " " 716 | } 717 | } 718 | } 719 | if len(g.c) > 0 { 720 | s += "\n- config: " 721 | for _, u := range g.c { 722 | if u != nil { 723 | s += u.Name() + " " 724 | } 725 | } 726 | } 727 | if len(g.p) > 0 { 728 | s += "\n- pre-run: " 729 | for _, u := range g.p { 730 | if u != nil { 731 | s += u.Name() + " " 732 | } 733 | } 734 | } 735 | if len(g.s) > 0 { 736 | s += "\n- serve: " 737 | for _, u := range g.s { 738 | if u != nil { 739 | t = "svc" 740 | s += u.Name() + " " 741 | } 742 | } 743 | } 744 | if len(g.x) > 0 { 745 | s += "\n- serve-context: " 746 | for _, u := range g.x { 747 | if u != nil { 748 | t = "svc" 749 | s += u.Name() + " " 750 | } 751 | } 752 | } 753 | 754 | return fmt.Sprintf("Group: %s [%s]%s", g.Name, t, s) 755 | } 756 | 757 | func debugLogError(err error) (kv []interface{}) { 758 | if err == nil { 759 | return 760 | } 761 | kv = append(kv, "error", err.Error()) 762 | return 763 | } 764 | --------------------------------------------------------------------------------