Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🔥 feat: Add support for graceful shutdown timeout in ListenConfig #3220

Merged
1 change: 1 addition & 0 deletions docs/api/fiber.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ app.Listen(":8080", fiber.ListenConfig{
| <Reference id="enableprefork">EnablePrefork</Reference> | `bool` | When set to true, this will spawn multiple Go processes listening on the same port. | `false` |
| <Reference id="enableprintroutes">EnablePrintRoutes</Reference> | `bool` | If set to true, will print all routes with their method, path, and handler. | `false` |
| <Reference id="gracefulcontext">GracefulContext</Reference> | `context.Context` | Field to shutdown Fiber by given context gracefully. | `nil` |
| <Reference id="ShutdownTimeout">ShutdownTimeout</Reference> | `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` |
| <Reference id="listeneraddrfunc">ListenerAddrFunc</Reference> | `func(addr net.Addr)` | Allows accessing and customizing `net.Listener`. | `nil` |
| <Reference id="listenernetwork">ListenerNetwork</Reference> | `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` |
| <Reference id="onshutdownerror">OnShutdownError</Reference> | `func(err error)` | Allows to customize error behavior when gracefully shutting down the server by given signal. Prints error with `log.Fatalf()` | `nil` |
Expand Down
26 changes: 21 additions & 5 deletions listen.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"strconv"
"strings"
"text/tabwriter"
"time"

"github.com/gofiber/fiber/v3/log"
"github.com/mattn/go-colorable"
Expand All @@ -37,8 +38,6 @@
)

// ListenConfig is a struct to customize startup of Fiber.
//
// TODO: Add timeout for graceful shutdown.
type ListenConfig struct {
// GracefulContext is a field to shutdown Fiber by given context gracefully.
//
Expand Down Expand Up @@ -94,6 +93,13 @@
// Default : ""
CertClientFile string `json:"cert_client_file"`

// When the graceful shutdown begins, use this field to set the timeout
// duration. If the timeout is reached, OnShutdownError will be called.
// Set to 0 to disable the timeout and wait indefinitely.
//
// Default: 10 * time.Second
ShutdownTimeout time.Duration `json:"shutdown_timeout"`

// When set to true, it will not print out the «Fiber» ASCII art and listening address.
//
// Default: false
Expand All @@ -116,8 +122,9 @@
return ListenConfig{
ListenerNetwork: NetworkTCP4,
OnShutdownError: func(err error) {
log.Fatalf("shutdown: %v", err) //nolint:revive // It's an optipn
log.Fatalf("shutdown: %v", err) //nolint:revive // It's an option

Check warning on line 125 in listen.go

View check run for this annotation

Codecov / codecov/patch

listen.go#L125

Added line #L125 was not covered by tests
},
ShutdownTimeout: 10 * time.Second,
}
}

Expand All @@ -128,7 +135,7 @@

if cfg.OnShutdownError == nil {
cfg.OnShutdownError = func(err error) {
log.Fatalf("shutdown: %v", err) //nolint:revive // It's an optipn
log.Fatalf("shutdown: %v", err) //nolint:revive // It's an option

Check warning on line 138 in listen.go

View check run for this annotation

Codecov / codecov/patch

listen.go#L138

Added line #L138 was not covered by tests
}
}

Expand Down Expand Up @@ -472,8 +479,17 @@
func (app *App) gracefulShutdown(ctx context.Context, cfg ListenConfig) {
<-ctx.Done()

if err := app.Shutdown(); err != nil { //nolint:contextcheck // TODO: Implement it
var err error

if cfg.ShutdownTimeout != 0 {
err = app.ShutdownWithTimeout(cfg.ShutdownTimeout) //nolint:contextcheck // TODO: Implement it
} else {
err = app.Shutdown() //nolint:contextcheck // TODO: Implement it
Comment on lines +485 to +487
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Address the TODO comments and implement context handling in shutdown methods

The // TODO: Implement it comments indicate that app.ShutdownWithTimeout and app.Shutdown may not fully handle the context as intended. Ensure that these methods are properly implemented to facilitate graceful shutdown within the specified timeout.

Additionally, after implementation, remove the //nolint:contextcheck directives to comply with linting standards.

Would you like assistance in implementing the context handling for ShutdownWithTimeout and Shutdown methods?

}

if err != nil {
cfg.OnShutdownError(err)
return
}

if success := cfg.OnShutdownSuccess; success != nil {
Expand Down
114 changes: 113 additions & 1 deletion listen_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ func Test_Listen_Graceful_Shutdown(t *testing.T) {
ExpectedStatusCode int
}{
{Time: 500 * time.Millisecond, ExpectedBody: "example.com", ExpectedStatusCode: StatusOK, ExpectedErr: nil},
{Time: 3 * time.Second, ExpectedBody: "", ExpectedStatusCode: StatusOK, ExpectedErr: errors.New("InmemoryListener is already closed: use of closed network connection")},
{Time: 3 * time.Second, ExpectedBody: "", ExpectedStatusCode: StatusOK, ExpectedErr: fasthttputil.ErrInmemoryListenerClosed},
}

for _, tc := range testCases {
Expand Down Expand Up @@ -115,6 +115,118 @@ func Test_Listen_Graceful_Shutdown(t *testing.T) {
mu.Unlock()
}

// go test -run Test_Listen_Graceful_Shutdown_Timeout
func Test_Listen_Graceful_Shutdown_Timeout(t *testing.T) {
var mu sync.Mutex
var shutdownSuccess bool
var shutdownTimeoutError error

app := New()

app.Get("/", func(c Ctx) error {
return c.SendString(c.Hostname())
})

ln := fasthttputil.NewInmemoryListener()
errs := make(chan error)

go func() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()

errs <- app.Listener(ln, ListenConfig{
DisableStartupMessage: true,
GracefulContext: ctx,
ShutdownTimeout: 500 * time.Millisecond,
OnShutdownSuccess: func() {
mu.Lock()
shutdownSuccess = true
mu.Unlock()
},
OnShutdownError: func(err error) {
mu.Lock()
shutdownTimeoutError = err
mu.Unlock()
},
})
}()

// Server readiness check
for i := 0; i < 10; i++ {
conn, err := ln.Dial()
// To test a graceful shutdown timeout, do not close the connection.
if err == nil {
_ = conn
break
}
// Wait a bit before retrying
time.Sleep(100 * time.Millisecond)
if i == 9 {
t.Fatalf("Server did not become ready in time: %v", err)
}
}

testCases := []struct {
ExpectedErr error
ExpectedShutdownError error
ExpectedBody string
Time time.Duration
ExpectedStatusCode int
ExpectedShutdownSuccess bool
}{
{
Time: 100 * time.Millisecond,
ExpectedBody: "example.com",
ExpectedStatusCode: StatusOK,
ExpectedErr: nil,
ExpectedShutdownError: nil,
ExpectedShutdownSuccess: false,
},
{
Time: 3 * time.Second,
ExpectedBody: "",
ExpectedStatusCode: StatusOK,
ExpectedErr: fasthttputil.ErrInmemoryListenerClosed,
ExpectedShutdownError: context.DeadlineExceeded,
ExpectedShutdownSuccess: false,
},
}

for _, tc := range testCases {
time.Sleep(tc.Time)

req := fasthttp.AcquireRequest()
req.SetRequestURI("http://example.com")

client := fasthttp.HostClient{}
client.Dial = func(_ string) (net.Conn, error) { return ln.Dial() }

resp := fasthttp.AcquireResponse()
err := client.Do(req, resp)

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)
}

mu.Lock()
err := <-errs
require.NoError(t, err)
mu.Unlock()
}

// go test -run Test_Listen_Prefork
func Test_Listen_Prefork(t *testing.T) {
testPreforkMaster = true
Expand Down
Loading