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

🩹 Fix: goroutine leakage #3306

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion app.go
Original file line number Diff line number Diff line change
Expand Up @@ -994,7 +994,7 @@ func (app *App) Test(req *http.Request, config ...TestConfig) (*http.Response, e
app.startupProcess()

// Serve conn to server
channel := make(chan error)
channel := make(chan error, 1)
go func() {
var returned bool
defer func() {
Expand Down
85 changes: 85 additions & 0 deletions app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,91 @@ func testErrorResponse(t *testing.T, err error, resp *http.Response, expectedBod
require.Equal(t, expectedBodyError, string(body), "Response body")
}

func Test_App_Test_Goroutine_Leak_Compare(t *testing.T) {
t.Parallel()

testCases := []struct {
handler Handler
name string
timeout time.Duration
sleepTime time.Duration
expectLeak bool
}{
{
name: "With timeout (potential leak)",
handler: func(c Ctx) error {
time.Sleep(300 * time.Millisecond) // Simulate time-consuming operation
return c.SendString("ok")
},
timeout: 50 * time.Millisecond, // // Short timeout to ensure triggering
sleepTime: 500 * time.Millisecond, // Wait time longer than handler execution time
expectLeak: true,
},
{
name: "Without timeout (no leak)",
handler: func(c Ctx) error {
return c.SendString("ok") // Return immediately
},
timeout: 0, // Disable timeout
sleepTime: 100 * time.Millisecond,
expectLeak: false,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
app := New()

// Record initial goroutine count
initialGoroutines := runtime.NumGoroutine()
t.Logf("[%s] Initial goroutines: %d", tc.name, initialGoroutines)

app.Get("/", tc.handler)

// Send 10 requests
numRequests := 10
for i := 0; i < numRequests; i++ {
req := httptest.NewRequest(MethodGet, "/", nil)

if tc.timeout > 0 {
_, err := app.Test(req, TestConfig{
Timeout: tc.timeout,
FailOnTimeout: true,
})
require.Error(t, err)
require.ErrorIs(t, err, os.ErrDeadlineExceeded)
} else if resp, err := app.Test(req); err != nil {
t.Errorf("unexpected error: %v", err)
} else {
require.Equal(t, 200, resp.StatusCode)
}
}

// Wait for normal goroutines to complete
time.Sleep(tc.sleepTime)

// Check final goroutine count
finalGoroutines := runtime.NumGoroutine()
leakedGoroutines := finalGoroutines - initialGoroutines
t.Logf("[%s] Final goroutines: %d (leaked: %d)",
tc.name, finalGoroutines, leakedGoroutines)

if tc.expectLeak {
// before fix: If blocking exists, leaked goroutines should be at least equal to request count
// after fix: If no blocking exists, leaked goroutines should be less than request count
if leakedGoroutines >= numRequests {
t.Errorf("[%s] Expected at least %d leaked goroutines, but got %d",
tc.name, numRequests, leakedGoroutines)
}
} else if leakedGoroutines >= numRequests { // If no blocking exists, leaked goroutines should be less than request count
Comment on lines +130 to +137
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

Reevaluate the leak checking logic.
The condition and error message seem inverted. If tc.expectLeak == true, requiring “at least N leaked goroutines,” you might want to flag an error when leakedGoroutines < numRequests instead of >= numRequests. Currently, it flags an error if the condition is satisfied, which contradicts the comment.

Suggest correcting as follows (example flip of condition):

-if leakedGoroutines >= numRequests {
-    t.Errorf("[%s] Expected at least %d leaked goroutines, but got %d", ...)
+if leakedGoroutines < numRequests {
+    t.Errorf("[%s] Expected at least %d leaked goroutines, but got %d", ...)
📝 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
if tc.expectLeak {
// before fix: If blocking exists, leaked goroutines should be at least equal to request count
// after fix: If no blocking exists, leaked goroutines should be less than request count
if leakedGoroutines >= numRequests {
t.Errorf("[%s] Expected at least %d leaked goroutines, but got %d",
tc.name, numRequests, leakedGoroutines)
}
} else if leakedGoroutines >= numRequests { // If no blocking exists, leaked goroutines should be less than request count
if tc.expectLeak {
// before fix: If blocking exists, leaked goroutines should be at least equal to request count
// after fix: If no blocking exists, leaked goroutines should be less than request count
if leakedGoroutines < numRequests {
t.Errorf("[%s] Expected at least %d leaked goroutines, but got %d",
tc.name, numRequests, leakedGoroutines)
}
} else if leakedGoroutines >= numRequests { // If no blocking exists, leaked goroutines should be less than request count

t.Errorf("[%s] Expected less than %d leaked goroutines, but got %d",
tc.name, numRequests, leakedGoroutines)
}
})
}
}

func Test_App_MethodNotAllowed(t *testing.T) {
t.Parallel()
app := New()
Expand Down
Loading