From 476e7d4fb5e775a313c6b5e3f955619d7f062045 Mon Sep 17 00:00:00 2001 From: JIeJaitt <498938874@qq.com> Date: Wed, 12 Feb 2025 15:28:06 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=A9=B9=20Fix:=20goroutine=20leakage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.go | 2 +- app_test.go | 85 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/app.go b/app.go index dca7efed00..c867cc4e95 100644 --- a/app.go +++ b/app.go @@ -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() { diff --git a/app_test.go b/app_test.go index 1b2b7a40d9..d64f9a6813 100644 --- a/app_test.go +++ b/app_test.go @@ -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 + 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()