├── .github └── workflows │ └── go.yml ├── LICENSE ├── README.md ├── example_test.go ├── finish.go ├── finish_test.go ├── go.mod ├── log.go └── options.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | 7 | test: 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | go-version: 12 | - '1.21' 13 | - '1.20' 14 | - '1.13.0' # minimum version 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Set up Go ${{ matrix.go-version }} 20 | uses: actions/setup-go@v3 21 | with: 22 | go-version: ${{ matrix.go-version }} 23 | 24 | - name: Test 25 | run: go test -cover -v 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2017, pseidemann 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # finish 2 | 3 | [![Go Reference](https://pkg.go.dev/badge/github.com/pseidemann/finish.svg)](https://pkg.go.dev/github.com/pseidemann/finish) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/pseidemann/finish)](https://goreportcard.com/report/github.com/pseidemann/finish) 5 | [![Build Status](https://github.com/pseidemann/finish/actions/workflows/go.yml/badge.svg)](https://github.com/pseidemann/finish/actions/workflows/go.yml) 6 | 7 | A non-intrusive package, adding a graceful shutdown to Go's HTTP server, by 8 | utilizing `http.Server`'s built-in `Shutdown()` method, with zero dependencies. 9 | 10 | 11 | ## Quick Start 12 | 13 | Assume the following code in a file called `simple.go`: 14 | ```go 15 | package main 16 | 17 | import ( 18 | "fmt" 19 | "log" 20 | "net/http" 21 | "time" 22 | 23 | "github.com/pseidemann/finish" 24 | ) 25 | 26 | func main() { 27 | http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) { 28 | time.Sleep(5 * time.Second) 29 | fmt.Fprintln(w, "world") 30 | }) 31 | 32 | srv := &http.Server{Addr: "localhost:8080"} 33 | 34 | fin := finish.New() 35 | fin.Add(srv) 36 | 37 | go func() { 38 | if err := srv.ListenAndServe(); err != http.ErrServerClosed { 39 | log.Fatal(err) 40 | } 41 | }() 42 | 43 | fin.Wait() 44 | } 45 | ``` 46 | 47 | Now execute that file: 48 | ```sh 49 | $ go run simple.go 50 | ``` 51 | 52 | Do a HTTP GET request: 53 | ```sh 54 | $ curl localhost:8080/hello 55 | ``` 56 | 57 | This will print "world" after 5 seconds. 58 | 59 | When the server is terminated with pressing `Ctrl+C` or `kill`, while `/hello` is 60 | loading, finish will wait until the request was handled, before the server gets 61 | killed. 62 | 63 | The output will look like this: 64 | ``` 65 | 2038/01/19 03:14:08 finish: shutdown signal received 66 | 2038/01/19 03:14:08 finish: shutting down server ... 67 | 2038/01/19 03:14:11 finish: server closed 68 | ``` 69 | 70 | 71 | ## Customization 72 | 73 | ### Change Timeout 74 | 75 | How to change the default timeout of 10 seconds. 76 | 77 | ```go 78 | package main 79 | 80 | import ( 81 | "fmt" 82 | "log" 83 | "net/http" 84 | "time" 85 | 86 | "github.com/pseidemann/finish" 87 | ) 88 | 89 | func main() { 90 | http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) { 91 | time.Sleep(5 * time.Second) 92 | fmt.Fprintln(w, "world") 93 | }) 94 | 95 | srv := &http.Server{Addr: "localhost:8080"} 96 | 97 | fin := &finish.Finisher{Timeout: 30 * time.Second} 98 | fin.Add(srv) 99 | 100 | go func() { 101 | if err := srv.ListenAndServe(); err != http.ErrServerClosed { 102 | log.Fatal(err) 103 | } 104 | }() 105 | 106 | fin.Wait() 107 | } 108 | ``` 109 | 110 | In this example the timeout is set to 30 seconds. 111 | 112 | 113 | ### Change Logger 114 | 115 | How to change the default logger. 116 | 117 | ```go 118 | package main 119 | 120 | import ( 121 | "fmt" 122 | "log" 123 | "net/http" 124 | "time" 125 | 126 | "github.com/pseidemann/finish" 127 | "github.com/sirupsen/logrus" 128 | ) 129 | 130 | func main() { 131 | http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) { 132 | time.Sleep(5 * time.Second) 133 | fmt.Fprintln(w, "world") 134 | }) 135 | 136 | srv := &http.Server{Addr: "localhost:8080"} 137 | 138 | fin := &finish.Finisher{Log: logrus.StandardLogger()} 139 | fin.Add(srv) 140 | 141 | go func() { 142 | if err := srv.ListenAndServe(); err != http.ErrServerClosed { 143 | log.Fatal(err) 144 | } 145 | }() 146 | 147 | fin.Wait() 148 | } 149 | ``` 150 | 151 | In this example, logrus is configured for logging. 152 | 153 | 154 | ### Change Signals 155 | 156 | How to change the default signals (`SIGINT`, `SIGTERM`) which will initiate the shutdown. 157 | 158 | ```go 159 | package main 160 | 161 | import ( 162 | "fmt" 163 | "log" 164 | "net/http" 165 | "syscall" 166 | "time" 167 | 168 | "github.com/pseidemann/finish" 169 | ) 170 | 171 | func main() { 172 | http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) { 173 | time.Sleep(5 * time.Second) 174 | fmt.Fprintln(w, "world") 175 | }) 176 | 177 | srv := &http.Server{Addr: "localhost:8080"} 178 | 179 | fin := &finish.Finisher{Signals: append(finish.DefaultSignals, syscall.SIGHUP)} 180 | fin.Add(srv) 181 | 182 | go func() { 183 | if err := srv.ListenAndServe(); err != http.ErrServerClosed { 184 | log.Fatal(err) 185 | } 186 | }() 187 | 188 | fin.Wait() 189 | } 190 | ``` 191 | 192 | In this example, finish will not only catch the default signals `SIGINT` and `SIGTERM` but also the signal `SIGHUP`. 193 | 194 | 195 | ### Full Example 196 | 197 | This example uses a custom router [httprouter](https://github.com/julienschmidt/httprouter), 198 | a different timeout, a custom logger [logrus](https://github.com/sirupsen/logrus), 199 | custom signals, options for `Add()` and multiple servers. 200 | 201 | ```go 202 | package main 203 | 204 | import ( 205 | "fmt" 206 | "log" 207 | "net/http" 208 | "syscall" 209 | "time" 210 | 211 | "github.com/julienschmidt/httprouter" 212 | "github.com/pseidemann/finish" 213 | "github.com/sirupsen/logrus" 214 | ) 215 | 216 | func main() { 217 | routerPub := httprouter.New() 218 | routerPub.HandlerFunc("GET", "/hello", func(w http.ResponseWriter, r *http.Request) { 219 | time.Sleep(5 * time.Second) 220 | fmt.Fprintln(w, "world") 221 | }) 222 | 223 | routerInt := httprouter.New() 224 | routerInt.HandlerFunc("GET", "/status", func(w http.ResponseWriter, r *http.Request) { 225 | fmt.Fprintln(w, "ok") 226 | }) 227 | 228 | srvPub := &http.Server{Addr: "localhost:8080", Handler: routerPub} 229 | srvInt := &http.Server{Addr: "localhost:3000", Handler: routerInt} 230 | 231 | fin := &finish.Finisher{ 232 | Timeout: 30 * time.Second, 233 | Log: logrus.StandardLogger(), 234 | Signals: append(finish.DefaultSignals, syscall.SIGHUP), 235 | } 236 | fin.Add(srvPub, finish.WithName("public server")) 237 | fin.Add(srvInt, finish.WithName("internal server"), finish.WithTimeout(5*time.Second)) 238 | 239 | go func() { 240 | logrus.Infof("starting public server at %s", srvPub.Addr) 241 | if err := srvPub.ListenAndServe(); err != http.ErrServerClosed { 242 | log.Fatal(err) 243 | } 244 | }() 245 | 246 | go func() { 247 | logrus.Infof("starting internal server at %s", srvInt.Addr) 248 | if err := srvInt.ListenAndServe(); err != http.ErrServerClosed { 249 | log.Fatal(err) 250 | } 251 | }() 252 | 253 | fin.Wait() 254 | } 255 | ``` 256 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package finish_test 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/pseidemann/finish" 10 | ) 11 | 12 | func Example() { 13 | http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) { 14 | time.Sleep(5 * time.Second) 15 | fmt.Fprintln(w, "world") 16 | }) 17 | 18 | srv := &http.Server{Addr: "localhost:8080"} 19 | 20 | fin := finish.New() 21 | fin.Add(srv) 22 | 23 | go func() { 24 | if err := srv.ListenAndServe(); err != http.ErrServerClosed { 25 | log.Fatal(err) 26 | } 27 | }() 28 | 29 | fin.Wait() 30 | } 31 | -------------------------------------------------------------------------------- /finish.go: -------------------------------------------------------------------------------- 1 | // Package finish adds a graceful shutdown to Go's HTTP server. 2 | // 3 | // It utilizes http.Server's built-in Shutdown() method. 4 | package finish 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "os" 10 | "os/signal" 11 | "sync" 12 | "syscall" 13 | "time" 14 | ) 15 | 16 | // DefaultTimeout is used if [Finisher].Timeout is not set. 17 | const DefaultTimeout = 10 * time.Second 18 | 19 | var ( 20 | // DefaultLogger is used if Finisher.Logger is not set. 21 | // It uses the Go standard log package. 22 | DefaultLogger = &defaultLogger{} 23 | // StdoutLogger can be used as a simple logger which writes to stdout 24 | // via the fmt standard package. 25 | StdoutLogger = &stdoutLogger{} 26 | // DefaultSignals is used if Finisher.Signals is not set. 27 | // The default shutdown signals are: 28 | // - SIGINT (triggered by pressing Control-C) 29 | // - SIGTERM (sent by `kill $pid` or e.g. systemd stop) 30 | DefaultSignals = []os.Signal{syscall.SIGINT, syscall.SIGTERM} 31 | ) 32 | 33 | // A Server is a type which can be shutdown. 34 | // 35 | // This is the interface expected by [Finisher.Add], which allows registering any server which implements the Shutdown() method. 36 | type Server interface { 37 | Shutdown(ctx context.Context) error 38 | } 39 | 40 | type serverKeeper struct { 41 | srv Server 42 | name string 43 | timeout time.Duration 44 | } 45 | 46 | // Finisher implements graceful shutdown of servers. 47 | type Finisher struct { 48 | // Timeout is the maximum amount of time to wait for 49 | // still running server requests to finish, 50 | // when the shutdown signal was received for each server. 51 | // 52 | // It defaults to DefaultTimeout which is 10 seconds. 53 | // 54 | // The timeout can be overridden on a per-server basis with passing the 55 | // WithTimeout() option to Add() while adding the server. 56 | Timeout time.Duration 57 | 58 | // Log can be set to change where finish logs to. 59 | // It defaults to DefaultLogger which uses the standard Go log package. 60 | Log Logger 61 | 62 | // Signals can be used to change which signals finish catches to initiate 63 | // the shutdown. 64 | // It defaults to DefaultSignals which contains SIGINT and SIGTERM. 65 | Signals []os.Signal 66 | 67 | mutex sync.Mutex 68 | keepers []*serverKeeper 69 | manSig chan interface{} 70 | } 71 | 72 | // New creates a Finisher. 73 | // 74 | // This is a convenience constructor if no changes to the default configuration are needed. 75 | func New() *Finisher { 76 | return &Finisher{} 77 | } 78 | 79 | func (f *Finisher) signals() []os.Signal { 80 | if f.Signals != nil { 81 | return f.Signals 82 | } 83 | return DefaultSignals 84 | } 85 | 86 | func (f *Finisher) log() Logger { 87 | if f.Log != nil { 88 | return f.Log 89 | } 90 | return DefaultLogger 91 | } 92 | 93 | func (f *Finisher) timeout() time.Duration { 94 | if f.Timeout != 0 { 95 | return f.Timeout 96 | } 97 | return DefaultTimeout 98 | } 99 | 100 | func (f *Finisher) getManSig() chan interface{} { 101 | f.mutex.Lock() 102 | defer f.mutex.Unlock() 103 | if f.manSig == nil { 104 | f.manSig = make(chan interface{}, 1) 105 | } 106 | return f.manSig 107 | } 108 | 109 | // Add a server for graceful shutdown. 110 | // 111 | // Options can be passed as the second argument to change the behavior for this server: 112 | // 113 | // To give the server a specific name instead of just “server #”: 114 | // 115 | // fin.Add(srv, finish.WithName("internal server")) 116 | // 117 | // To override the timeout, configured in Finisher, for this specific server: 118 | // 119 | // fin.Add(srv, finish.WithTimeout(5*time.Second)) 120 | // 121 | // To do both at the same time: 122 | // 123 | // fin.Add(srv, finish.WithName("internal server"), finish.WithTimeout(5*time.Second)) 124 | func (f *Finisher) Add(srv Server, opts ...Option) { 125 | keeper := &serverKeeper{ 126 | srv: srv, 127 | timeout: f.timeout(), 128 | } 129 | 130 | for _, opt := range opts { 131 | if err := opt(keeper); err != nil { 132 | panic(err) 133 | } 134 | } 135 | 136 | f.keepers = append(f.keepers, keeper) 137 | } 138 | 139 | // Wait blocks until one of the shutdown signals is received and then closes all servers with a timeout. 140 | func (f *Finisher) Wait() { 141 | f.updateNames() 142 | 143 | signals := f.signals() 144 | stop := make(chan os.Signal, len(signals)) 145 | signal.Notify(stop, signals...) 146 | 147 | // wait for signal 148 | select { 149 | case sig := <-stop: 150 | if sig == syscall.SIGINT { 151 | // fix prints after "^C" 152 | fmt.Println("") 153 | } 154 | case <-f.getManSig(): 155 | // Trigger() was called 156 | } 157 | 158 | f.log().Infof("finish: shutdown signal received") 159 | 160 | for _, keeper := range f.keepers { 161 | ctx, cancel := context.WithTimeout(context.Background(), keeper.timeout) 162 | defer cancel() 163 | f.log().Infof("finish: shutting down %s ...", keeper.name) 164 | if err := keeper.srv.Shutdown(ctx); err != nil { 165 | if err == context.DeadlineExceeded { 166 | f.log().Errorf("finish: shutdown timeout for %s", keeper.name) 167 | } else { 168 | f.log().Errorf("finish: error while shutting down %s: %s", keeper.name, err) 169 | } 170 | } else { 171 | f.log().Infof("finish: %s closed", keeper.name) 172 | } 173 | } 174 | } 175 | 176 | // Trigger the shutdown signal manually. 177 | // 178 | // This is probably only useful for testing. 179 | func (f *Finisher) Trigger() { 180 | f.getManSig() <- nil 181 | } 182 | 183 | func (f *Finisher) updateNames() { 184 | if len(f.keepers) == 1 && f.keepers[0].name == "" { 185 | f.keepers[0].name = "server" 186 | return 187 | } 188 | 189 | for i, keeper := range f.keepers { 190 | if keeper.name == "" { 191 | keeper.name = fmt.Sprintf("server #%d", i+1) 192 | } 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /finish_test.go: -------------------------------------------------------------------------------- 1 | package finish 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "log" 9 | "os" 10 | "reflect" 11 | "strings" 12 | "syscall" 13 | "testing" 14 | "time" 15 | ) 16 | 17 | var errTest = errors.New("test error") 18 | 19 | type testServer struct { 20 | shutdown bool 21 | wait time.Duration 22 | } 23 | 24 | func (t *testServer) Shutdown(ctx context.Context) error { 25 | wait := time.NewTimer(t.wait) 26 | 27 | select { 28 | case <-ctx.Done(): 29 | return ctx.Err() 30 | case <-wait.C: 31 | // server finished fake busy work 32 | } 33 | 34 | t.shutdown = true 35 | 36 | return nil 37 | } 38 | 39 | type logRecorder struct { 40 | infos []string 41 | errors []string 42 | } 43 | 44 | func (l *logRecorder) Infof(format string, args ...interface{}) { 45 | l.infos = append(l.infos, fmt.Sprintf(format, args...)) 46 | } 47 | 48 | func (l *logRecorder) Errorf(format string, args ...interface{}) { 49 | l.errors = append(l.errors, fmt.Sprintf(format, args...)) 50 | } 51 | 52 | func captureStdout(f func()) string { 53 | reader, writer, err := os.Pipe() 54 | if err != nil { 55 | panic(err) 56 | } 57 | 58 | before := os.Stdout 59 | os.Stdout = writer 60 | 61 | f() 62 | 63 | if err := writer.Close(); err != nil { 64 | panic(err) 65 | } 66 | 67 | var buf bytes.Buffer 68 | if _, err := buf.ReadFrom(reader); err != nil { 69 | panic(err) 70 | } 71 | 72 | if err := reader.Close(); err != nil { 73 | panic(err) 74 | } 75 | 76 | os.Stdout = before 77 | 78 | return buf.String() 79 | } 80 | 81 | func captureLog(f func()) string { 82 | var buf bytes.Buffer 83 | log.SetOutput(&buf) 84 | f() 85 | log.SetOutput(os.Stderr) 86 | return buf.String() 87 | } 88 | 89 | func Test(t *testing.T) { 90 | srv := &testServer{wait: time.Second} 91 | log := &logRecorder{} 92 | 93 | fin := &Finisher{Log: log} 94 | fin.Add(srv) 95 | 96 | keeper := fin.keepers[0] 97 | 98 | if keeper.srv != srv { 99 | t.Error("expected server to be added") 100 | } 101 | 102 | if keeper.name != "" { 103 | t.Error("expected name to be empty") 104 | } 105 | 106 | if keeper.timeout != DefaultTimeout { 107 | t.Error("expected timeout to be the default") 108 | } 109 | 110 | go fin.Trigger() 111 | 112 | if srv.shutdown { 113 | t.Error("expected server not to be shutdown yet") 114 | } 115 | 116 | fin.Wait() 117 | 118 | if !srv.shutdown { 119 | t.Error("expected server to be shutdown") 120 | } 121 | 122 | if keeper.name != "server" { 123 | t.Error("expected name to be 'server'") 124 | } 125 | 126 | if !reflect.DeepEqual(log.infos, []string{ 127 | "finish: shutdown signal received", 128 | "finish: shutting down server ...", 129 | "finish: server closed", 130 | }) { 131 | t.Error("wrong log output") 132 | } 133 | 134 | if log.errors != nil { 135 | t.Error("expected no error logs") 136 | } 137 | } 138 | 139 | func TestDefaultLogger(t *testing.T) { 140 | srv := &testServer{wait: 2 * time.Second} 141 | 142 | fin := &Finisher{Timeout: time.Second} 143 | fin.Add(srv) 144 | 145 | go fin.Trigger() 146 | 147 | log := captureLog(func() { 148 | fin.Wait() 149 | }) 150 | 151 | // using Contains() because the default logger contains timestamps 152 | 153 | if !strings.Contains(log, "finish: shutdown signal received") { 154 | t.Error("missing log") 155 | } 156 | 157 | if !strings.Contains(log, "finish: shutting down server ...") { 158 | t.Error("missing log") 159 | } 160 | 161 | // trigger error to get coverage for defaultLogger's Errorf() 162 | if !strings.Contains(log, "finish: shutdown timeout for server") { 163 | t.Error("missing log") 164 | } 165 | } 166 | 167 | func TestStdoutLogger(t *testing.T) { 168 | srv := &testServer{wait: 2 * time.Second} 169 | 170 | fin := &Finisher{Timeout: time.Second, Log: StdoutLogger} 171 | fin.Add(srv) 172 | 173 | go fin.Trigger() 174 | 175 | stdout := captureStdout(func() { 176 | fin.Wait() 177 | }) 178 | 179 | if stdout != "finish: shutdown signal received\n"+ 180 | "finish: shutting down server ...\n"+ 181 | "finish: shutdown timeout for server\n" { 182 | t.Error("wrong log") 183 | } 184 | } 185 | 186 | func TestSettingName(t *testing.T) { 187 | srv := &testServer{} 188 | log := &logRecorder{} 189 | 190 | fin := &Finisher{Log: log} 191 | fin.Add(srv, WithName("foobar")) 192 | 193 | keeper := fin.keepers[0] 194 | 195 | if keeper.name != "foobar" { 196 | t.Error("expected name to be set") 197 | } 198 | 199 | go fin.Trigger() 200 | 201 | fin.Wait() 202 | 203 | if !reflect.DeepEqual(log.infos, []string{ 204 | "finish: shutdown signal received", 205 | "finish: shutting down foobar ...", 206 | "finish: foobar closed", 207 | }) { 208 | t.Error("wrong log output") 209 | } 210 | 211 | if log.errors != nil { 212 | t.Error("expected no error logs") 213 | } 214 | } 215 | 216 | func TestUpdateNames(t *testing.T) { 217 | srv := &testServer{} 218 | 219 | fin := New() 220 | fin.Add(srv, WithName("foobar")) 221 | fin.Add(srv) 222 | 223 | fin.updateNames() 224 | 225 | if fin.keepers[0].name != "foobar" { 226 | t.Error("wrong name") 227 | } 228 | 229 | if fin.keepers[1].name != "server #2" { 230 | t.Error("wrong name") 231 | } 232 | } 233 | 234 | func TestGlobalTimeout(t *testing.T) { 235 | srv := &testServer{} 236 | 237 | fin := &Finisher{Timeout: 21 * time.Second} 238 | fin.Add(srv) 239 | 240 | keeper := fin.keepers[0] 241 | 242 | if keeper.timeout != 21*time.Second { 243 | t.Error("expected timeout to be changed") 244 | } 245 | } 246 | 247 | func TestOverridingTimeout(t *testing.T) { 248 | srv := &testServer{} 249 | 250 | fin := New() 251 | fin.Add(srv, WithTimeout(42*time.Second)) 252 | 253 | keeper := fin.keepers[0] 254 | 255 | if keeper.timeout != 42*time.Second { 256 | t.Error("expected timeout to be set") 257 | } 258 | } 259 | 260 | func TestOptionError(t *testing.T) { 261 | testOpt := func(keeper *serverKeeper) error { 262 | return errTest 263 | } 264 | 265 | srv := &testServer{} 266 | 267 | fin := New() 268 | func() { 269 | defer func() { 270 | if err := recover(); err != errTest { 271 | t.Error("expected Add() to panic") 272 | } 273 | }() 274 | 275 | fin.Add(srv, testOpt) 276 | }() 277 | } 278 | 279 | func TestSlowServer(t *testing.T) { 280 | srv := &testServer{wait: 2 * time.Second} 281 | log := &logRecorder{} 282 | 283 | fin := &Finisher{Log: log} 284 | fin.Add(srv, WithTimeout(time.Second)) 285 | 286 | go fin.Trigger() 287 | 288 | fin.Wait() 289 | 290 | if !reflect.DeepEqual(log.infos, []string{ 291 | "finish: shutdown signal received", 292 | "finish: shutting down server ...", 293 | }) { 294 | t.Error("wrong log output") 295 | } 296 | 297 | if !reflect.DeepEqual(log.errors, []string{ 298 | "finish: shutdown timeout for server", 299 | }) { 300 | t.Error("wrong error log output") 301 | } 302 | } 303 | 304 | type testServerErr struct{} 305 | 306 | func (t *testServerErr) Shutdown(ctx context.Context) error { 307 | return errTest 308 | } 309 | 310 | func TestServerError(t *testing.T) { 311 | srv := &testServerErr{} 312 | log := &logRecorder{} 313 | 314 | fin := &Finisher{Log: log} 315 | fin.Add(srv, WithTimeout(time.Second)) 316 | 317 | go fin.Trigger() 318 | 319 | fin.Wait() 320 | 321 | if !reflect.DeepEqual(log.infos, []string{ 322 | "finish: shutdown signal received", 323 | "finish: shutting down server ...", 324 | }) { 325 | t.Error("wrong log output") 326 | } 327 | 328 | if !reflect.DeepEqual(log.errors, []string{ 329 | "finish: error while shutting down server: test error", 330 | }) { 331 | t.Error("wrong error log output") 332 | } 333 | } 334 | 335 | func TestSigIntPrint(t *testing.T) { 336 | srv := &testServer{} 337 | log := &logRecorder{} 338 | 339 | fin := &Finisher{Log: log} 340 | fin.Add(srv) 341 | 342 | go func() { 343 | // sleep so Wait() can actually catch the signal 344 | time.Sleep(time.Second) 345 | // trigger signal 346 | p, err := os.FindProcess(os.Getpid()) 347 | if err != nil { 348 | panic(err) 349 | } 350 | p.Signal(syscall.SIGINT) 351 | }() 352 | 353 | stdout := captureStdout(func() { 354 | fin.Wait() 355 | }) 356 | 357 | if stdout != "\n" { 358 | t.Error("expected newline to be printed to stdout") 359 | } 360 | 361 | if !reflect.DeepEqual(log.infos, []string{ 362 | "finish: shutdown signal received", 363 | "finish: shutting down server ...", 364 | "finish: server closed", 365 | }) { 366 | t.Error("wrong log output") 367 | } 368 | 369 | if log.errors != nil { 370 | t.Error("expected no error logs") 371 | } 372 | } 373 | 374 | func TestCustomSignal(t *testing.T) { 375 | srv := &testServer{} 376 | log := &logRecorder{} 377 | 378 | mySignal := syscall.SIGUSR1 379 | 380 | fin := &Finisher{Log: log, Signals: []os.Signal{mySignal}} 381 | fin.Add(srv) 382 | 383 | go func() { 384 | // sleep so Wait() can actually catch the signal 385 | time.Sleep(time.Second) 386 | // trigger custom signal 387 | p, err := os.FindProcess(os.Getpid()) 388 | if err != nil { 389 | panic(err) 390 | } 391 | p.Signal(mySignal) 392 | }() 393 | 394 | fin.Wait() 395 | 396 | if !reflect.DeepEqual(log.infos, []string{ 397 | "finish: shutdown signal received", 398 | "finish: shutting down server ...", 399 | "finish: server closed", 400 | }) { 401 | t.Error("wrong log output") 402 | } 403 | 404 | if log.errors != nil { 405 | t.Error("expected no error logs") 406 | } 407 | } 408 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/pseidemann/finish 2 | 3 | go 1.13 4 | -------------------------------------------------------------------------------- /log.go: -------------------------------------------------------------------------------- 1 | package finish 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | ) 7 | 8 | // Logger is the interface expected by [Finisher].Log. 9 | // 10 | // It allows using any loggers which implement the Infof() and Errorf() methods. 11 | type Logger interface { 12 | Infof(format string, v ...interface{}) 13 | Errorf(format string, v ...interface{}) 14 | } 15 | 16 | // default logger 17 | 18 | type defaultLogger struct{} 19 | 20 | func (l *defaultLogger) Infof(format string, v ...interface{}) { 21 | log.Printf(format, v...) 22 | } 23 | 24 | func (l *defaultLogger) Errorf(format string, v ...interface{}) { 25 | l.Infof(format, v...) 26 | } 27 | 28 | // stdout logger 29 | 30 | type stdoutLogger struct{} 31 | 32 | func (l *stdoutLogger) Infof(format string, v ...interface{}) { 33 | fmt.Printf(format+"\n", v...) 34 | } 35 | 36 | func (l *stdoutLogger) Errorf(format string, v ...interface{}) { 37 | l.Infof(format, v...) 38 | } 39 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package finish 2 | 3 | import "time" 4 | 5 | // An Option can be used to change the behavior when registering a server via [Finisher.Add]. 6 | type Option option 7 | 8 | type option func(keeper *serverKeeper) error 9 | 10 | // WithTimeout overrides the global [Finisher].Timeout for the server to be registered via [Finisher.Add]. 11 | func WithTimeout(timeout time.Duration) Option { 12 | return func(keeper *serverKeeper) error { 13 | keeper.timeout = timeout 14 | return nil 15 | } 16 | } 17 | 18 | // WithName sets a custom name for the server to be registered via [Finisher.Add]. 19 | // 20 | // If there will be only one server registered, the name defaults to “server”. 21 | // Otherwise, the names of the servers default to “server #”. 22 | func WithName(name string) Option { 23 | return func(keeper *serverKeeper) error { 24 | keeper.name = name 25 | return nil 26 | } 27 | } 28 | --------------------------------------------------------------------------------