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: Improve and Optimize ShutdownWithContext Func #3162

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
137da86
feat: Optimize ShutdownWithContext method in app.go
Oct 10, 2024
b41d084
feat: Enhance ShutdownWithContext test for improved reliability
Oct 10, 2024
e1ad8e9
Merge branch 'gofiber:main' into jiejaitt-feature/improve-shutdown-wi…
JIeJaitt Oct 12, 2024
a683166
📚 Doc: update the docs to explain shutdown & hook execution order
JIeJaitt Oct 12, 2024
a4a5831
Merge branch 'main' into jiejaitt-feature/improve-shutdown-with-context
JIeJaitt Oct 12, 2024
424ecbb
Merge branch 'main' of github.com:JIeJaitt/fiber into jiejaitt-featur…
JIeJaitt Oct 12, 2024
98fbdff
Merge branch 'jiejaitt-feature/improve-shutdown-with-context' of gith…
JIeJaitt Oct 12, 2024
796922f
🩹 Fix: Possible Data Race on shutdownHookCalled Variable
JIeJaitt Oct 12, 2024
e465b5b
🩹 Fix: Remove the default Case
JIeJaitt Oct 12, 2024
ee866ec
🩹 Fix: Import sync/atomic
JIeJaitt Oct 12, 2024
e0a56be
🩹 Fix: golangci-lint problem
JIeJaitt Oct 13, 2024
d01a09e
Merge branches 'jiejaitt-feature/improve-shutdown-with-context' and '…
JIeJaitt Oct 13, 2024
750a7fa
🎨 Style: add block in api.md
JIeJaitt Oct 28, 2024
2ea0bb3
Merge branch 'main' of github.com:JIeJaitt/fiber into jiejaitt-featur…
JIeJaitt Oct 28, 2024
b9509fe
🩹 Fix: go mod tidy
JIeJaitt Oct 28, 2024
c2792e7
feat: Optimize ShutdownWithContext method in app.go
Oct 10, 2024
18111e5
feat: Enhance ShutdownWithContext test for improved reliability
Oct 10, 2024
83ea43d
📚 Doc: update the docs to explain shutdown & hook execution order
JIeJaitt Oct 12, 2024
66dcb42
🩹 Fix: Possible Data Race on shutdownHookCalled Variable
JIeJaitt Oct 12, 2024
f3902c5
🩹 Fix: Remove the default Case
JIeJaitt Oct 12, 2024
da193ac
🩹 Fix: Import sync/atomic
JIeJaitt Oct 12, 2024
0a92125
🩹 Fix: golangci-lint problem
JIeJaitt Oct 13, 2024
b0bc70c
🎨 Style: add block in api.md
JIeJaitt Oct 28, 2024
44cbc62
🩹 Fix: go mod tidy
JIeJaitt Oct 28, 2024
0e99032
Merge branch 'jiejaitt-feature/improve-shutdown-with-context' of gith…
JIeJaitt Oct 28, 2024
a32bddd
Merge branch 'main' of github.com:gofiber/fiber into jiejaitt-feature…
JIeJaitt Oct 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Copy link
Member

Choose a reason for hiding this comment

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

It would be nice to update the docs to explain shutdown & hook execution order

Copy link
Author

Choose a reason for hiding this comment

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

It would be nice to update the docs to explain shutdown & hook execution order

done

}

return app.server.ShutdownWithContext(ctx)
}

Expand Down
72 changes: 44 additions & 28 deletions app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -860,45 +860,61 @@
t.Parallel()

app := New()
shutdownHookCalled := false
app.Hooks().OnShutdown(func() error {
shutdownHookCalled = true
return nil
})
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

Possible Data Race on shutdownHookCalled Variable

The shutdownHookCalled boolean is accessed by multiple goroutines without proper synchronization, which could lead to a data race. In Go, variables shared between goroutines should be protected using synchronization mechanisms like channels, mutexes, or atomic operations.

Suggested Fix: Use an Atomic Variable

Replace shutdownHookCalled with an int32 and use atomic.StoreInt32 and atomic.LoadInt32 for thread-safe operations.

+import "sync/atomic"

-	shutdownHookCalled := false
+	var shutdownHookCalled int32

	app.Hooks().OnShutdown(func() error {
-		shutdownHookCalled = true
+		atomic.StoreInt32(&shutdownHookCalled, 1)
		return nil
	})

	// ...

-	assert.True(t, shutdownHookCalled, "Shutdown hook was not called")
+	assert.Equal(t, int32(1), atomic.LoadInt32(&shutdownHookCalled), "Shutdown hook was not called")

Committable suggestion was skipped due to low confidence.


app.Get("/", func(ctx Ctx) error {
time.Sleep(5 * time.Second)
return ctx.SendString("body")
})

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)
Copy link
Member

Choose a reason for hiding this comment

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

why do we need a sleep here? i think it's arbitrary since we already wait for clientDone channel

Copy link
Author

Choose a reason for hiding this comment

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

why do we need a sleep here? i think it's arbitrary since we already wait for clientDone channel

This sleep is to ensure that our server has started processing the request. Although we have confirmed that the client has sent the request through <-clientDone, this means that the request has been sent to the server's listening port, and there is no guarantee that the server's processing coroutine has been sent. Start processing the request, so I think sleep 100 * Millisecond is required here as well.


shutdownErr := make(chan error, 1)
go func() {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
shutdownErr <- app.ShutdownWithContext(ctx)
}()
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

Ensure Proper Context Cancellation

The context created with context.WithTimeout should have its cancellation function deferred immediately to prevent potential resource leaks, even if an error occurs before the defer statement is reached.

Suggested Improvement

Move the defer cancel() directly after context creation.

	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
+	defer cancel()
	shutdownErr <- app.ShutdownWithContext(ctx)
-	defer cancel()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
shutdownErr <- app.ShutdownWithContext(ctx)
}()
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")

Check failure on line 906 in app_test.go

View workflow job for this annotation

GitHub Actions / lint

require-error: for error assertions use require (testifylint)
assert.True(t, errors.Is(err, context.DeadlineExceeded), "Expected DeadlineExceeded error")

Check failure on line 907 in app_test.go

View workflow job for this annotation

GitHub Actions / lint

error-is-as: use assert.ErrorIs (testifylint)
}

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
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Avoid Skipping Server Error Handling

Using a default case in the select statement may cause the test to proceed without waiting for the server to shut down, potentially leading to flaky tests. It's important to handle the server error explicitly.

Suggested Fix: Remove the default Case

This ensures the test waits for serverErr to receive an error or completes the shutdown process.

	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
	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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
}
case err := <-serverErr:
assert.NoError(t, err, "Server should have shut down without error")

}

// go test -run Test_App_Mixed_Routes_WithSameLen
Expand Down
2 changes: 1 addition & 1 deletion docs/api/fiber.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Member

Choose a reason for hiding this comment

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

Missing space before "Shutdown

Copy link
Author

Choose a reason for hiding this comment

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

Thank you, I have solved this problem


```go
func (app *App) Shutdown() error
Expand Down
Loading