From 4fbfc1784bf7f10af2cc0684bfb5e99e157e0db2 Mon Sep 17 00:00:00 2001 From: ksw2000 <13825170+ksw2000@users.noreply.github.com> Date: Fri, 29 Nov 2024 08:34:43 +0000 Subject: [PATCH 1/8] =?UTF-8?q?=F0=9F=94=A5=20feat:=20Add=20support=20for?= =?UTF-8?q?=20graceful=20shutdown=20timeout=20in=20Listen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/api/fiber.md | 1 + listen.go | 21 +++++++-- listen_test.go | 107 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 126 insertions(+), 3 deletions(-) diff --git a/docs/api/fiber.md b/docs/api/fiber.md index 6892225e11..902e190f4a 100644 --- a/docs/api/fiber.md +++ b/docs/api/fiber.md @@ -108,6 +108,7 @@ app.Listen(":8080", fiber.ListenConfig{ | EnablePrefork | `bool` | When set to true, this will spawn multiple Go processes listening on the same port. | `false` | | EnablePrintRoutes | `bool` | If set to true, will print all routes with their method, path, and handler. | `false` | | GracefulContext | `context.Context` | Field to shutdown Fiber by given context gracefully. | `nil` | +| GracefulShutdownTimeout| `time.Duration` | Field to set the timeout for graceful shutdown. Set to 0 to disable the timeout. | `0` | | ListenerAddrFunc | `func(addr net.Addr)` | Allows accessing and customizing `net.Listener`. | `nil` | | ListenerNetwork | `string` | Known networks are "tcp", "tcp4" (IPv4-only), "tcp6" (IPv6-only). WARNING: When prefork is set to true, only "tcp4" and "tcp6" can be chosen. | `tcp4` | | OnShutdownError | `func(err error)` | Allows to customize error behavior when gracefully shutting down the server by given signal. Prints error with `log.Fatalf()` | `nil` | diff --git a/listen.go b/listen.go index 0df4e1a060..c37c7d4664 100644 --- a/listen.go +++ b/listen.go @@ -18,6 +18,7 @@ import ( "strconv" "strings" "text/tabwriter" + "time" "github.com/gofiber/fiber/v3/log" "github.com/mattn/go-colorable" @@ -37,8 +38,6 @@ const ( ) // ListenConfig is a struct to customize startup of Fiber. -// -// TODO: Add timeout for graceful shutdown. type ListenConfig struct { // GracefulContext is a field to shutdown Fiber by given context gracefully. // @@ -94,6 +93,13 @@ type ListenConfig struct { // Default : "" CertClientFile string `json:"cert_client_file"` + // When the graceful shutdown begins, use this field to set the timeout + // duration. If the timeout is reached, OnShutdownError will be called. + // The default value is 0, which means the timeout setting is disabled. + // + // Default: 0 + GracefulShutdownTimeout time.Duration `json:"graceful_shutdown_timeout"` + // When set to true, it will not print out the «Fiber» ASCII art and listening address. // // Default: false @@ -472,8 +478,17 @@ func (app *App) printRoutesMessage() { func (app *App) gracefulShutdown(ctx context.Context, cfg ListenConfig) { <-ctx.Done() - if err := app.Shutdown(); err != nil { //nolint:contextcheck // TODO: Implement it + var err error + + if cfg.GracefulShutdownTimeout != 0 { + err = app.ShutdownWithTimeout(cfg.GracefulShutdownTimeout) //nolint:contextcheck // TODO: Implement it + } else { + err = app.Shutdown() //nolint:contextcheck // TODO: Implement it + } + + if err != nil { cfg.OnShutdownError(err) + return } if success := cfg.OnShutdownSuccess; success != nil { diff --git a/listen_test.go b/listen_test.go index c828a911cb..ea8684ab81 100644 --- a/listen_test.go +++ b/listen_test.go @@ -115,6 +115,113 @@ func Test_Listen_Graceful_Shutdown(t *testing.T) { mu.Unlock() } +// go test -run Test_Listen_Graceful_Shutdown_Timeout +func Test_Listen_Graceful_Shutdown_Timeout(t *testing.T) { + var mu sync.Mutex + var shutdownSuccess bool + var shutdownTimeoutError error + + app := New() + + app.Get("/", func(c Ctx) error { + return c.SendString(c.Hostname()) + }) + + ln := fasthttputil.NewInmemoryListener() + errs := make(chan error) + + go func() { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + errs <- app.Listener(ln, ListenConfig{ + DisableStartupMessage: true, + GracefulContext: ctx, + GracefulShutdownTimeout: 500 * time.Millisecond, + OnShutdownSuccess: func() { + mu.Lock() + shutdownSuccess = true + mu.Unlock() + }, + OnShutdownError: func(err error) { + mu.Lock() + shutdownTimeoutError = err + mu.Unlock() + }, + }) + }() + + // Server readiness check + for i := 0; i < 10; i++ { + conn, err := ln.Dial() + // To test a graceful shutdown timeout, do not close the connection. + if err == nil { + _ = conn + break + } + // Wait a bit before retrying + time.Sleep(100 * time.Millisecond) + if i == 9 { + t.Fatalf("Server did not become ready in time: %v", err) + } + } + + testCases := []struct { + ExpectedErr error + ExpectedShutdownError error + ExpectedBody string + Time time.Duration + ExpectedStatusCode int + ExpectedShutdownSuccess bool + }{ + { + Time: 100 * time.Millisecond, + ExpectedBody: "example.com", + ExpectedStatusCode: StatusOK, + ExpectedErr: nil, + ExpectedShutdownError: nil, + ExpectedShutdownSuccess: false, + }, + { + Time: 3 * time.Second, + ExpectedBody: "", + ExpectedStatusCode: StatusOK, + ExpectedErr: errors.New("InmemoryListener is already closed: use of closed network connection"), + ExpectedShutdownError: context.DeadlineExceeded, + ExpectedShutdownSuccess: false, + }, + } + + for _, tc := range testCases { + time.Sleep(tc.Time) + + req := fasthttp.AcquireRequest() + req.SetRequestURI("http://example.com") + + client := fasthttp.HostClient{} + client.Dial = func(_ string) (net.Conn, error) { return ln.Dial() } + + resp := fasthttp.AcquireResponse() + err := client.Do(req, resp) + + require.Equal(t, tc.ExpectedErr, err) + require.Equal(t, tc.ExpectedStatusCode, resp.StatusCode()) + require.Equal(t, tc.ExpectedBody, string(resp.Body())) + mu.Lock() + require.Equal(t, tc.ExpectedShutdownSuccess, shutdownSuccess) + require.Equal(t, tc.ExpectedShutdownError, shutdownTimeoutError) + mu.Unlock() + + fasthttp.ReleaseRequest(req) + fasthttp.ReleaseResponse(resp) + } + + mu.Lock() + err := <-errs + require.NoError(t, err) + mu.Unlock() +} + // go test -run Test_Listen_Prefork func Test_Listen_Prefork(t *testing.T) { testPreforkMaster = true From 21f40c67112d08943d5288546f0870b39a7fcc40 Mon Sep 17 00:00:00 2001 From: ksw2000 <13825170+ksw2000@users.noreply.github.com> Date: Fri, 29 Nov 2024 09:11:48 +0000 Subject: [PATCH 2/8] =?UTF-8?q?=F0=9F=93=9A=20doc:=20update=20the=20descri?= =?UTF-8?q?ption=20of=20GracefulShutdownTimeout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/api/fiber.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/fiber.md b/docs/api/fiber.md index 902e190f4a..3a6fa9bb20 100644 --- a/docs/api/fiber.md +++ b/docs/api/fiber.md @@ -108,7 +108,7 @@ app.Listen(":8080", fiber.ListenConfig{ | EnablePrefork | `bool` | When set to true, this will spawn multiple Go processes listening on the same port. | `false` | | EnablePrintRoutes | `bool` | If set to true, will print all routes with their method, path, and handler. | `false` | | GracefulContext | `context.Context` | Field to shutdown Fiber by given context gracefully. | `nil` | -| GracefulShutdownTimeout| `time.Duration` | Field to set the timeout for graceful shutdown. Set to 0 to disable the timeout. | `0` | +| GracefulShutdownTimeout| `time.Duration` | Specifies the maximum duration to wait for the server to gracefully shutdown. When the timeout is reached, the shutdown process is interrupted and a `context.DeadlineExceeded` error is passed to the `OnShutdownError` callback. Set to 0 (default) to disable the timeout and wait indefinitely. | `0` | | ListenerAddrFunc | `func(addr net.Addr)` | Allows accessing and customizing `net.Listener`. | `nil` | | ListenerNetwork | `string` | Known networks are "tcp", "tcp4" (IPv4-only), "tcp6" (IPv6-only). WARNING: When prefork is set to true, only "tcp4" and "tcp6" can be chosen. | `tcp4` | | OnShutdownError | `func(err error)` | Allows to customize error behavior when gracefully shutting down the server by given signal. Prints error with `log.Fatalf()` | `nil` | From 58c0feb6be454dc7f229deb19a626220e8551c68 Mon Sep 17 00:00:00 2001 From: ksw2000 <13825170+ksw2000@users.noreply.github.com> Date: Fri, 29 Nov 2024 09:16:00 +0000 Subject: [PATCH 3/8] =?UTF-8?q?=E2=99=BB=EF=B8=8Frefact:=20use=20require.E?= =?UTF-8?q?rrorIs=20instead=20of=20require.Equal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- listen_test.go | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/listen_test.go b/listen_test.go index ea8684ab81..39982de403 100644 --- a/listen_test.go +++ b/listen_test.go @@ -204,9 +204,14 @@ func Test_Listen_Graceful_Shutdown_Timeout(t *testing.T) { resp := fasthttp.AcquireResponse() err := client.Do(req, resp) - require.Equal(t, tc.ExpectedErr, err) - require.Equal(t, tc.ExpectedStatusCode, resp.StatusCode()) - require.Equal(t, tc.ExpectedBody, string(resp.Body())) + if err == nil { + require.NoError(t, err) + require.Equal(t, tc.ExpectedStatusCode, resp.StatusCode()) + require.Equal(t, tc.ExpectedBody, string(resp.Body())) + } else { + require.ErrorIs(t, err, tc.ExpectedErr) + } + mu.Lock() require.Equal(t, tc.ExpectedShutdownSuccess, shutdownSuccess) require.Equal(t, tc.ExpectedShutdownError, shutdownTimeoutError) From fbd364857ebd4dcf59aaa176a5ad5d36f3ca8c74 Mon Sep 17 00:00:00 2001 From: ksw2000 <13825170+ksw2000@users.noreply.github.com> Date: Fri, 29 Nov 2024 09:29:40 +0000 Subject: [PATCH 4/8] fix: Target error should be in err chain by using fasthttputil.ErrInmemoryListenerClosed --- listen_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/listen_test.go b/listen_test.go index 39982de403..2088d6a879 100644 --- a/listen_test.go +++ b/listen_test.go @@ -85,7 +85,7 @@ func Test_Listen_Graceful_Shutdown(t *testing.T) { ExpectedStatusCode int }{ {Time: 500 * time.Millisecond, ExpectedBody: "example.com", ExpectedStatusCode: StatusOK, ExpectedErr: nil}, - {Time: 3 * time.Second, ExpectedBody: "", ExpectedStatusCode: StatusOK, ExpectedErr: errors.New("InmemoryListener is already closed: use of closed network connection")}, + {Time: 3 * time.Second, ExpectedBody: "", ExpectedStatusCode: StatusOK, ExpectedErr: fasthttputil.ErrInmemoryListenerClosed}, } for _, tc := range testCases { @@ -186,7 +186,7 @@ func Test_Listen_Graceful_Shutdown_Timeout(t *testing.T) { Time: 3 * time.Second, ExpectedBody: "", ExpectedStatusCode: StatusOK, - ExpectedErr: errors.New("InmemoryListener is already closed: use of closed network connection"), + ExpectedErr: fasthttputil.ErrInmemoryListenerClosed, ExpectedShutdownError: context.DeadlineExceeded, ExpectedShutdownSuccess: false, }, @@ -209,7 +209,7 @@ func Test_Listen_Graceful_Shutdown_Timeout(t *testing.T) { require.Equal(t, tc.ExpectedStatusCode, resp.StatusCode()) require.Equal(t, tc.ExpectedBody, string(resp.Body())) } else { - require.ErrorIs(t, err, tc.ExpectedErr) + require.Equal(t, tc.ExpectedErr, err) } mu.Lock() From dd466cfdb744b5f81b0cc9061024697e4ac431bb Mon Sep 17 00:00:00 2001 From: ksw2000 <13825170+ksw2000@users.noreply.github.com> Date: Fri, 29 Nov 2024 09:33:38 +0000 Subject: [PATCH 5/8] =?UTF-8?q?=E2=99=BB=EF=B8=8Frefact:=20use=20require.E?= =?UTF-8?q?rrorIs=20instead=20of=20require.Equal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- listen_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/listen_test.go b/listen_test.go index 2088d6a879..f1db265c20 100644 --- a/listen_test.go +++ b/listen_test.go @@ -209,7 +209,7 @@ func Test_Listen_Graceful_Shutdown_Timeout(t *testing.T) { require.Equal(t, tc.ExpectedStatusCode, resp.StatusCode()) require.Equal(t, tc.ExpectedBody, string(resp.Body())) } else { - require.Equal(t, tc.ExpectedErr, err) + require.ErrorIs(t, err, tc.ExpectedErr) } mu.Lock() From dd0b5286e2cda10261529dcc455869ec8e91ed7b Mon Sep 17 00:00:00 2001 From: ksw2000 <13825170+ksw2000@users.noreply.github.com> Date: Mon, 2 Dec 2024 11:59:09 +0000 Subject: [PATCH 6/8] =?UTF-8?q?=F0=9F=93=9Adoc:=20update=20the=20descripti?= =?UTF-8?q?on=20of=20GracefulShutdownTimeout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/api/fiber.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/fiber.md b/docs/api/fiber.md index 3a6fa9bb20..d8fd7b04f4 100644 --- a/docs/api/fiber.md +++ b/docs/api/fiber.md @@ -108,7 +108,7 @@ app.Listen(":8080", fiber.ListenConfig{ | EnablePrefork | `bool` | When set to true, this will spawn multiple Go processes listening on the same port. | `false` | | EnablePrintRoutes | `bool` | If set to true, will print all routes with their method, path, and handler. | `false` | | GracefulContext | `context.Context` | Field to shutdown Fiber by given context gracefully. | `nil` | -| GracefulShutdownTimeout| `time.Duration` | Specifies the maximum duration to wait for the server to gracefully shutdown. When the timeout is reached, the shutdown process is interrupted and a `context.DeadlineExceeded` error is passed to the `OnShutdownError` callback. Set to 0 (default) to disable the timeout and wait indefinitely. | `0` | +| GracefulShutdownTimeout| `time.Duration` | Specifies the maximum duration to wait for the server to gracefully shutdown. When the timeout is reached, the graceful shutdown process is interrupted and forcibly terminated, and the `context.DeadlineExceeded` error is passed to the `OnShutdownError` callback. Set to 0 (default) to disable the timeout and wait indefinitely. | `0` | | ListenerAddrFunc | `func(addr net.Addr)` | Allows accessing and customizing `net.Listener`. | `nil` | | ListenerNetwork | `string` | Known networks are "tcp", "tcp4" (IPv4-only), "tcp6" (IPv6-only). WARNING: When prefork is set to true, only "tcp4" and "tcp6" can be chosen. | `tcp4` | | OnShutdownError | `func(err error)` | Allows to customize error behavior when gracefully shutting down the server by given signal. Prints error with `log.Fatalf()` | `nil` | From 95b1fbc9ca3fabdb077bea30b937a6c0f5c63a0c Mon Sep 17 00:00:00 2001 From: ksw2000 <13825170+ksw2000@users.noreply.github.com> Date: Wed, 4 Dec 2024 04:31:10 +0000 Subject: [PATCH 7/8] =?UTF-8?q?=E2=99=BB=EF=B8=8Frefact:=20rename=20Gracef?= =?UTF-8?q?ulShutdownTimeout=20to=20ShutdownTimeout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/api/fiber.md | 2 +- listen.go | 6 +++--- listen_test.go | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/api/fiber.md b/docs/api/fiber.md index 91e1c8d38f..8bdb2a6171 100644 --- a/docs/api/fiber.md +++ b/docs/api/fiber.md @@ -110,7 +110,7 @@ app.Listen(":8080", fiber.ListenConfig{ | EnablePrefork | `bool` | When set to true, this will spawn multiple Go processes listening on the same port. | `false` | | EnablePrintRoutes | `bool` | If set to true, will print all routes with their method, path, and handler. | `false` | | GracefulContext | `context.Context` | Field to shutdown Fiber by given context gracefully. | `nil` | -| GracefulShutdownTimeout| `time.Duration` | Specifies the maximum duration to wait for the server to gracefully shutdown. When the timeout is reached, the graceful shutdown process is interrupted and forcibly terminated, and the `context.DeadlineExceeded` error is passed to the `OnShutdownError` callback. Set to 0 (default) to disable the timeout and wait indefinitely. | `0` | +| ShutdownTimeout | `time.Duration` | Specifies the maximum duration to wait for the server to gracefully shutdown. When the timeout is reached, the graceful shutdown process is interrupted and forcibly terminated, and the `context.DeadlineExceeded` error is passed to the `OnShutdownError` callback. Set to 0 (default) to disable the timeout and wait indefinitely. | `0` | | ListenerAddrFunc | `func(addr net.Addr)` | Allows accessing and customizing `net.Listener`. | `nil` | | ListenerNetwork | `string` | Known networks are "tcp", "tcp4" (IPv4-only), "tcp6" (IPv6-only). WARNING: When prefork is set to true, only "tcp4" and "tcp6" can be chosen. | `tcp4` | | OnShutdownError | `func(err error)` | Allows to customize error behavior when gracefully shutting down the server by given signal. Prints error with `log.Fatalf()` | `nil` | diff --git a/listen.go b/listen.go index c37c7d4664..a44c7c4edf 100644 --- a/listen.go +++ b/listen.go @@ -98,7 +98,7 @@ type ListenConfig struct { // The default value is 0, which means the timeout setting is disabled. // // Default: 0 - GracefulShutdownTimeout time.Duration `json:"graceful_shutdown_timeout"` + ShutdownTimeout time.Duration `json:"shutdown_timeout"` // When set to true, it will not print out the «Fiber» ASCII art and listening address. // @@ -480,8 +480,8 @@ func (app *App) gracefulShutdown(ctx context.Context, cfg ListenConfig) { var err error - if cfg.GracefulShutdownTimeout != 0 { - err = app.ShutdownWithTimeout(cfg.GracefulShutdownTimeout) //nolint:contextcheck // TODO: Implement it + if cfg.ShutdownTimeout != 0 { + err = app.ShutdownWithTimeout(cfg.ShutdownTimeout) //nolint:contextcheck // TODO: Implement it } else { err = app.Shutdown() //nolint:contextcheck // TODO: Implement it } diff --git a/listen_test.go b/listen_test.go index f1db265c20..123cf2b3b8 100644 --- a/listen_test.go +++ b/listen_test.go @@ -135,9 +135,9 @@ func Test_Listen_Graceful_Shutdown_Timeout(t *testing.T) { defer cancel() errs <- app.Listener(ln, ListenConfig{ - DisableStartupMessage: true, - GracefulContext: ctx, - GracefulShutdownTimeout: 500 * time.Millisecond, + DisableStartupMessage: true, + GracefulContext: ctx, + ShutdownTimeout: 500 * time.Millisecond, OnShutdownSuccess: func() { mu.Lock() shutdownSuccess = true From 436898fde73aa918bfc4e79680f9fcee461d9774 Mon Sep 17 00:00:00 2001 From: ksw2000 <13825170+ksw2000@users.noreply.github.com> Date: Wed, 4 Dec 2024 12:14:45 +0000 Subject: [PATCH 8/8] =?UTF-8?q?=F0=9F=A9=B9fix:=20set=20default=20Shutdown?= =?UTF-8?q?Timeout=20to=2010s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/api/fiber.md | 2 +- listen.go | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/api/fiber.md b/docs/api/fiber.md index 8bdb2a6171..2c50339d50 100644 --- a/docs/api/fiber.md +++ b/docs/api/fiber.md @@ -110,7 +110,7 @@ app.Listen(":8080", fiber.ListenConfig{ | EnablePrefork | `bool` | When set to true, this will spawn multiple Go processes listening on the same port. | `false` | | EnablePrintRoutes | `bool` | If set to true, will print all routes with their method, path, and handler. | `false` | | GracefulContext | `context.Context` | Field to shutdown Fiber by given context gracefully. | `nil` | -| ShutdownTimeout | `time.Duration` | Specifies the maximum duration to wait for the server to gracefully shutdown. When the timeout is reached, the graceful shutdown process is interrupted and forcibly terminated, and the `context.DeadlineExceeded` error is passed to the `OnShutdownError` callback. Set to 0 (default) to disable the timeout and wait indefinitely. | `0` | +| ShutdownTimeout | `time.Duration` | Specifies the maximum duration to wait for the server to gracefully shutdown. When the timeout is reached, the graceful shutdown process is interrupted and forcibly terminated, and the `context.DeadlineExceeded` error is passed to the `OnShutdownError` callback. Set to 0 to disable the timeout and wait indefinitely. | `10 * time.Second` | | ListenerAddrFunc | `func(addr net.Addr)` | Allows accessing and customizing `net.Listener`. | `nil` | | ListenerNetwork | `string` | Known networks are "tcp", "tcp4" (IPv4-only), "tcp6" (IPv6-only). WARNING: When prefork is set to true, only "tcp4" and "tcp6" can be chosen. | `tcp4` | | OnShutdownError | `func(err error)` | Allows to customize error behavior when gracefully shutting down the server by given signal. Prints error with `log.Fatalf()` | `nil` | diff --git a/listen.go b/listen.go index a44c7c4edf..624b2539f3 100644 --- a/listen.go +++ b/listen.go @@ -95,9 +95,9 @@ type ListenConfig struct { // When the graceful shutdown begins, use this field to set the timeout // duration. If the timeout is reached, OnShutdownError will be called. - // The default value is 0, which means the timeout setting is disabled. + // Set to 0 to disable the timeout and wait indefinitely. // - // Default: 0 + // Default: 10 * time.Second ShutdownTimeout time.Duration `json:"shutdown_timeout"` // When set to true, it will not print out the «Fiber» ASCII art and listening address. @@ -122,8 +122,9 @@ func listenConfigDefault(config ...ListenConfig) ListenConfig { return ListenConfig{ ListenerNetwork: NetworkTCP4, OnShutdownError: func(err error) { - log.Fatalf("shutdown: %v", err) //nolint:revive // It's an optipn + log.Fatalf("shutdown: %v", err) //nolint:revive // It's an option }, + ShutdownTimeout: 10 * time.Second, } } @@ -134,7 +135,7 @@ func listenConfigDefault(config ...ListenConfig) ListenConfig { if cfg.OnShutdownError == nil { cfg.OnShutdownError = func(err error) { - log.Fatalf("shutdown: %v", err) //nolint:revive // It's an optipn + log.Fatalf("shutdown: %v", err) //nolint:revive // It's an option } }