From 137da86daf5bbf6eea4b927c0bdb33090557ee00 Mon Sep 17 00:00:00 2001 From: "yingjie.huang" Date: Thu, 10 Oct 2024 16:44:41 +0800 Subject: [PATCH 01/39] feat: Optimize ShutdownWithContext method in app.go - Reorder mutex lock acquisition to the start of the function - Early return if server is not running - Use defer for executing shutdown hooks - Simplify nil check for hooks - Remove TODO comment This commit improves the readability, robustness, and execution order of the shutdown process. It ensures consistent state throughout the shutdown and guarantees hook execution even in error cases. --- app.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app.go b/app.go index e0240d3c163..e3f91cddfd4 100644 --- a/app.go +++ b/app.go @@ -841,16 +841,18 @@ func (app *App) ShutdownWithTimeout(timeout time.Duration) error { // // ShutdownWithContext does not close keepalive connections so its recommended to set ReadTimeout to something else than 0. func (app *App) ShutdownWithContext(ctx context.Context) error { - if app.hooks != nil { - // TODO: check should be defered? - app.hooks.executeOnShutdownHooks() - } - app.mutex.Lock() defer app.mutex.Unlock() + if app.server == nil { return ErrNotRunning } + + // Execute shutdown hooks in a deferred function + if app.hooks != nil { + defer app.hooks.executeOnShutdownHooks() + } + return app.server.ShutdownWithContext(ctx) } From b41d084c480aa36ac6e3eaf829e65f6def255ed1 Mon Sep 17 00:00:00 2001 From: "yingjie.huang" Date: Thu, 10 Oct 2024 17:14:30 +0800 Subject: [PATCH 02/39] feat: Enhance ShutdownWithContext test for improved reliability - Add shutdown hook verification - Implement better synchronization with channels - Improve error handling and assertions - Adjust timeouts for more consistent results - Add server state check after shutdown attempt - Include comments explaining expected behavior This commit improves the comprehensiveness and reliability of the ShutdownWithContext test, ensuring proper verification of shutdown hooks, timeout behavior, and server state during long-running requests. --- app_test.go | 72 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 44 insertions(+), 28 deletions(-) diff --git a/app_test.go b/app_test.go index 6b493de1ebf..f4feca94893 100644 --- a/app_test.go +++ b/app_test.go @@ -860,6 +860,12 @@ func Test_App_ShutdownWithContext(t *testing.T) { t.Parallel() app := New() + shutdownHookCalled := false + app.Hooks().OnShutdown(func() error { + shutdownHookCalled = true + return nil + }) + app.Get("/", func(ctx Ctx) error { time.Sleep(5 * time.Second) return ctx.SendString("body") @@ -867,38 +873,48 @@ func Test_App_ShutdownWithContext(t *testing.T) { ln := fasthttputil.NewInmemoryListener() - go func() { - err := app.Listener(ln) - assert.NoError(t, err) - }() - - time.Sleep(1 * time.Second) - - go func() { - conn, err := ln.Dial() - assert.NoError(t, err) - - _, err = conn.Write([]byte("GET / HTTP/1.1\r\nHost: google.com\r\n\r\n")) - assert.NoError(t, err) - }() + serverErr := make(chan error, 1) + go func() { + serverErr <- app.Listener(ln) + }() + + time.Sleep(100 * time.Millisecond) + + clientDone := make(chan struct{}) + go func() { + conn, err := ln.Dial() + assert.NoError(t, err) + _, err = conn.Write([]byte("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")) + assert.NoError(t, err) + close(clientDone) + }() + + <-clientDone + time.Sleep(100 * time.Millisecond) + + shutdownErr := make(chan error, 1) + go func() { + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + shutdownErr <- app.ShutdownWithContext(ctx) + }() - time.Sleep(1 * time.Second) + select { + case <-time.After(2 * time.Second): + t.Fatal("shutdown did not complete in time") + case err := <-shutdownErr: + assert.Error(t, err, "Expected shutdown to return an error due to timeout") + assert.True(t, errors.Is(err, context.DeadlineExceeded), "Expected DeadlineExceeded error") + } - shutdownErr := make(chan error) - go func() { - ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) - defer cancel() - shutdownErr <- app.ShutdownWithContext(ctx) - }() + assert.True(t, shutdownHookCalled, "Shutdown hook was not called") select { - case <-time.After(5 * time.Second): - t.Fatal("idle connections not closed on shutdown") - case err := <-shutdownErr: - if err == nil || !errors.Is(err, context.DeadlineExceeded) { - t.Fatalf("unexpected err %v. Expecting %v", err, context.DeadlineExceeded) - } - } + case err := <-serverErr: + assert.NoError(t, err, "Server should have shut down without error") + default: + // Server is still running, which is expected as the long-running request prevented full shutdown + } } // go test -run Test_App_Mixed_Routes_WithSameLen From a6831665f6329835728fb06a81f6acff97814617 Mon Sep 17 00:00:00 2001 From: JIeJaitt <498938874@qq.com> Date: Sat, 12 Oct 2024 23:06:41 +0800 Subject: [PATCH 03/39] =?UTF-8?q?=F0=9F=93=9A=20Doc:=20update=20the=20docs?= =?UTF-8?q?=20to=20explain=20shutdown=20&=20hook=20execution=20order?= 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 16f23b9e614..73878c61aa4 100644 --- a/docs/api/fiber.md +++ b/docs/api/fiber.md @@ -205,7 +205,7 @@ Shutdown gracefully shuts down the server without interrupting any active connec ShutdownWithTimeout will forcefully close any active connections after the timeout expires. -ShutdownWithContext shuts down the server including by force if the context's deadline is exceeded. +ShutdownWithContext shuts down the server including by force if the context's deadline is exceeded.Shutdown hooks will still be executed, even if an error occurs during the shutdown process, as they are deferred to ensure cleanup happens regardless of errors. ```go func (app *App) Shutdown() error From 796922faf2ad4c0e2728e46e28c6cff8969d6ff1 Mon Sep 17 00:00:00 2001 From: JIeJaitt <498938874@qq.com> Date: Sun, 13 Oct 2024 00:24:59 +0800 Subject: [PATCH 04/39] =?UTF-8?q?=F0=9F=A9=B9=20Fix:=20Possible=20Data=20R?= =?UTF-8?q?ace=20on=20shutdownHookCalled=20Variable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app_test.go b/app_test.go index f4feca94893..20286d34faa 100644 --- a/app_test.go +++ b/app_test.go @@ -860,9 +860,9 @@ func Test_App_ShutdownWithContext(t *testing.T) { t.Parallel() app := New() - shutdownHookCalled := false + var shutdownHookCalled int32 app.Hooks().OnShutdown(func() error { - shutdownHookCalled = true + atomic.StoreInt32(&shutdownHookCalled, 1) return nil }) @@ -907,7 +907,7 @@ func Test_App_ShutdownWithContext(t *testing.T) { assert.True(t, errors.Is(err, context.DeadlineExceeded), "Expected DeadlineExceeded error") } - assert.True(t, shutdownHookCalled, "Shutdown hook was not called") + assert.Equal(t, int32(1), atomic.LoadInt32(&shutdownHookCalled), "Shutdown hook was not called") select { case err := <-serverErr: From e465b5bd176912d3f421f263c317d6827935da19 Mon Sep 17 00:00:00 2001 From: JIeJaitt <498938874@qq.com> Date: Sun, 13 Oct 2024 00:27:31 +0800 Subject: [PATCH 05/39] =?UTF-8?q?=F0=9F=A9=B9=20Fix:=20Remove=20the=20defa?= =?UTF-8?q?ult=20Case?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app_test.go b/app_test.go index 20286d34faa..49daaf99c84 100644 --- a/app_test.go +++ b/app_test.go @@ -912,7 +912,7 @@ func Test_App_ShutdownWithContext(t *testing.T) { select { case err := <-serverErr: assert.NoError(t, err, "Server should have shut down without error") - default: + // default: // Server is still running, which is expected as the long-running request prevented full shutdown } } From ee866ecbe615937b642f9af71c6209bbe58779b5 Mon Sep 17 00:00:00 2001 From: JIeJaitt <498938874@qq.com> Date: Sun, 13 Oct 2024 00:48:55 +0800 Subject: [PATCH 06/39] =?UTF-8?q?=F0=9F=A9=B9=20Fix:=20Import=20sync/atomi?= =?UTF-8?q?c?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/app_test.go b/app_test.go index 49daaf99c84..359d201947f 100644 --- a/app_test.go +++ b/app_test.go @@ -22,6 +22,7 @@ import ( "strings" "testing" "time" + "sync/atomic" "github.com/gofiber/utils/v2" From e0a56bef5fa3bea3546ea2f77d5f275ba8eb85ca Mon Sep 17 00:00:00 2001 From: JIeJaitt <498938874@qq.com> Date: Sun, 13 Oct 2024 19:49:29 +0800 Subject: [PATCH 07/39] =?UTF-8?q?=F0=9F=A9=B9=20Fix:=20golangci-lint=20pro?= =?UTF-8?q?blem?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app_test.go | 81 ++++++++++++++++++++++++++--------------------------- go.mod | 1 + go.sum | 2 ++ 3 files changed, 43 insertions(+), 41 deletions(-) diff --git a/app_test.go b/app_test.go index 359d201947f..e23bca57b2e 100644 --- a/app_test.go +++ b/app_test.go @@ -20,9 +20,9 @@ import ( "regexp" "runtime" "strings" + "sync/atomic" "testing" "time" - "sync/atomic" "github.com/gofiber/utils/v2" @@ -861,11 +861,11 @@ func Test_App_ShutdownWithContext(t *testing.T) { t.Parallel() app := New() - var shutdownHookCalled int32 - app.Hooks().OnShutdown(func() error { - atomic.StoreInt32(&shutdownHookCalled, 1) - return nil - }) + var shutdownHookCalled atomic.Int32 + app.Hooks().OnShutdown(func() error { + shutdownHookCalled.Store(1) + return nil + }) app.Get("/", func(ctx Ctx) error { time.Sleep(5 * time.Second) @@ -874,48 +874,47 @@ func Test_App_ShutdownWithContext(t *testing.T) { ln := fasthttputil.NewInmemoryListener() - serverErr := make(chan error, 1) - go func() { - serverErr <- app.Listener(ln) - }() - - time.Sleep(100 * time.Millisecond) - - clientDone := make(chan struct{}) - go func() { - conn, err := ln.Dial() - assert.NoError(t, err) - _, err = conn.Write([]byte("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")) - assert.NoError(t, err) - close(clientDone) - }() - + serverErr := make(chan error, 1) + go func() { + serverErr <- app.Listener(ln) + }() + + time.Sleep(100 * time.Millisecond) + + clientDone := make(chan struct{}) + go func() { + conn, err := ln.Dial() + assert.NoError(t, err) + _, err = conn.Write([]byte("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")) + assert.NoError(t, err) + close(clientDone) + }() + <-clientDone - time.Sleep(100 * time.Millisecond) + // Sleep to ensure the server has started processing the request + time.Sleep(100 * time.Millisecond) shutdownErr := make(chan error, 1) - go func() { - ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) - defer cancel() - shutdownErr <- app.ShutdownWithContext(ctx) - }() + go func() { + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + shutdownErr <- app.ShutdownWithContext(ctx) + }() select { - case <-time.After(2 * time.Second): - t.Fatal("shutdown did not complete in time") - case err := <-shutdownErr: - assert.Error(t, err, "Expected shutdown to return an error due to timeout") - assert.True(t, errors.Is(err, context.DeadlineExceeded), "Expected DeadlineExceeded error") - } + case <-time.After(2 * time.Second): + t.Fatal("shutdown did not complete in time") + case err := <-shutdownErr: + require.Error(t, err, "Expected shutdown to return an error due to timeout") + require.ErrorIs(t, err, context.DeadlineExceeded, "Expected DeadlineExceeded error") + } - assert.Equal(t, int32(1), atomic.LoadInt32(&shutdownHookCalled), "Shutdown hook was not called") + assert.Equal(t, int32(1), shutdownHookCalled.Load(), "Shutdown hook was not called") - select { - case err := <-serverErr: - assert.NoError(t, err, "Server should have shut down without error") - // default: - // Server is still running, which is expected as the long-running request prevented full shutdown - } + err := <-serverErr + assert.NoError(t, err, "Server should have shut down without error") + // default: + // Server is still running, which is expected as the long-running request prevented full shutdown } // go test -run Test_App_Mixed_Routes_WithSameLen diff --git a/go.mod b/go.mod index 8f3a2a43d36..4b15327e8a8 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( github.com/philhofer/fwd v1.1.3-0.20240612014219-fbbf4953d986 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect + golang.org/dl v0.0.0-20241001165935-bedb0f791d00 // indirect golang.org/x/net v0.29.0 // indirect golang.org/x/sys v0.25.0 // indirect golang.org/x/text v0.18.0 // indirect diff --git a/go.sum b/go.sum index 1d53c4560b2..a8c6f97f42a 100644 --- a/go.sum +++ b/go.sum @@ -29,6 +29,8 @@ github.com/valyala/fasthttp v1.56.0 h1:bEZdJev/6LCBlpdORfrLu/WOZXXxvrUQSiyniuaoW github.com/valyala/fasthttp v1.56.0/go.mod h1:sReBt3XZVnudxuLOx4J/fMrJVorWRiWY2koQKgABiVI= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +golang.org/dl v0.0.0-20241001165935-bedb0f791d00 h1:OX0WPBB1pQPZy1SL0+q5C/VuuM6e1wv6uEuB9iyBi/I= +golang.org/dl v0.0.0-20241001165935-bedb0f791d00/go.mod h1:fwQ+hlTD8I6TIzOGkQqxQNfE2xqR+y7SzGaDkksVFkw= golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= From 750a7facb922d11207a1c8fe5f4604eeb0a67ef8 Mon Sep 17 00:00:00 2001 From: JIeJaitt <498938874@qq.com> Date: Mon, 28 Oct 2024 14:25:29 +0800 Subject: [PATCH 08/39] =?UTF-8?q?=F0=9F=8E=A8=20Style:=20add=20block=20in?= =?UTF-8?q?=20api.md?= 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 73878c61aa4..a0ab078776a 100644 --- a/docs/api/fiber.md +++ b/docs/api/fiber.md @@ -205,7 +205,7 @@ Shutdown gracefully shuts down the server without interrupting any active connec ShutdownWithTimeout will forcefully close any active connections after the timeout expires. -ShutdownWithContext shuts down the server including by force if the context's deadline is exceeded.Shutdown hooks will still be executed, even if an error occurs during the shutdown process, as they are deferred to ensure cleanup happens regardless of errors. +ShutdownWithContext shuts down the server including by force if the context's deadline is exceeded. Shutdown hooks will still be executed, even if an error occurs during the shutdown process, as they are deferred to ensure cleanup happens regardless of errors. ```go func (app *App) Shutdown() error From b9509fe4d2b4b7e97f1d98970e0ece3537616372 Mon Sep 17 00:00:00 2001 From: JIeJaitt <498938874@qq.com> Date: Mon, 28 Oct 2024 16:37:52 +0800 Subject: [PATCH 09/39] =?UTF-8?q?=F0=9F=A9=B9=20Fix:=20go=20mod=20tidy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 1 - go.sum | 2 -- 2 files changed, 3 deletions(-) diff --git a/go.mod b/go.mod index dd7324b3f0d..2b5e60a1bf0 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,6 @@ require ( github.com/philhofer/fwd v1.1.3-0.20240612014219-fbbf4953d986 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect - golang.org/dl v0.0.0-20241001165935-bedb0f791d00 // indirect golang.org/x/net v0.29.0 // indirect golang.org/x/sys v0.25.0 // indirect golang.org/x/text v0.18.0 // indirect diff --git a/go.sum b/go.sum index fd50cfe1c0b..42768451af9 100644 --- a/go.sum +++ b/go.sum @@ -33,8 +33,6 @@ github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVS github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -golang.org/dl v0.0.0-20241001165935-bedb0f791d00 h1:OX0WPBB1pQPZy1SL0+q5C/VuuM6e1wv6uEuB9iyBi/I= -golang.org/dl v0.0.0-20241001165935-bedb0f791d00/go.mod h1:fwQ+hlTD8I6TIzOGkQqxQNfE2xqR+y7SzGaDkksVFkw= golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= From c2792e7bee78600918d34709af731f120f647058 Mon Sep 17 00:00:00 2001 From: "yingjie.huang" Date: Thu, 10 Oct 2024 16:44:41 +0800 Subject: [PATCH 10/39] feat: Optimize ShutdownWithContext method in app.go - Reorder mutex lock acquisition to the start of the function - Early return if server is not running - Use defer for executing shutdown hooks - Simplify nil check for hooks - Remove TODO comment This commit improves the readability, robustness, and execution order of the shutdown process. It ensures consistent state throughout the shutdown and guarantees hook execution even in error cases. --- app.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app.go b/app.go index 38a3d173196..a562cc7f0a0 100644 --- a/app.go +++ b/app.go @@ -878,16 +878,18 @@ func (app *App) ShutdownWithTimeout(timeout time.Duration) error { // // ShutdownWithContext does not close keepalive connections so its recommended to set ReadTimeout to something else than 0. func (app *App) ShutdownWithContext(ctx context.Context) error { - if app.hooks != nil { - // TODO: check should be defered? - app.hooks.executeOnShutdownHooks() - } - app.mutex.Lock() defer app.mutex.Unlock() + if app.server == nil { return ErrNotRunning } + + // Execute shutdown hooks in a deferred function + if app.hooks != nil { + defer app.hooks.executeOnShutdownHooks() + } + return app.server.ShutdownWithContext(ctx) } From 18111e55d13ee38111fbb5deb61bc62a602026ed Mon Sep 17 00:00:00 2001 From: "yingjie.huang" Date: Thu, 10 Oct 2024 17:14:30 +0800 Subject: [PATCH 11/39] feat: Enhance ShutdownWithContext test for improved reliability - Add shutdown hook verification - Implement better synchronization with channels - Improve error handling and assertions - Adjust timeouts for more consistent results - Add server state check after shutdown attempt - Include comments explaining expected behavior This commit improves the comprehensiveness and reliability of the ShutdownWithContext test, ensuring proper verification of shutdown hooks, timeout behavior, and server state during long-running requests. --- app_test.go | 72 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 44 insertions(+), 28 deletions(-) diff --git a/app_test.go b/app_test.go index 6b493de1ebf..f4feca94893 100644 --- a/app_test.go +++ b/app_test.go @@ -860,6 +860,12 @@ func Test_App_ShutdownWithContext(t *testing.T) { t.Parallel() app := New() + shutdownHookCalled := false + app.Hooks().OnShutdown(func() error { + shutdownHookCalled = true + return nil + }) + app.Get("/", func(ctx Ctx) error { time.Sleep(5 * time.Second) return ctx.SendString("body") @@ -867,38 +873,48 @@ func Test_App_ShutdownWithContext(t *testing.T) { ln := fasthttputil.NewInmemoryListener() - go func() { - err := app.Listener(ln) - assert.NoError(t, err) - }() - - time.Sleep(1 * time.Second) - - go func() { - conn, err := ln.Dial() - assert.NoError(t, err) - - _, err = conn.Write([]byte("GET / HTTP/1.1\r\nHost: google.com\r\n\r\n")) - assert.NoError(t, err) - }() + serverErr := make(chan error, 1) + go func() { + serverErr <- app.Listener(ln) + }() + + time.Sleep(100 * time.Millisecond) + + clientDone := make(chan struct{}) + go func() { + conn, err := ln.Dial() + assert.NoError(t, err) + _, err = conn.Write([]byte("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")) + assert.NoError(t, err) + close(clientDone) + }() + + <-clientDone + time.Sleep(100 * time.Millisecond) + + shutdownErr := make(chan error, 1) + go func() { + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + shutdownErr <- app.ShutdownWithContext(ctx) + }() - time.Sleep(1 * time.Second) + select { + case <-time.After(2 * time.Second): + t.Fatal("shutdown did not complete in time") + case err := <-shutdownErr: + assert.Error(t, err, "Expected shutdown to return an error due to timeout") + assert.True(t, errors.Is(err, context.DeadlineExceeded), "Expected DeadlineExceeded error") + } - shutdownErr := make(chan error) - go func() { - ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) - defer cancel() - shutdownErr <- app.ShutdownWithContext(ctx) - }() + assert.True(t, shutdownHookCalled, "Shutdown hook was not called") select { - case <-time.After(5 * time.Second): - t.Fatal("idle connections not closed on shutdown") - case err := <-shutdownErr: - if err == nil || !errors.Is(err, context.DeadlineExceeded) { - t.Fatalf("unexpected err %v. Expecting %v", err, context.DeadlineExceeded) - } - } + case err := <-serverErr: + assert.NoError(t, err, "Server should have shut down without error") + default: + // Server is still running, which is expected as the long-running request prevented full shutdown + } } // go test -run Test_App_Mixed_Routes_WithSameLen From 83ea43d4f688ec4f961e00b5983c116133ce2d97 Mon Sep 17 00:00:00 2001 From: JIeJaitt <498938874@qq.com> Date: Sat, 12 Oct 2024 23:06:41 +0800 Subject: [PATCH 12/39] =?UTF-8?q?=F0=9F=93=9A=20Doc:=20update=20the=20docs?= =?UTF-8?q?=20to=20explain=20shutdown=20&=20hook=20execution=20order?= 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 6892225e113..4cb1e0a77c0 100644 --- a/docs/api/fiber.md +++ b/docs/api/fiber.md @@ -205,7 +205,7 @@ Shutdown gracefully shuts down the server without interrupting any active connec ShutdownWithTimeout will forcefully close any active connections after the timeout expires. -ShutdownWithContext shuts down the server including by force if the context's deadline is exceeded. +ShutdownWithContext shuts down the server including by force if the context's deadline is exceeded.Shutdown hooks will still be executed, even if an error occurs during the shutdown process, as they are deferred to ensure cleanup happens regardless of errors. ```go func (app *App) Shutdown() error From 66dcb42ec96dff22439b20c9438e00a46ae7664d Mon Sep 17 00:00:00 2001 From: JIeJaitt <498938874@qq.com> Date: Sun, 13 Oct 2024 00:24:59 +0800 Subject: [PATCH 13/39] =?UTF-8?q?=F0=9F=A9=B9=20Fix:=20Possible=20Data=20R?= =?UTF-8?q?ace=20on=20shutdownHookCalled=20Variable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app_test.go b/app_test.go index f4feca94893..20286d34faa 100644 --- a/app_test.go +++ b/app_test.go @@ -860,9 +860,9 @@ func Test_App_ShutdownWithContext(t *testing.T) { t.Parallel() app := New() - shutdownHookCalled := false + var shutdownHookCalled int32 app.Hooks().OnShutdown(func() error { - shutdownHookCalled = true + atomic.StoreInt32(&shutdownHookCalled, 1) return nil }) @@ -907,7 +907,7 @@ func Test_App_ShutdownWithContext(t *testing.T) { assert.True(t, errors.Is(err, context.DeadlineExceeded), "Expected DeadlineExceeded error") } - assert.True(t, shutdownHookCalled, "Shutdown hook was not called") + assert.Equal(t, int32(1), atomic.LoadInt32(&shutdownHookCalled), "Shutdown hook was not called") select { case err := <-serverErr: From f3902c5729f4f40dc1c4bc71e7e6cc1809b8b33d Mon Sep 17 00:00:00 2001 From: JIeJaitt <498938874@qq.com> Date: Sun, 13 Oct 2024 00:27:31 +0800 Subject: [PATCH 14/39] =?UTF-8?q?=F0=9F=A9=B9=20Fix:=20Remove=20the=20defa?= =?UTF-8?q?ult=20Case?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app_test.go b/app_test.go index 20286d34faa..49daaf99c84 100644 --- a/app_test.go +++ b/app_test.go @@ -912,7 +912,7 @@ func Test_App_ShutdownWithContext(t *testing.T) { select { case err := <-serverErr: assert.NoError(t, err, "Server should have shut down without error") - default: + // default: // Server is still running, which is expected as the long-running request prevented full shutdown } } From da193ac1037063304f73c51424972f5df22f20ef Mon Sep 17 00:00:00 2001 From: JIeJaitt <498938874@qq.com> Date: Sun, 13 Oct 2024 00:48:55 +0800 Subject: [PATCH 15/39] =?UTF-8?q?=F0=9F=A9=B9=20Fix:=20Import=20sync/atomi?= =?UTF-8?q?c?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/app_test.go b/app_test.go index 49daaf99c84..359d201947f 100644 --- a/app_test.go +++ b/app_test.go @@ -22,6 +22,7 @@ import ( "strings" "testing" "time" + "sync/atomic" "github.com/gofiber/utils/v2" From 0a921254c20725c07edcbbe2b23fcc5e99c65c40 Mon Sep 17 00:00:00 2001 From: JIeJaitt <498938874@qq.com> Date: Sun, 13 Oct 2024 19:49:29 +0800 Subject: [PATCH 16/39] =?UTF-8?q?=F0=9F=A9=B9=20Fix:=20golangci-lint=20pro?= =?UTF-8?q?blem?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app_test.go | 81 ++++++++++++++++++++++++++--------------------------- go.mod | 1 + go.sum | 2 ++ 3 files changed, 43 insertions(+), 41 deletions(-) diff --git a/app_test.go b/app_test.go index 359d201947f..e23bca57b2e 100644 --- a/app_test.go +++ b/app_test.go @@ -20,9 +20,9 @@ import ( "regexp" "runtime" "strings" + "sync/atomic" "testing" "time" - "sync/atomic" "github.com/gofiber/utils/v2" @@ -861,11 +861,11 @@ func Test_App_ShutdownWithContext(t *testing.T) { t.Parallel() app := New() - var shutdownHookCalled int32 - app.Hooks().OnShutdown(func() error { - atomic.StoreInt32(&shutdownHookCalled, 1) - return nil - }) + var shutdownHookCalled atomic.Int32 + app.Hooks().OnShutdown(func() error { + shutdownHookCalled.Store(1) + return nil + }) app.Get("/", func(ctx Ctx) error { time.Sleep(5 * time.Second) @@ -874,48 +874,47 @@ func Test_App_ShutdownWithContext(t *testing.T) { ln := fasthttputil.NewInmemoryListener() - serverErr := make(chan error, 1) - go func() { - serverErr <- app.Listener(ln) - }() - - time.Sleep(100 * time.Millisecond) - - clientDone := make(chan struct{}) - go func() { - conn, err := ln.Dial() - assert.NoError(t, err) - _, err = conn.Write([]byte("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")) - assert.NoError(t, err) - close(clientDone) - }() - + serverErr := make(chan error, 1) + go func() { + serverErr <- app.Listener(ln) + }() + + time.Sleep(100 * time.Millisecond) + + clientDone := make(chan struct{}) + go func() { + conn, err := ln.Dial() + assert.NoError(t, err) + _, err = conn.Write([]byte("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")) + assert.NoError(t, err) + close(clientDone) + }() + <-clientDone - time.Sleep(100 * time.Millisecond) + // Sleep to ensure the server has started processing the request + time.Sleep(100 * time.Millisecond) shutdownErr := make(chan error, 1) - go func() { - ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) - defer cancel() - shutdownErr <- app.ShutdownWithContext(ctx) - }() + go func() { + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + shutdownErr <- app.ShutdownWithContext(ctx) + }() select { - case <-time.After(2 * time.Second): - t.Fatal("shutdown did not complete in time") - case err := <-shutdownErr: - assert.Error(t, err, "Expected shutdown to return an error due to timeout") - assert.True(t, errors.Is(err, context.DeadlineExceeded), "Expected DeadlineExceeded error") - } + case <-time.After(2 * time.Second): + t.Fatal("shutdown did not complete in time") + case err := <-shutdownErr: + require.Error(t, err, "Expected shutdown to return an error due to timeout") + require.ErrorIs(t, err, context.DeadlineExceeded, "Expected DeadlineExceeded error") + } - assert.Equal(t, int32(1), atomic.LoadInt32(&shutdownHookCalled), "Shutdown hook was not called") + assert.Equal(t, int32(1), shutdownHookCalled.Load(), "Shutdown hook was not called") - select { - case err := <-serverErr: - assert.NoError(t, err, "Server should have shut down without error") - // default: - // Server is still running, which is expected as the long-running request prevented full shutdown - } + err := <-serverErr + assert.NoError(t, err, "Server should have shut down without error") + // default: + // Server is still running, which is expected as the long-running request prevented full shutdown } // go test -run Test_App_Mixed_Routes_WithSameLen diff --git a/go.mod b/go.mod index 2b5e60a1bf0..dd7324b3f0d 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( github.com/philhofer/fwd v1.1.3-0.20240612014219-fbbf4953d986 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect + golang.org/dl v0.0.0-20241001165935-bedb0f791d00 // indirect golang.org/x/net v0.29.0 // indirect golang.org/x/sys v0.25.0 // indirect golang.org/x/text v0.18.0 // indirect diff --git a/go.sum b/go.sum index 42768451af9..fd50cfe1c0b 100644 --- a/go.sum +++ b/go.sum @@ -33,6 +33,8 @@ github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVS github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +golang.org/dl v0.0.0-20241001165935-bedb0f791d00 h1:OX0WPBB1pQPZy1SL0+q5C/VuuM6e1wv6uEuB9iyBi/I= +golang.org/dl v0.0.0-20241001165935-bedb0f791d00/go.mod h1:fwQ+hlTD8I6TIzOGkQqxQNfE2xqR+y7SzGaDkksVFkw= golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= From b0bc70c71e15ca94bc2ae3c71903c0bf57d5e931 Mon Sep 17 00:00:00 2001 From: JIeJaitt <498938874@qq.com> Date: Mon, 28 Oct 2024 14:25:29 +0800 Subject: [PATCH 17/39] =?UTF-8?q?=F0=9F=8E=A8=20Style:=20add=20block=20in?= =?UTF-8?q?=20api.md?= 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 4cb1e0a77c0..55566a109df 100644 --- a/docs/api/fiber.md +++ b/docs/api/fiber.md @@ -205,7 +205,7 @@ Shutdown gracefully shuts down the server without interrupting any active connec ShutdownWithTimeout will forcefully close any active connections after the timeout expires. -ShutdownWithContext shuts down the server including by force if the context's deadline is exceeded.Shutdown hooks will still be executed, even if an error occurs during the shutdown process, as they are deferred to ensure cleanup happens regardless of errors. +ShutdownWithContext shuts down the server including by force if the context's deadline is exceeded. Shutdown hooks will still be executed, even if an error occurs during the shutdown process, as they are deferred to ensure cleanup happens regardless of errors. ```go func (app *App) Shutdown() error From 44cbc627eb910e609a24978e6855892abab39773 Mon Sep 17 00:00:00 2001 From: JIeJaitt <498938874@qq.com> Date: Mon, 28 Oct 2024 16:37:52 +0800 Subject: [PATCH 18/39] =?UTF-8?q?=F0=9F=A9=B9=20Fix:=20go=20mod=20tidy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go.mod | 1 - go.sum | 2 -- 2 files changed, 3 deletions(-) diff --git a/go.mod b/go.mod index dd7324b3f0d..2b5e60a1bf0 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,6 @@ require ( github.com/philhofer/fwd v1.1.3-0.20240612014219-fbbf4953d986 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/valyala/tcplisten v1.0.0 // indirect - golang.org/dl v0.0.0-20241001165935-bedb0f791d00 // indirect golang.org/x/net v0.29.0 // indirect golang.org/x/sys v0.25.0 // indirect golang.org/x/text v0.18.0 // indirect diff --git a/go.sum b/go.sum index fd50cfe1c0b..42768451af9 100644 --- a/go.sum +++ b/go.sum @@ -33,8 +33,6 @@ github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVS github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= -golang.org/dl v0.0.0-20241001165935-bedb0f791d00 h1:OX0WPBB1pQPZy1SL0+q5C/VuuM6e1wv6uEuB9iyBi/I= -golang.org/dl v0.0.0-20241001165935-bedb0f791d00/go.mod h1:fwQ+hlTD8I6TIzOGkQqxQNfE2xqR+y7SzGaDkksVFkw= golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= From 87b2aab9dd7c4253e624f58bdcb4cb3de7c4897d Mon Sep 17 00:00:00 2001 From: JIeJaitt <498938874@qq.com> Date: Tue, 10 Dec 2024 21:00:31 +0800 Subject: [PATCH 19/39] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20replaced?= =?UTF-8?q?=20OnShutdown=20=20by=20OnPreShutdown=20and=20OnPostShutdown?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.go | 5 ++-- app_test.go | 9 +++---- hooks.go | 65 ++++++++++++++++++++++++++++++++++++++++----------- hooks_test.go | 11 +++++---- 4 files changed, 65 insertions(+), 25 deletions(-) diff --git a/app.go b/app.go index 2775e3f92db..82ae9d55b09 100644 --- a/app.go +++ b/app.go @@ -907,10 +907,11 @@ func (app *App) ShutdownWithContext(ctx context.Context) error { return ErrNotRunning } - // Execute shutdown hooks in a deferred function + // Execute the Shutdown hook if app.hooks != nil { - defer app.hooks.executeOnShutdownHooks() + app.hooks.executeOnPreShutdownHooks() } + defer app.hooks.executeOnPostShutdownHooks(nil) return app.server.ShutdownWithContext(ctx) } diff --git a/app_test.go b/app_test.go index 3becbf7e731..78f3825247a 100644 --- a/app_test.go +++ b/app_test.go @@ -863,10 +863,11 @@ func Test_App_ShutdownWithContext(t *testing.T) { app := New() var shutdownHookCalled atomic.Int32 - app.Hooks().OnShutdown(func() error { - shutdownHookCalled.Store(1) - return nil - }) + // TODO: add test + // app.Hooks().OnShutdown(func() error { + // shutdownHookCalled.Store(1) + // return nil + // }) app.Get("/", func(ctx Ctx) error { time.Sleep(5 * time.Second) diff --git a/hooks.go b/hooks.go index 3da5c671ffa..7f926832c31 100644 --- a/hooks.go +++ b/hooks.go @@ -11,9 +11,11 @@ type ( OnGroupHandler = func(Group) error OnGroupNameHandler = OnGroupHandler OnListenHandler = func(ListenData) error - OnShutdownHandler = func() error - OnForkHandler = func(int) error - OnMountHandler = func(*App) error + // OnShutdownHandler = func() error + OnPreShutdownHandler = func() error + OnPostShutdownHandler = func(error) error + OnForkHandler = func(int) error + OnMountHandler = func(*App) error ) // Hooks is a struct to use it with App. @@ -27,9 +29,11 @@ type Hooks struct { onGroup []OnGroupHandler onGroupName []OnGroupNameHandler onListen []OnListenHandler - onShutdown []OnShutdownHandler - onFork []OnForkHandler - onMount []OnMountHandler + // onShutdown []OnShutdownHandler + onPreShutdown []OnPreShutdownHandler + onPostShutdown []OnPostShutdownHandler + onFork []OnForkHandler + onMount []OnMountHandler } // ListenData is a struct to use it with OnListenHandler @@ -47,9 +51,11 @@ func newHooks(app *App) *Hooks { onGroupName: make([]OnGroupNameHandler, 0), onName: make([]OnNameHandler, 0), onListen: make([]OnListenHandler, 0), - onShutdown: make([]OnShutdownHandler, 0), - onFork: make([]OnForkHandler, 0), - onMount: make([]OnMountHandler, 0), + // onShutdown: make([]OnShutdownHandler, 0), + onPreShutdown: make([]OnPreShutdownHandler, 0), + onPostShutdown: make([]OnPostShutdownHandler, 0), + onFork: make([]OnForkHandler, 0), + onMount: make([]OnMountHandler, 0), } } @@ -96,10 +102,25 @@ func (h *Hooks) OnListen(handler ...OnListenHandler) { h.app.mutex.Unlock() } +// TODO:To be deleted, replaced by OnPreShutdown and OnPostShutdown // OnShutdown is a hook to execute user functions after Shutdown. -func (h *Hooks) OnShutdown(handler ...OnShutdownHandler) { +// func (h *Hooks) OnShutdown(handler ...OnShutdownHandler) { +// h.app.mutex.Lock() +// h.onShutdown = append(h.onShutdown, handler...) +// h.app.mutex.Unlock() +// } + +// OnPreShutdown is a hook to execute user functions before Shutdown. +func (h *Hooks) OnPreShutdown(handler ...OnPreShutdownHandler) { h.app.mutex.Lock() - h.onShutdown = append(h.onShutdown, handler...) + h.onPreShutdown = append(h.onPreShutdown, handler...) + h.app.mutex.Unlock() +} + +// OnPostShutdown is a hook to execute user functions after Shutdown. +func (h *Hooks) OnPostShutdown(handler ...OnPostShutdownHandler) { + h.app.mutex.Lock() + h.onPostShutdown = append(h.onPostShutdown, handler...) h.app.mutex.Unlock() } @@ -191,10 +212,26 @@ func (h *Hooks) executeOnListenHooks(listenData ListenData) error { return nil } -func (h *Hooks) executeOnShutdownHooks() { - for _, v := range h.onShutdown { +// func (h *Hooks) executeOnShutdownHooks() { +// for _, v := range h.onShutdown { +// if err := v(); err != nil { +// log.Errorf("failed to call shutdown hook: %v", err) +// } +// } +// } + +func (h *Hooks) executeOnPreShutdownHooks() { + for _, v := range h.onPreShutdown { if err := v(); err != nil { - log.Errorf("failed to call shutdown hook: %v", err) + log.Errorf("failed to call pre shutdown hook: %v", err) + } + } +} + +func (h *Hooks) executeOnPostShutdownHooks(err error) { + for _, v := range h.onPostShutdown { + if err := v(err); err != nil { + log.Errorf("failed to call pre shutdown hook: %v", err) } } } diff --git a/hooks_test.go b/hooks_test.go index f96f5707064..c313031c083 100644 --- a/hooks_test.go +++ b/hooks_test.go @@ -188,12 +188,13 @@ func Test_Hook_OnShutdown(t *testing.T) { buf := bytebufferpool.Get() defer bytebufferpool.Put(buf) - app.Hooks().OnShutdown(func() error { - _, err := buf.WriteString("shutdowning") - require.NoError(t, err) + // TODO: add test + // app.Hooks().OnShutdown(func() error { + // _, err := buf.WriteString("shutdowning") + // require.NoError(t, err) - return nil - }) + // return nil + // }) require.NoError(t, app.Shutdown()) require.Equal(t, "shutdowning", buf.String()) From 33899134a450d26d7612e1fa35dc1770ab1096cf Mon Sep 17 00:00:00 2001 From: JIeJaitt <498938874@qq.com> Date: Wed, 11 Dec 2024 10:34:34 +0800 Subject: [PATCH 20/39] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20streamli?= =?UTF-8?q?ne=20post-shutdown=20hook=20execution=20in=20graceful=20shutdow?= =?UTF-8?q?n=20process?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.go | 1 - hooks.go | 2 +- hooks_test.go | 32 +++++++++++++++++++++++++------- listen.go | 6 ++---- 4 files changed, 28 insertions(+), 13 deletions(-) diff --git a/app.go b/app.go index 82ae9d55b09..f6a24546a89 100644 --- a/app.go +++ b/app.go @@ -911,7 +911,6 @@ func (app *App) ShutdownWithContext(ctx context.Context) error { if app.hooks != nil { app.hooks.executeOnPreShutdownHooks() } - defer app.hooks.executeOnPostShutdownHooks(nil) return app.server.ShutdownWithContext(ctx) } diff --git a/hooks.go b/hooks.go index 7f926832c31..86a273d8fcf 100644 --- a/hooks.go +++ b/hooks.go @@ -231,7 +231,7 @@ func (h *Hooks) executeOnPreShutdownHooks() { func (h *Hooks) executeOnPostShutdownHooks(err error) { for _, v := range h.onPostShutdown { if err := v(err); err != nil { - log.Errorf("failed to call pre shutdown hook: %v", err) + log.Errorf("failed to call post shutdown hook: %v", err) } } } diff --git a/hooks_test.go b/hooks_test.go index c313031c083..4cbfa20297b 100644 --- a/hooks_test.go +++ b/hooks_test.go @@ -181,20 +181,38 @@ func Test_Hook_OnGroupName_Error(t *testing.T) { grp.Get("/test", testSimpleHandler) } -func Test_Hook_OnShutdown(t *testing.T) { +// func Test_Hook_OnShutdown(t *testing.T) { +// t.Parallel() +// app := New() + +// buf := bytebufferpool.Get() +// defer bytebufferpool.Put(buf) + +// // TODO: add test +// app.Hooks().OnShutdown(func() error { +// _, err := buf.WriteString("shutdowning") +// require.NoError(t, err) + +// return nil +// }) + +// require.NoError(t, app.Shutdown()) +// require.Equal(t, "shutdowning", buf.String()) +// } + +func Test_Hook_OnPrehutdown(t *testing.T) { t.Parallel() app := New() buf := bytebufferpool.Get() defer bytebufferpool.Put(buf) - // TODO: add test - // app.Hooks().OnShutdown(func() error { - // _, err := buf.WriteString("shutdowning") - // require.NoError(t, err) + app.Hooks().OnPreShutdown(func() error { + _, err := buf.WriteString("shutdowning") + require.NoError(t, err) - // return nil - // }) + return nil + }) require.NoError(t, app.Shutdown()) require.Equal(t, "shutdowning", buf.String()) diff --git a/listen.go b/listen.go index e0c55369684..739d81983a0 100644 --- a/listen.go +++ b/listen.go @@ -502,11 +502,9 @@ func (app *App) gracefulShutdown(ctx context.Context, cfg ListenConfig) { } if err != nil { - cfg.OnShutdownError(err) + app.hooks.executeOnPostShutdownHooks(err) return } - if success := cfg.OnShutdownSuccess; success != nil { - success() - } + app.hooks.executeOnPostShutdownHooks(nil) } From 6aeb04825241e1dc41be1760cfe090815b042155 Mon Sep 17 00:00:00 2001 From: JIeJaitt <498938874@qq.com> Date: Wed, 11 Dec 2024 16:59:12 +0800 Subject: [PATCH 21/39] =?UTF-8?q?=F0=9F=9A=A8=20Test:=20add=20test=20for?= =?UTF-8?q?=20gracefulShutdown?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app_test.go | 10 +-- hooks.go | 51 ++++-------- hooks_test.go | 93 ++++++++++++++++----- listen.go | 20 ----- listen_test.go | 219 ++++++++++++++++--------------------------------- 5 files changed, 162 insertions(+), 231 deletions(-) diff --git a/app_test.go b/app_test.go index 78f3825247a..cd919b21f6d 100644 --- a/app_test.go +++ b/app_test.go @@ -863,11 +863,11 @@ func Test_App_ShutdownWithContext(t *testing.T) { app := New() var shutdownHookCalled atomic.Int32 - // TODO: add test - // app.Hooks().OnShutdown(func() error { - // shutdownHookCalled.Store(1) - // return nil - // }) + + app.Hooks().OnPreShutdown(func() error { + shutdownHookCalled.Store(1) + return nil + }) app.Get("/", func(ctx Ctx) error { time.Sleep(5 * time.Second) diff --git a/hooks.go b/hooks.go index 86a273d8fcf..314717d04b3 100644 --- a/hooks.go +++ b/hooks.go @@ -6,12 +6,11 @@ import ( // OnRouteHandler Handlers define a function to create hooks for Fiber. type ( - OnRouteHandler = func(Route) error - OnNameHandler = OnRouteHandler - OnGroupHandler = func(Group) error - OnGroupNameHandler = OnGroupHandler - OnListenHandler = func(ListenData) error - // OnShutdownHandler = func() error + OnRouteHandler = func(Route) error + OnNameHandler = OnRouteHandler + OnGroupHandler = func(Group) error + OnGroupNameHandler = OnGroupHandler + OnListenHandler = func(ListenData) error OnPreShutdownHandler = func() error OnPostShutdownHandler = func(error) error OnForkHandler = func(int) error @@ -24,12 +23,11 @@ type Hooks struct { app *App // Hooks - onRoute []OnRouteHandler - onName []OnNameHandler - onGroup []OnGroupHandler - onGroupName []OnGroupNameHandler - onListen []OnListenHandler - // onShutdown []OnShutdownHandler + onRoute []OnRouteHandler + onName []OnNameHandler + onGroup []OnGroupHandler + onGroupName []OnGroupNameHandler + onListen []OnListenHandler onPreShutdown []OnPreShutdownHandler onPostShutdown []OnPostShutdownHandler onFork []OnForkHandler @@ -45,13 +43,12 @@ type ListenData struct { func newHooks(app *App) *Hooks { return &Hooks{ - app: app, - onRoute: make([]OnRouteHandler, 0), - onGroup: make([]OnGroupHandler, 0), - onGroupName: make([]OnGroupNameHandler, 0), - onName: make([]OnNameHandler, 0), - onListen: make([]OnListenHandler, 0), - // onShutdown: make([]OnShutdownHandler, 0), + app: app, + onRoute: make([]OnRouteHandler, 0), + onGroup: make([]OnGroupHandler, 0), + onGroupName: make([]OnGroupNameHandler, 0), + onName: make([]OnNameHandler, 0), + onListen: make([]OnListenHandler, 0), onPreShutdown: make([]OnPreShutdownHandler, 0), onPostShutdown: make([]OnPostShutdownHandler, 0), onFork: make([]OnForkHandler, 0), @@ -102,14 +99,6 @@ func (h *Hooks) OnListen(handler ...OnListenHandler) { h.app.mutex.Unlock() } -// TODO:To be deleted, replaced by OnPreShutdown and OnPostShutdown -// OnShutdown is a hook to execute user functions after Shutdown. -// func (h *Hooks) OnShutdown(handler ...OnShutdownHandler) { -// h.app.mutex.Lock() -// h.onShutdown = append(h.onShutdown, handler...) -// h.app.mutex.Unlock() -// } - // OnPreShutdown is a hook to execute user functions before Shutdown. func (h *Hooks) OnPreShutdown(handler ...OnPreShutdownHandler) { h.app.mutex.Lock() @@ -212,14 +201,6 @@ func (h *Hooks) executeOnListenHooks(listenData ListenData) error { return nil } -// func (h *Hooks) executeOnShutdownHooks() { -// for _, v := range h.onShutdown { -// if err := v(); err != nil { -// log.Errorf("failed to call shutdown hook: %v", err) -// } -// } -// } - func (h *Hooks) executeOnPreShutdownHooks() { for _, v := range h.onPreShutdown { if err := v(); err != nil { diff --git a/hooks_test.go b/hooks_test.go index 4cbfa20297b..b39146b15eb 100644 --- a/hooks_test.go +++ b/hooks_test.go @@ -181,25 +181,6 @@ func Test_Hook_OnGroupName_Error(t *testing.T) { grp.Get("/test", testSimpleHandler) } -// func Test_Hook_OnShutdown(t *testing.T) { -// t.Parallel() -// app := New() - -// buf := bytebufferpool.Get() -// defer bytebufferpool.Put(buf) - -// // TODO: add test -// app.Hooks().OnShutdown(func() error { -// _, err := buf.WriteString("shutdowning") -// require.NoError(t, err) - -// return nil -// }) - -// require.NoError(t, app.Shutdown()) -// require.Equal(t, "shutdowning", buf.String()) -// } - func Test_Hook_OnPrehutdown(t *testing.T) { t.Parallel() app := New() @@ -208,14 +189,84 @@ func Test_Hook_OnPrehutdown(t *testing.T) { defer bytebufferpool.Put(buf) app.Hooks().OnPreShutdown(func() error { - _, err := buf.WriteString("shutdowning") + _, err := buf.WriteString("pre-shutdowning") require.NoError(t, err) return nil }) require.NoError(t, app.Shutdown()) - require.Equal(t, "shutdowning", buf.String()) + require.Equal(t, "pre-shutdowning", buf.String()) +} + +func Test_Hook_OnPostShutdown(t *testing.T) { + t.Run("should execute post shutdown hook with error", func(t *testing.T) { + app := New() + + hookCalled := false + var receivedErr error + expectedErr := errors.New("test shutdown error") + + app.Hooks().OnPostShutdown(func(err error) error { + hookCalled = true + receivedErr = err + return nil + }) + + go func() { + _ = app.Listen(":0") + }() + + time.Sleep(100 * time.Millisecond) + + app.hooks.executeOnPostShutdownHooks(expectedErr) + + if !hookCalled { + t.Fatal("hook was not called") + } + + if receivedErr != expectedErr { + t.Fatalf("hook received wrong error: want %v, got %v", expectedErr, receivedErr) + } + }) + + t.Run("should execute multiple hooks in order", func(t *testing.T) { + app := New() + + execution := make([]int, 0) + + app.Hooks().OnPostShutdown(func(err error) error { + execution = append(execution, 1) + return nil + }) + + app.Hooks().OnPostShutdown(func(err error) error { + execution = append(execution, 2) + return nil + }) + + app.hooks.executeOnPostShutdownHooks(nil) + + if len(execution) != 2 { + t.Fatalf("expected 2 hooks to execute, got %d", len(execution)) + } + + if execution[0] != 1 || execution[1] != 2 { + t.Fatal("hooks executed in wrong order") + } + }) + + t.Run("should handle hook error", func(t *testing.T) { + app := New() + hookErr := errors.New("hook error") + + app.Hooks().OnPostShutdown(func(err error) error { + return hookErr + }) + + // Should not panic + app.hooks.executeOnPostShutdownHooks(nil) + }) } func Test_Hook_OnListen(t *testing.T) { diff --git a/listen.go b/listen.go index 739d81983a0..8fdb0ff4530 100644 --- a/listen.go +++ b/listen.go @@ -60,17 +60,6 @@ type ListenConfig struct { // Default: nil BeforeServeFunc func(app *App) error `json:"before_serve_func"` - // OnShutdownError allows to customize error behavior when to graceful shutdown server by given signal. - // - // Print error with log.Fatalf() by default. - // Default: nil - OnShutdownError func(err error) - - // OnShutdownSuccess allows to customize success behavior when to graceful shutdown server by given signal. - // - // Default: nil - OnShutdownSuccess func() - // AutoCertManager manages TLS certificates automatically using the ACME protocol, // Enables integration with Let's Encrypt or other ACME-compatible providers. // @@ -129,9 +118,6 @@ func listenConfigDefault(config ...ListenConfig) ListenConfig { if len(config) < 1 { return ListenConfig{ ListenerNetwork: NetworkTCP4, - OnShutdownError: func(err error) { - log.Fatalf("shutdown: %v", err) //nolint:revive // It's an option - }, ShutdownTimeout: 10 * time.Second, } } @@ -141,12 +127,6 @@ func listenConfigDefault(config ...ListenConfig) ListenConfig { cfg.ListenerNetwork = NetworkTCP4 } - if cfg.OnShutdownError == nil { - cfg.OnShutdownError = func(err error) { - log.Fatalf("shutdown: %v", err) //nolint:revive // It's an option - } - } - return cfg } diff --git a/listen_test.go b/listen_test.go index 123cf2b3b8e..f1376a6e7c7 100644 --- a/listen_test.go +++ b/listen_test.go @@ -15,6 +15,7 @@ import ( "testing" "time" + "github.com/gofiber/utils/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/valyala/fasthttp" @@ -37,193 +38,111 @@ func Test_Listen(t *testing.T) { // go test -run Test_Listen_Graceful_Shutdown func Test_Listen_Graceful_Shutdown(t *testing.T) { - var mu sync.Mutex - var shutdown bool - - app := New() - - app.Get("/", func(c Ctx) error { - return c.SendString(c.Hostname()) + t.Run("Basic Graceful Shutdown", func(t *testing.T) { + testGracefulShutdown(t, 0) }) - 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, - OnShutdownSuccess: func() { - mu.Lock() - shutdown = true - mu.Unlock() - }, - }) - }() - - // Server readiness check - for i := 0; i < 10; i++ { - conn, err := ln.Dial() - if err == nil { - conn.Close() //nolint:errcheck // ignore error - 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 - ExpectedBody string - Time time.Duration - ExpectedStatusCode int - }{ - {Time: 500 * time.Millisecond, ExpectedBody: "example.com", ExpectedStatusCode: StatusOK, ExpectedErr: nil}, - {Time: 3 * time.Second, ExpectedBody: "", ExpectedStatusCode: StatusOK, ExpectedErr: fasthttputil.ErrInmemoryListenerClosed}, - } - - 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())) - - fasthttp.ReleaseRequest(req) - fasthttp.ReleaseResponse(resp) - } - - mu.Lock() - err := <-errs - require.True(t, shutdown) - require.NoError(t, err) - mu.Unlock() + t.Run("Shutdown With Timeout", func(t *testing.T) { + testGracefulShutdown(t, 500*time.Millisecond) + }) } -// go test -run Test_Listen_Graceful_Shutdown_Timeout -func Test_Listen_Graceful_Shutdown_Timeout(t *testing.T) { +func testGracefulShutdown(t *testing.T, shutdownTimeout time.Duration) { var mu sync.Mutex - var shutdownSuccess bool - var shutdownTimeoutError error + var shutdown bool app := New() - app.Get("/", func(c Ctx) error { return c.SendString(c.Hostname()) }) ln := fasthttputil.NewInmemoryListener() - errs := make(chan error) + errs := make(chan error, 1) - go func() { - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) - defer cancel() + app.hooks.OnPostShutdown(func(err error) error { + mu.Lock() + defer mu.Unlock() + shutdown = true + return nil + }) + + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + go func() { errs <- app.Listener(ln, ListenConfig{ DisableStartupMessage: true, GracefulContext: ctx, - ShutdownTimeout: 500 * time.Millisecond, - OnShutdownSuccess: func() { - mu.Lock() - shutdownSuccess = true - mu.Unlock() - }, - OnShutdownError: func(err error) { - mu.Lock() - shutdownTimeoutError = err - mu.Unlock() - }, + ShutdownTimeout: shutdownTimeout, }) }() - // Server readiness check - for i := 0; i < 10; i++ { + require.Eventually(t, func() bool { 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) + conn.Close() + return true } + return false + }, time.Second, 100*time.Millisecond, "Server failed to become ready") + + client := fasthttp.HostClient{ + Dial: func(_ string) (net.Conn, error) { return ln.Dial() }, } testCases := []struct { - ExpectedErr error - ExpectedShutdownError error - ExpectedBody string - Time time.Duration - ExpectedStatusCode int - ExpectedShutdownSuccess bool + name string + waitTime time.Duration + expectedBody string + expectedStatusCode int + expectedErr error + closeConnection bool }{ { - Time: 100 * time.Millisecond, - ExpectedBody: "example.com", - ExpectedStatusCode: StatusOK, - ExpectedErr: nil, - ExpectedShutdownError: nil, - ExpectedShutdownSuccess: false, + name: "Server running normally", + waitTime: 500 * time.Millisecond, + expectedBody: "example.com", + expectedStatusCode: StatusOK, + expectedErr: nil, + closeConnection: true, }, { - Time: 3 * time.Second, - ExpectedBody: "", - ExpectedStatusCode: StatusOK, - ExpectedErr: fasthttputil.ErrInmemoryListenerClosed, - ExpectedShutdownError: context.DeadlineExceeded, - ExpectedShutdownSuccess: false, + name: "Server shutdown complete", + waitTime: 3 * time.Second, + expectedBody: "", + expectedStatusCode: StatusOK, + expectedErr: fasthttputil.ErrInmemoryListenerClosed, + closeConnection: true, }, } 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) - - 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) - mu.Unlock() - - fasthttp.ReleaseRequest(req) - fasthttp.ReleaseResponse(resp) + tc := tc + t.Run(tc.name, func(t *testing.T) { + time.Sleep(tc.waitTime) + + req := fasthttp.AcquireRequest() + defer fasthttp.ReleaseRequest(req) + req.SetRequestURI("http://example.com") + + resp := fasthttp.AcquireResponse() + defer fasthttp.ReleaseResponse(resp) + + err := client.Do(req, resp) + + if tc.expectedErr == nil { + assert.NoError(t, err) + assert.Equal(t, tc.expectedStatusCode, resp.StatusCode()) + assert.Equal(t, tc.expectedBody, utils.UnsafeString(resp.Body())) + } else { + assert.ErrorIs(t, err, tc.expectedErr) + } + }) } mu.Lock() - err := <-errs - require.NoError(t, err) + assert.True(t, shutdown) + assert.NoError(t, <-errs) mu.Unlock() } From 41487b58e2506c8ab53db2138d0f2f9230ced4ed Mon Sep 17 00:00:00 2001 From: JIeJaitt <498938874@qq.com> Date: Tue, 11 Feb 2025 16:18:27 +0800 Subject: [PATCH 22/39] =?UTF-8?q?=F0=9F=94=A5=20Feature:=20Using=20execute?= =?UTF-8?q?OnPreShutdownHooks=20and=20executeOnPostShutdownHooks=20Instead?= =?UTF-8?q?=20of=20OnShutdownSuccess=20and=20OnShutdownError?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.go | 17 +++++-- app_test.go | 131 ++++++++++++++++++++++++++++++++++------------------ listen.go | 6 --- 3 files changed, 99 insertions(+), 55 deletions(-) diff --git a/app.go b/app.go index b64512dfdec..79cfb1a0575 100644 --- a/app.go +++ b/app.go @@ -894,6 +894,13 @@ func (app *App) HandlersCount() uint32 { // // Make sure the program doesn't exit and waits instead for Shutdown to return. // +// Important: app.Listen() must be called in a separate goroutine, otherwise shutdown hooks will not work +// as Listen() is a blocking operation. Example: +// +// go app.Listen(":3000") +// // ... +// app.Shutdown() +// // Shutdown does not close keepalive connections so its recommended to set ReadTimeout to something else than 0. func (app *App) Shutdown() error { return app.ShutdownWithContext(context.Background()) @@ -921,16 +928,18 @@ func (app *App) ShutdownWithContext(ctx context.Context) error { app.mutex.Lock() defer app.mutex.Unlock() + var err error + if app.server == nil { return ErrNotRunning } // Execute the Shutdown hook - if app.hooks != nil { - app.hooks.executeOnPreShutdownHooks() - } + app.hooks.executeOnPreShutdownHooks() + defer app.hooks.executeOnPostShutdownHooks(err) - return app.server.ShutdownWithContext(ctx) + err = app.server.ShutdownWithContext(ctx) + return err } // Server returns the underlying fasthttp server diff --git a/app_test.go b/app_test.go index 2638197bf86..f467aa859c1 100644 --- a/app_test.go +++ b/app_test.go @@ -21,7 +21,7 @@ import ( "regexp" "runtime" "strings" - "sync/atomic" + "sync" "testing" "time" @@ -880,62 +880,103 @@ func Test_App_ShutdownWithTimeout(t *testing.T) { func Test_App_ShutdownWithContext(t *testing.T) { t.Parallel() - app := New() - var shutdownHookCalled atomic.Int32 - - app.Hooks().OnPreShutdown(func() error { - shutdownHookCalled.Store(1) - return nil - }) + t.Run("successful shutdown", func(t *testing.T) { + t.Parallel() + app := New() - app.Get("/", func(ctx Ctx) error { - time.Sleep(5 * time.Second) - return ctx.SendString("body") - }) + // Fast request that should complete + app.Get("/", func(c Ctx) error { + return c.SendString("OK") + }) - ln := fasthttputil.NewInmemoryListener() + ln := fasthttputil.NewInmemoryListener() + serverStarted := make(chan bool, 1) - serverErr := make(chan error, 1) - go func() { - serverErr <- app.Listener(ln) - }() + go func() { + serverStarted <- true + _ = app.Listener(ln) + }() - time.Sleep(100 * time.Millisecond) + <-serverStarted - clientDone := make(chan struct{}) - go func() { + // Execute normal request conn, err := ln.Dial() - assert.NoError(t, err) + require.NoError(t, err) _, err = conn.Write([]byte("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")) - assert.NoError(t, err) - close(clientDone) - }() - - <-clientDone - // Sleep to ensure the server has started processing the request - time.Sleep(100 * time.Millisecond) + require.NoError(t, err) - shutdownErr := make(chan error, 1) - go func() { - ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + // Shutdown with sufficient timeout + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - shutdownErr <- app.ShutdownWithContext(ctx) - }() - select { - case <-time.After(2 * time.Second): - t.Fatal("shutdown did not complete in time") - case err := <-shutdownErr: - require.Error(t, err, "Expected shutdown to return an error due to timeout") - require.ErrorIs(t, err, context.DeadlineExceeded, "Expected DeadlineExceeded error") - } + err = app.ShutdownWithContext(ctx) + require.NoError(t, err, "Expected successful shutdown") + }) + + t.Run("shutdown with hooks", func(t *testing.T) { + t.Parallel() + app := New() + + hookOrder := make([]string, 0) + var hookMutex sync.Mutex + + app.Hooks().OnPreShutdown(func() error { + hookMutex.Lock() + hookOrder = append(hookOrder, "pre") + hookMutex.Unlock() + return nil + }) + + app.Hooks().OnPostShutdown(func(err error) error { + hookMutex.Lock() + hookOrder = append(hookOrder, "post") + hookMutex.Unlock() + return nil + }) + + ln := fasthttputil.NewInmemoryListener() + go func() { + _ = app.Listener(ln) + }() + + time.Sleep(100 * time.Millisecond) + + err := app.ShutdownWithContext(context.Background()) + require.NoError(t, err) - assert.Equal(t, int32(1), shutdownHookCalled.Load(), "Shutdown hook was not called") + require.Equal(t, []string{"pre", "post"}, hookOrder, "Hooks should execute in order") + }) + + t.Run("timeout with long running request", func(t *testing.T) { + t.Parallel() + app := New() + + app.Get("/", func(c Ctx) error { + time.Sleep(2 * time.Second) + return c.SendString("OK") + }) + + ln := fasthttputil.NewInmemoryListener() + go func() { + _ = app.Listener(ln) + }() - err := <-serverErr - assert.NoError(t, err, "Server should have shut down without error") - // default: - // Server is still running, which is expected as the long-running request prevented full shutdown + time.Sleep(100 * time.Millisecond) + + // Start long request + go func() { + conn, _ := ln.Dial() + _, _ = conn.Write([]byte("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")) + }() + + time.Sleep(100 * time.Millisecond) // Wait for request to start + + ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + defer cancel() + + err := app.ShutdownWithContext(ctx) + require.ErrorIs(t, err, context.DeadlineExceeded) + }) } // go test -run Test_App_Mixed_Routes_WithSameLen diff --git a/listen.go b/listen.go index 61d361bbe37..cdc01c6ff98 100644 --- a/listen.go +++ b/listen.go @@ -134,12 +134,6 @@ func listenConfigDefault(config ...ListenConfig) ListenConfig { cfg.ListenerNetwork = NetworkTCP4 } - if cfg.OnShutdownError == nil { - cfg.OnShutdownError = func(err error) { - log.Fatalf("shutdown: %v", err) //nolint:revive // It's an option - } - } - if cfg.TLSMinVersion == 0 { cfg.TLSMinVersion = tls.VersionTLS12 } From e5a1ef5587b68d0eba60a7aff15a7579c6d39dd5 Mon Sep 17 00:00:00 2001 From: JIeJaitt <498938874@qq.com> Date: Tue, 11 Feb 2025 16:36:36 +0800 Subject: [PATCH 23/39] =?UTF-8?q?=F0=9F=A9=B9=20Fix:=20deal=20Listener=20e?= =?UTF-8?q?rr?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app_test.go | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/app_test.go b/app_test.go index f467aa859c1..d11d2574e0b 100644 --- a/app_test.go +++ b/app_test.go @@ -894,7 +894,9 @@ func Test_App_ShutdownWithContext(t *testing.T) { go func() { serverStarted <- true - _ = app.Listener(ln) + if err := app.Listener(ln); err != nil { + t.Errorf("Failed to start listener: %v", err) + } }() <-serverStarted @@ -936,7 +938,9 @@ func Test_App_ShutdownWithContext(t *testing.T) { ln := fasthttputil.NewInmemoryListener() go func() { - _ = app.Listener(ln) + if err := app.Listener(ln); err != nil { + t.Errorf("Failed to start listener: %v", err) + } }() time.Sleep(100 * time.Millisecond) @@ -965,8 +969,14 @@ func Test_App_ShutdownWithContext(t *testing.T) { // Start long request go func() { - conn, _ := ln.Dial() - _, _ = conn.Write([]byte("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")) + conn, err := ln.Dial() + if err != nil { + t.Errorf("Failed to dial: %v", err) + return + } + if _, err := conn.Write([]byte("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")); err != nil { + t.Errorf("Failed to write: %v", err) + } }() time.Sleep(100 * time.Millisecond) // Wait for request to start From bab303879be43e70694d1fd8b392bd2b0676ebf5 Mon Sep 17 00:00:00 2001 From: JIeJaitt <498938874@qq.com> Date: Tue, 11 Feb 2025 17:59:38 +0800 Subject: [PATCH 24/39] =?UTF-8?q?=F0=9F=A9=B9=20Fix:=20go=20lint=20error?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app_test.go | 6 ++++-- hooks_test.go | 14 ++++++++------ listen_test.go | 31 ++++++++++++++++++------------- 3 files changed, 30 insertions(+), 21 deletions(-) diff --git a/app_test.go b/app_test.go index d11d2574e0b..b2b35a5c3ec 100644 --- a/app_test.go +++ b/app_test.go @@ -929,7 +929,7 @@ func Test_App_ShutdownWithContext(t *testing.T) { return nil }) - app.Hooks().OnPostShutdown(func(err error) error { + app.Hooks().OnPostShutdown(func(_ error) error { hookMutex.Lock() hookOrder = append(hookOrder, "post") hookMutex.Unlock() @@ -962,7 +962,9 @@ func Test_App_ShutdownWithContext(t *testing.T) { ln := fasthttputil.NewInmemoryListener() go func() { - _ = app.Listener(ln) + if err := app.Listener(ln); err != nil { + t.Errorf("Failed to start listener: %v", err) + } }() time.Sleep(100 * time.Millisecond) diff --git a/hooks_test.go b/hooks_test.go index b39146b15eb..8054a320d6e 100644 --- a/hooks_test.go +++ b/hooks_test.go @@ -214,7 +214,9 @@ func Test_Hook_OnPostShutdown(t *testing.T) { }) go func() { - _ = app.Listen(":0") + if err := app.Listen(":0"); err != nil { + t.Errorf("Failed to start listener: %v", err) + } }() time.Sleep(100 * time.Millisecond) @@ -225,7 +227,7 @@ func Test_Hook_OnPostShutdown(t *testing.T) { t.Fatal("hook was not called") } - if receivedErr != expectedErr { + if !errors.Is(receivedErr, expectedErr) { t.Fatalf("hook received wrong error: want %v, got %v", expectedErr, receivedErr) } }) @@ -235,12 +237,12 @@ func Test_Hook_OnPostShutdown(t *testing.T) { execution := make([]int, 0) - app.Hooks().OnPostShutdown(func(err error) error { + app.Hooks().OnPostShutdown(func(_ error) error { execution = append(execution, 1) return nil }) - app.Hooks().OnPostShutdown(func(err error) error { + app.Hooks().OnPostShutdown(func(_ error) error { execution = append(execution, 2) return nil }) @@ -256,11 +258,11 @@ func Test_Hook_OnPostShutdown(t *testing.T) { } }) - t.Run("should handle hook error", func(t *testing.T) { + t.Run("should handle hook error", func(_ *testing.T) { app := New() hookErr := errors.New("hook error") - app.Hooks().OnPostShutdown(func(err error) error { + app.Hooks().OnPostShutdown(func(_ error) error { return hookErr }) diff --git a/listen_test.go b/listen_test.go index 80f6a8a6ae6..8a0ee1ff6a9 100644 --- a/listen_test.go +++ b/listen_test.go @@ -48,6 +48,8 @@ func Test_Listen_Graceful_Shutdown(t *testing.T) { } func testGracefulShutdown(t *testing.T, shutdownTimeout time.Duration) { + t.Helper() + var mu sync.Mutex var shutdown bool @@ -59,7 +61,7 @@ func testGracefulShutdown(t *testing.T, shutdownTimeout time.Duration) { ln := fasthttputil.NewInmemoryListener() errs := make(chan error, 1) - app.hooks.OnPostShutdown(func(err error) error { + app.hooks.OnPostShutdown(func(_ error) error { mu.Lock() defer mu.Unlock() shutdown = true @@ -80,7 +82,9 @@ func testGracefulShutdown(t *testing.T, shutdownTimeout time.Duration) { require.Eventually(t, func() bool { conn, err := ln.Dial() if err == nil { - conn.Close() + if err := conn.Close(); err != nil { + t.Logf("error closing connection: %v", err) + } return true } return false @@ -90,14 +94,16 @@ func testGracefulShutdown(t *testing.T, shutdownTimeout time.Duration) { Dial: func(_ string) (net.Conn, error) { return ln.Dial() }, } - testCases := []struct { + type testCase struct { name string - waitTime time.Duration + expectedErr error expectedBody string + waitTime time.Duration expectedStatusCode int - expectedErr error closeConnection bool - }{ + } + + testCases := []testCase{ { name: "Server running normally", waitTime: 500 * time.Millisecond, @@ -117,7 +123,6 @@ func testGracefulShutdown(t *testing.T, shutdownTimeout time.Duration) { } for _, tc := range testCases { - tc := tc t.Run(tc.name, func(t *testing.T) { time.Sleep(tc.waitTime) @@ -131,18 +136,18 @@ func testGracefulShutdown(t *testing.T, shutdownTimeout time.Duration) { err := client.Do(req, resp) if tc.expectedErr == nil { - assert.NoError(t, err) - assert.Equal(t, tc.expectedStatusCode, resp.StatusCode()) - assert.Equal(t, tc.expectedBody, utils.UnsafeString(resp.Body())) + require.NoError(t, err) + require.Equal(t, tc.expectedStatusCode, resp.StatusCode()) + require.Equal(t, tc.expectedBody, utils.UnsafeString(resp.Body())) } else { - assert.ErrorIs(t, err, tc.expectedErr) + require.ErrorIs(t, err, tc.expectedErr) } }) } mu.Lock() - assert.True(t, shutdown) - assert.NoError(t, <-errs) + require.True(t, shutdown) + require.NoError(t, <-errs) mu.Unlock() } From 187ad519bf57fba73ebf3b79e960aa348332031e Mon Sep 17 00:00:00 2001 From: JIeJaitt <498938874@qq.com> Date: Tue, 11 Feb 2025 18:42:59 +0800 Subject: [PATCH 25/39] =?UTF-8?q?=F0=9F=A9=B9=20Fix:=20reduced=20memory=20?= =?UTF-8?q?alignment?= 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 8a0ee1ff6a9..7bc269741a1 100644 --- a/listen_test.go +++ b/listen_test.go @@ -97,10 +97,10 @@ func testGracefulShutdown(t *testing.T, shutdownTimeout time.Duration) { type testCase struct { name string expectedErr error - expectedBody string waitTime time.Duration expectedStatusCode int closeConnection bool + expectedBody string } testCases := []testCase{ From e4de2c3033c8fd93124fbe1e515f486c1d23b385 Mon Sep 17 00:00:00 2001 From: JIeJaitt <498938874@qq.com> Date: Tue, 11 Feb 2025 19:07:36 +0800 Subject: [PATCH 26/39] =?UTF-8?q?=F0=9F=A9=B9=20Fix:=20reduced=20memory=20?= =?UTF-8?q?alignment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- listen_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/listen_test.go b/listen_test.go index 7bc269741a1..791a701bb81 100644 --- a/listen_test.go +++ b/listen_test.go @@ -95,12 +95,12 @@ func testGracefulShutdown(t *testing.T, shutdownTimeout time.Duration) { } type testCase struct { - name string expectedErr error + expectedBody string + name string waitTime time.Duration expectedStatusCode int closeConnection bool - expectedBody string } testCases := []testCase{ From 4df7e0fc376681c5d19715ff970066c7817cae68 Mon Sep 17 00:00:00 2001 From: JIeJaitt <498938874@qq.com> Date: Wed, 12 Feb 2025 09:20:59 +0800 Subject: [PATCH 27/39] =?UTF-8?q?=F0=9F=A9=B9=20Fix:=20context=20should=20?= =?UTF-8?q?be=20created=20inside=20the=20concatenation.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- listen_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/listen_test.go b/listen_test.go index 791a701bb81..8c9720765a1 100644 --- a/listen_test.go +++ b/listen_test.go @@ -68,10 +68,10 @@ func testGracefulShutdown(t *testing.T, shutdownTimeout time.Duration) { return nil }) - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) - defer cancel() - go func() { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + errs <- app.Listener(ln, ListenConfig{ DisableStartupMessage: true, GracefulContext: ctx, From ec393066d91d6a2f87a07c9e92f9493bb47091c1 Mon Sep 17 00:00:00 2001 From: JIeJaitt <498938874@qq.com> Date: Thu, 13 Feb 2025 20:20:41 +0800 Subject: [PATCH 28/39] =?UTF-8?q?=F0=9F=93=9A=20Doc:=20update=20what=5Fnew?= =?UTF-8?q?.md=20and=20hooks.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/api/hooks.md | 23 +++++++++++++++++- docs/whats_new.md | 59 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/docs/api/hooks.md b/docs/api/hooks.md index 4852866602f..bd3f105a15c 100644 --- a/docs/api/hooks.md +++ b/docs/api/hooks.md @@ -16,6 +16,8 @@ With Fiber you can execute custom user functions at specific method execution po - [OnListen](#onlisten) - [OnFork](#onfork) - [OnShutdown](#onshutdown) + - [OnPreShutdown](#onpreshutdown) + - [OnPostShutdown](#onpostshutdown) - [OnMount](#onmount) ## Constants @@ -28,7 +30,8 @@ type OnGroupHandler = func(Group) error type OnGroupNameHandler = OnGroupHandler type OnListenHandler = func(ListenData) error type OnForkHandler = func(int) error -type OnShutdownHandler = func() error +type OnPreShutdownHandler = func() error +type OnPostShutdownHandler = func(error) error type OnMountHandler = func(*App) error ``` @@ -176,12 +179,30 @@ func (h *Hooks) OnFork(handler ...OnForkHandler) ## OnShutdown +in v3, `OnShutdown` is split into `OnPreShutdown` and `OnPostShutdown`. + `OnShutdown` is a hook to execute user functions after shutdown. ```go title="Signature" func (h *Hooks) OnShutdown(handler ...OnShutdownHandler) ``` +### OnPreShutdown + +`OnPreShutdown` is a hook to execute user functions before shutdown. + +```go title="Signature" +func (h *Hooks) OnPreShutdown(handler ...OnPreShutdownHandler) +``` + +### OnPostShutdown + +`OnPostShutdown` is a hook to execute user functions after shutdown. + +```go title="Signature" +func (h *Hooks) OnPostShutdown(handler ...OnPostShutdownHandler) +``` + ## OnMount `OnMount` is a hook to execute user functions after the mounting process. The mount event is fired when a sub-app is mounted on a parent app. The parent app is passed as a parameter. It works for both app and group mounting. diff --git a/docs/whats_new.md b/docs/whats_new.md index 1958632a292..7f0b632208a 100644 --- a/docs/whats_new.md +++ b/docs/whats_new.md @@ -16,6 +16,8 @@ In this guide, we'll walk you through the most important changes in Fiber `v3` a Here's a quick overview of the changes in Fiber `v3`: - [🚀 App](#-app) +- [🎣 Hooks](#-hooks) +- [🚀 Listen](#-listen) - [🗺️ Router](#-router) - [🧠 Context](#-context) - [📎 Binding](#-binding) @@ -158,6 +160,63 @@ app.Listen(":444", fiber.ListenConfig{ }) ``` +## 🎣 Hooks + +We have made several changes to the Fiber hooks, including: + +- Added new shutdown hooks to provide better control over the shutdown process: + - `OnPreShutdown` - Executes before the server starts shutting down + - `OnPostShutdown` - Executes after the server has shut down, receives any shutdown error +- Deprecated `OnShutdown` in favor of the new pre/post shutdown hooks +- Improved shutdown hook execution order and reliability +- Added mutex protection for hook registration and execution + +Important: When using shutdown hooks, ensure app.Listen() is called in a separate goroutine: + +```go +// Correct usage +go app.Listen(":3000") +// ... register shutdown hooks +app.Shutdown() + +// Incorrect usage - hooks won't work +app.Listen(":3000") // This blocks +app.Shutdown() // Never reached +``` + +## 🚀 Listen + +We have made several changes to the Fiber listen, including: + +- Removed `OnShutdownError` and `OnShutdownSuccess` from `ListenerConfig` in favor of using `OnPostShutdown` hook which receives the shutdown error + +```go +app := fiber.New() + +// Before - using ListenerConfig callbacks +app.Listen(":3000", fiber.ListenerConfig{ + OnShutdownError: func(err error) { + log.Printf("Shutdown error: %v", err) + }, + OnShutdownSuccess: func() { + log.Println("Shutdown successful") + }, +}) + +// After - using OnPostShutdown hook +app.Hooks().OnPostShutdown(func(err error) error { + if err != nil { + log.Printf("Shutdown error: %v", err) + } else { + log.Println("Shutdown successful") + } + return nil +}) +go app.Listen(":3000") +``` + +This change simplifies the shutdown handling by consolidating the shutdown callbacks into a single hook that receives the error status. + ## 🗺 Router We have slightly adapted our router interface From 7c01623732f5304ea56659fc22e431757f477890 Mon Sep 17 00:00:00 2001 From: JIeJaitt <498938874@qq.com> Date: Thu, 13 Feb 2025 20:21:42 +0800 Subject: [PATCH 29/39] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor:=20use=20bl?= =?UTF-8?q?ocking=20channel=20instead=20of=20time.Sleep?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app_test.go | 13 +++++++++++-- hooks_test.go | 33 ++++++++++++++++++++++++--------- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/app_test.go b/app_test.go index b2b35a5c3ec..740c173b10e 100644 --- a/app_test.go +++ b/app_test.go @@ -846,20 +846,29 @@ func Test_App_ShutdownWithTimeout(t *testing.T) { }) ln := fasthttputil.NewInmemoryListener() + serverReady := make(chan struct{}) // Signal that the server is ready to start + go func() { + serverReady <- struct{}{} err := app.Listener(ln) assert.NoError(t, err) }() - time.Sleep(1 * time.Second) + <-serverReady // Waiting for the server to be ready + + // Create a connection and send a request + connReady := make(chan struct{}) go func() { conn, err := ln.Dial() assert.NoError(t, err) _, err = conn.Write([]byte("GET / HTTP/1.1\r\nHost: google.com\r\n\r\n")) assert.NoError(t, err) + + connReady <- struct{}{} // Signal that the request has been sent }() - time.Sleep(1 * time.Second) + + <-connReady // Waiting for the request to be sent shutdownErr := make(chan error) go func() { diff --git a/hooks_test.go b/hooks_test.go index 8054a320d6e..11006d57348 100644 --- a/hooks_test.go +++ b/hooks_test.go @@ -202,33 +202,48 @@ func Test_Hook_OnPrehutdown(t *testing.T) { func Test_Hook_OnPostShutdown(t *testing.T) { t.Run("should execute post shutdown hook with error", func(t *testing.T) { app := New() - - hookCalled := false - var receivedErr error expectedErr := errors.New("test shutdown error") + // Use channels to synchronize and pass results + hookResult := make(chan struct { + err error + called bool + }, 1) + app.Hooks().OnPostShutdown(func(err error) error { - hookCalled = true - receivedErr = err + hookResult <- struct { + err error + called bool + }{ + err: err, + called: true, + } return nil }) + // Use channel to make sure the server is up + serverReady := make(chan struct{}) + go func() { + serverReady <- struct{}{} // Signal that the server is ready to start if err := app.Listen(":0"); err != nil { t.Errorf("Failed to start listener: %v", err) } }() - time.Sleep(100 * time.Millisecond) + <-serverReady // Wait for the server to be ready app.hooks.executeOnPostShutdownHooks(expectedErr) - if !hookCalled { + // Wait for the hook to finish executing and get the result + result := <-hookResult + + if !result.called { t.Fatal("hook was not called") } - if !errors.Is(receivedErr, expectedErr) { - t.Fatalf("hook received wrong error: want %v, got %v", expectedErr, receivedErr) + if !errors.Is(result.err, expectedErr) { + t.Fatalf("hook received wrong error: want %v, got %v", expectedErr, result.err) } }) From 0f63b6bf280e3d982da7e47a6499f86c756e81f3 Mon Sep 17 00:00:00 2001 From: JIeJaitt <498938874@qq.com> Date: Thu, 13 Feb 2025 20:41:33 +0800 Subject: [PATCH 30/39] =?UTF-8?q?=F0=9F=A9=B9=20Fix:=20Improve=20synchroni?= =?UTF-8?q?zation=20in=20error=20propagation=20test.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hooks_test.go | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/hooks_test.go b/hooks_test.go index 11006d57348..d35b02e6c5e 100644 --- a/hooks_test.go +++ b/hooks_test.go @@ -3,6 +3,7 @@ package fiber import ( "errors" "fmt" + "sync" "testing" "time" @@ -203,12 +204,15 @@ func Test_Hook_OnPostShutdown(t *testing.T) { t.Run("should execute post shutdown hook with error", func(t *testing.T) { app := New() expectedErr := errors.New("test shutdown error") + var wg sync.WaitGroup + wg.Add(1) // Use channels to synchronize and pass results hookResult := make(chan struct { err error called bool - }, 1) + }, 1) // Buffer size of 1 prevents deadlock + defer close(hookResult) app.Hooks().OnPostShutdown(func(err error) error { hookResult <- struct { @@ -223,8 +227,10 @@ func Test_Hook_OnPostShutdown(t *testing.T) { // Use channel to make sure the server is up serverReady := make(chan struct{}) + defer close(serverReady) go func() { + defer wg.Done() serverReady <- struct{}{} // Signal that the server is ready to start if err := app.Listen(":0"); err != nil { t.Errorf("Failed to start listener: %v", err) @@ -236,7 +242,16 @@ func Test_Hook_OnPostShutdown(t *testing.T) { app.hooks.executeOnPostShutdownHooks(expectedErr) // Wait for the hook to finish executing and get the result - result := <-hookResult + var result struct { + err error + called bool + } + + select { + case result = <-hookResult: + case <-time.After(5 * time.Second): + t.Fatal("Hook execution timeout") + } if !result.called { t.Fatal("hook was not called") @@ -245,6 +260,7 @@ func Test_Hook_OnPostShutdown(t *testing.T) { if !errors.Is(result.err, expectedErr) { t.Fatalf("hook received wrong error: want %v, got %v", expectedErr, result.err) } + wg.Wait() // Ensure server goroutine completes }) t.Run("should execute multiple hooks in order", func(t *testing.T) { From 294e1cdf5b6f6bc8756470716a9cfcdd6cc4deb3 Mon Sep 17 00:00:00 2001 From: JIeJaitt <498938874@qq.com> Date: Thu, 13 Feb 2025 20:43:48 +0800 Subject: [PATCH 31/39] =?UTF-8?q?=F0=9F=A9=B9=20Fix:=20Replace=20sleep=20w?= =?UTF-8?q?ith=20proper=20synchronization.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app_test.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app_test.go b/app_test.go index 740c173b10e..27ae242b1a1 100644 --- a/app_test.go +++ b/app_test.go @@ -964,7 +964,9 @@ func Test_App_ShutdownWithContext(t *testing.T) { t.Parallel() app := New() + requestStarted := make(chan struct{}) app.Get("/", func(c Ctx) error { + close(requestStarted) time.Sleep(2 * time.Second) return c.SendString("OK") }) @@ -976,7 +978,12 @@ func Test_App_ShutdownWithContext(t *testing.T) { } }() - time.Sleep(100 * time.Millisecond) + select { + case <-requestStarted: + // Request has started processing + case <-time.After(time.Second): + t.Fatal("Request did not start in time") + } // Start long request go func() { From 0860595d177bb69193126a2b6747f05f56efa0f1 Mon Sep 17 00:00:00 2001 From: JIeJaitt <498938874@qq.com> Date: Fri, 14 Feb 2025 09:59:16 +0800 Subject: [PATCH 32/39] =?UTF-8?q?=F0=9F=A9=B9=20Fix:=20Server=20but=20not?= =?UTF-8?q?=20shut=20down=20properly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hooks_test.go | 56 +++++++++++++++++++++++++-------------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/hooks_test.go b/hooks_test.go index d35b02e6c5e..5c14b85f2ac 100644 --- a/hooks_test.go +++ b/hooks_test.go @@ -208,14 +208,14 @@ func Test_Hook_OnPostShutdown(t *testing.T) { wg.Add(1) // Use channels to synchronize and pass results - hookResult := make(chan struct { + hookCalled := make(chan struct { err error called bool - }, 1) // Buffer size of 1 prevents deadlock - defer close(hookResult) + }, 1) + defer close(hookCalled) app.Hooks().OnPostShutdown(func(err error) error { - hookResult <- struct { + hookCalled <- struct { err error called bool }{ @@ -225,42 +225,42 @@ func Test_Hook_OnPostShutdown(t *testing.T) { return nil }) - // Use channel to make sure the server is up - serverReady := make(chan struct{}) - defer close(serverReady) - + // Start server in goroutine go func() { defer wg.Done() - serverReady <- struct{}{} // Signal that the server is ready to start + // Ignore errors when shutting down the server if err := app.Listen(":0"); err != nil { - t.Errorf("Failed to start listener: %v", err) + if !errors.Is(err, errors.New("server closed")) { + t.Errorf("unexpected error: %v", err) + } } }() + // Wait a bit for the server to start + time.Sleep(100 * time.Millisecond) - <-serverReady // Wait for the server to be ready - - app.hooks.executeOnPostShutdownHooks(expectedErr) - - // Wait for the hook to finish executing and get the result - var result struct { - err error - called bool - } + // Trigger shutdown with our expected error + go app.hooks.executeOnPostShutdownHooks(expectedErr) + // Wait for hook to be called with timeout select { - case result = <-hookResult: - case <-time.After(5 * time.Second): - t.Fatal("Hook execution timeout") + case result := <-hookCalled: + if !result.called { + t.Error("hook was not called") + } + if !errors.Is(result.err, expectedErr) { + t.Errorf("hook received wrong error: want %v, got %v", expectedErr, result.err) + } + case <-time.After(3 * time.Second): + t.Fatal("hook execution timeout") } - if !result.called { - t.Fatal("hook was not called") + // Shutdown the server + if err := app.Shutdown(); err != nil { + t.Errorf("shutdown error: %v", err) } - if !errors.Is(result.err, expectedErr) { - t.Fatalf("hook received wrong error: want %v, got %v", expectedErr, result.err) - } - wg.Wait() // Ensure server goroutine completes + // Wait for server goroutine to complete + wg.Wait() }) t.Run("should execute multiple hooks in order", func(t *testing.T) { From 3954fb70e605a87008d90a88b3dbc0f26b5d16da Mon Sep 17 00:00:00 2001 From: JIeJaitt <498938874@qq.com> Date: Fri, 14 Feb 2025 10:40:16 +0800 Subject: [PATCH 33/39] =?UTF-8?q?=F0=9F=A9=B9=20Fix:=20Using=20channels=20?= =?UTF-8?q?to=20synchronize=20and=20pass=20results?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hooks_test.go | 60 +++++++++++---------------------------------------- 1 file changed, 12 insertions(+), 48 deletions(-) diff --git a/hooks_test.go b/hooks_test.go index 5c14b85f2ac..b95e07331fd 100644 --- a/hooks_test.go +++ b/hooks_test.go @@ -3,7 +3,6 @@ package fiber import ( "errors" "fmt" - "sync" "testing" "time" @@ -204,63 +203,33 @@ func Test_Hook_OnPostShutdown(t *testing.T) { t.Run("should execute post shutdown hook with error", func(t *testing.T) { app := New() expectedErr := errors.New("test shutdown error") - var wg sync.WaitGroup - wg.Add(1) - - // Use channels to synchronize and pass results - hookCalled := make(chan struct { - err error - called bool - }, 1) + + hookCalled := make(chan error, 1) defer close(hookCalled) app.Hooks().OnPostShutdown(func(err error) error { - hookCalled <- struct { - err error - called bool - }{ - err: err, - called: true, - } + hookCalled <- err return nil }) - // Start server in goroutine go func() { - defer wg.Done() - // Ignore errors when shutting down the server if err := app.Listen(":0"); err != nil { - if !errors.Is(err, errors.New("server closed")) { - t.Errorf("unexpected error: %v", err) - } + return } }() - // Wait a bit for the server to start + time.Sleep(100 * time.Millisecond) - // Trigger shutdown with our expected error - go app.hooks.executeOnPostShutdownHooks(expectedErr) + app.hooks.executeOnPostShutdownHooks(expectedErr) - // Wait for hook to be called with timeout select { - case result := <-hookCalled: - if !result.called { - t.Error("hook was not called") - } - if !errors.Is(result.err, expectedErr) { - t.Errorf("hook received wrong error: want %v, got %v", expectedErr, result.err) - } - case <-time.After(3 * time.Second): + case err := <-hookCalled: + require.Equal(t, expectedErr, err) + case <-time.After(time.Second): t.Fatal("hook execution timeout") } - // Shutdown the server - if err := app.Shutdown(); err != nil { - t.Errorf("shutdown error: %v", err) - } - - // Wait for server goroutine to complete - wg.Wait() + require.NoError(t, app.Shutdown()) }) t.Run("should execute multiple hooks in order", func(t *testing.T) { @@ -280,13 +249,8 @@ func Test_Hook_OnPostShutdown(t *testing.T) { app.hooks.executeOnPostShutdownHooks(nil) - if len(execution) != 2 { - t.Fatalf("expected 2 hooks to execute, got %d", len(execution)) - } - - if execution[0] != 1 || execution[1] != 2 { - t.Fatal("hooks executed in wrong order") - } + require.Len(t, execution, 2, "expected 2 hooks to execute") + require.Equal(t, []int{1, 2}, execution, "hooks executed in wrong order") }) t.Run("should handle hook error", func(_ *testing.T) { From d5fa4e3875c30dfe7a83e2005c2e041d791d6554 Mon Sep 17 00:00:00 2001 From: JIeJaitt <498938874@qq.com> Date: Fri, 14 Feb 2025 11:31:42 +0800 Subject: [PATCH 34/39] =?UTF-8?q?=F0=9F=A9=B9=20Fix:=20timeout=20with=20lo?= =?UTF-8?q?ng=20running=20request?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app_test.go | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/app_test.go b/app_test.go index bd5dd49b4c8..18f6cef98d3 100644 --- a/app_test.go +++ b/app_test.go @@ -1050,8 +1050,12 @@ func Test_App_ShutdownWithContext(t *testing.T) { app := New() requestStarted := make(chan struct{}) + requestProcessing := make(chan struct{}) + app.Get("/", func(c Ctx) error { close(requestStarted) + // Wait for signal to continue processing the request + <-requestProcessing time.Sleep(2 * time.Second) return c.SendString("OK") }) @@ -1063,14 +1067,10 @@ func Test_App_ShutdownWithContext(t *testing.T) { } }() - select { - case <-requestStarted: - // Request has started processing - case <-time.After(time.Second): - t.Fatal("Request did not start in time") - } + // Ensure server is fully started + time.Sleep(100 * time.Millisecond) - // Start long request + // Start a long-running request go func() { conn, err := ln.Dial() if err != nil { @@ -1082,8 +1082,16 @@ func Test_App_ShutdownWithContext(t *testing.T) { } }() - time.Sleep(100 * time.Millisecond) // Wait for request to start + // Wait for request to start + select { + case <-requestStarted: + // Request has started, signal to continue processing + close(requestProcessing) + case <-time.After(2 * time.Second): + t.Fatal("Request did not start in time") + } + // Attempt shutdown, should timeout ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) defer cancel() From d8434e074bde4421b97f8d8d72acc08af9004bad Mon Sep 17 00:00:00 2001 From: JIeJaitt <498938874@qq.com> Date: Mon, 17 Feb 2025 19:32:29 +0800 Subject: [PATCH 35/39] =?UTF-8?q?=F0=9F=93=9A=20Doc:=20remove=20OnShutdown?= =?UTF-8?q?Error=20and=20OnShutdownSuccess=20from=20fiber.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/api/fiber.md | 4 +--- listen.go | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/api/fiber.md b/docs/api/fiber.md index 22d3b8b395f..a79b2ba0c39 100644 --- a/docs/api/fiber.md +++ b/docs/api/fiber.md @@ -111,11 +111,9 @@ 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 to disable the timeout and wait indefinitely. | `10 * time.Second` | +| 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 `OnPostShutdown` 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` | -| OnShutdownSuccess | `func()` | Allows customizing success behavior when gracefully shutting down the server by given signal. | `nil` | | TLSConfigFunc | `func(tlsConfig *tls.Config)` | Allows customizing `tls.Config` as you want. | `nil` | | AutoCertManager | `*autocert.Manager` | Manages TLS certificates automatically using the ACME protocol. Enables integration with Let's Encrypt or other ACME-compatible providers. | `nil` | | TLSMinVersion | `uint16` | Allows customizing the TLS minimum version. | `tls.VersionTLS12` | diff --git a/listen.go b/listen.go index cdc01c6ff98..f33c9dafdae 100644 --- a/listen.go +++ b/listen.go @@ -91,7 +91,7 @@ type ListenConfig struct { 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. + // duration. If the timeout is reached, OnPostShutdown will be called with the error. // Set to 0 to disable the timeout and wait indefinitely. // // Default: 10 * time.Second From 3cb0c122ad834ee2806949049a51755e7179bd5b Mon Sep 17 00:00:00 2001 From: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Date: Mon, 17 Feb 2025 23:11:22 -0500 Subject: [PATCH 36/39] Update hooks.md --- docs/api/hooks.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/hooks.md b/docs/api/hooks.md index bd3f105a15c..4e4eed4e63b 100644 --- a/docs/api/hooks.md +++ b/docs/api/hooks.md @@ -179,7 +179,7 @@ func (h *Hooks) OnFork(handler ...OnForkHandler) ## OnShutdown -in v3, `OnShutdown` is split into `OnPreShutdown` and `OnPostShutdown`. +Since v3, the `OnShutdown` hook is split into `OnPreShutdown` and `OnPostShutdown`. `OnShutdown` is a hook to execute user functions after shutdown. From c2e23774733ada58502ba25f3a5cdcf03c3f163a Mon Sep 17 00:00:00 2001 From: JIeJaitt <498938874@qq.com> Date: Sat, 22 Feb 2025 23:53:20 +0800 Subject: [PATCH 37/39] =?UTF-8?q?=F0=9F=9A=A8=20Test:=20Add=20graceful=20s?= =?UTF-8?q?hutdown=20timeout=20error=20test=20case?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- listen_test.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/listen_test.go b/listen_test.go index 8c9720765a1..1a5bd77fa1a 100644 --- a/listen_test.go +++ b/listen_test.go @@ -45,6 +45,10 @@ func Test_Listen_Graceful_Shutdown(t *testing.T) { t.Run("Shutdown With Timeout", func(t *testing.T) { testGracefulShutdown(t, 500*time.Millisecond) }) + + t.Run("Shutdown With Timeout Error", func(t *testing.T) { + testGracefulShutdown(t, 1*time.Nanosecond) + }) } func testGracefulShutdown(t *testing.T, shutdownTimeout time.Duration) { @@ -52,19 +56,22 @@ func testGracefulShutdown(t *testing.T, shutdownTimeout time.Duration) { var mu sync.Mutex var shutdown bool + var receivedErr error app := New() app.Get("/", func(c Ctx) error { + time.Sleep(10 * time.Millisecond) return c.SendString(c.Hostname()) }) ln := fasthttputil.NewInmemoryListener() errs := make(chan error, 1) - app.hooks.OnPostShutdown(func(_ error) error { + app.hooks.OnPostShutdown(func(err error) error { mu.Lock() defer mu.Unlock() shutdown = true + receivedErr = err return nil }) @@ -147,6 +154,10 @@ func testGracefulShutdown(t *testing.T, shutdownTimeout time.Duration) { mu.Lock() require.True(t, shutdown) + if shutdownTimeout == 1*time.Nanosecond { + require.Error(t, receivedErr) + require.ErrorIs(t, receivedErr, context.DeadlineExceeded) + } require.NoError(t, <-errs) mu.Unlock() } From ce9ecc78f05bd3bea803d0326e4da05f88deb592 Mon Sep 17 00:00:00 2001 From: JIeJaitt <498938874@qq.com> Date: Sat, 22 Feb 2025 23:55:58 +0800 Subject: [PATCH 38/39] =?UTF-8?q?=F0=9F=93=9D=20Doc:=20Restructure=20hooks?= =?UTF-8?q?=20documentation=20for=20OnPreShutdown=20and=20OnPostShutdown?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/api/hooks.md | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/docs/api/hooks.md b/docs/api/hooks.md index 4e4eed4e63b..b71589bd619 100644 --- a/docs/api/hooks.md +++ b/docs/api/hooks.md @@ -15,9 +15,8 @@ With Fiber you can execute custom user functions at specific method execution po - [OnGroupName](#ongroupname) - [OnListen](#onlisten) - [OnFork](#onfork) -- [OnShutdown](#onshutdown) - - [OnPreShutdown](#onpreshutdown) - - [OnPostShutdown](#onpostshutdown) +- [OnPreShutdown](#onpreshutdown) +- [OnPostShutdown](#onpostshutdown) - [OnMount](#onmount) ## Constants @@ -177,17 +176,9 @@ func main() { func (h *Hooks) OnFork(handler ...OnForkHandler) ``` -## OnShutdown -Since v3, the `OnShutdown` hook is split into `OnPreShutdown` and `OnPostShutdown`. -`OnShutdown` is a hook to execute user functions after shutdown. - -```go title="Signature" -func (h *Hooks) OnShutdown(handler ...OnShutdownHandler) -``` - -### OnPreShutdown +## OnPreShutdown `OnPreShutdown` is a hook to execute user functions before shutdown. @@ -195,7 +186,7 @@ func (h *Hooks) OnShutdown(handler ...OnShutdownHandler) func (h *Hooks) OnPreShutdown(handler ...OnPreShutdownHandler) ``` -### OnPostShutdown +## OnPostShutdown `OnPostShutdown` is a hook to execute user functions after shutdown. From 8fde47fd94e85d8237884f97615a21b5dc35f84b Mon Sep 17 00:00:00 2001 From: JIeJaitt <498938874@qq.com> Date: Sun, 23 Feb 2025 00:10:44 +0800 Subject: [PATCH 39/39] =?UTF-8?q?=F0=9F=93=9D=20Doc:=20Remove=20extra=20wh?= =?UTF-8?q?itespace=20in=20hooks=20documentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/api/hooks.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/api/hooks.md b/docs/api/hooks.md index b71589bd619..a6e6c1ace1f 100644 --- a/docs/api/hooks.md +++ b/docs/api/hooks.md @@ -176,8 +176,6 @@ func main() { func (h *Hooks) OnFork(handler ...OnForkHandler) ``` - - ## OnPreShutdown `OnPreShutdown` is a hook to execute user functions before shutdown.