-
Notifications
You must be signed in to change notification settings - Fork 480
Expand file tree
/
Copy pathgoroutineleak_test.go
More file actions
61 lines (49 loc) 路 2.07 KB
/
Copy pathgoroutineleak_test.go
File metadata and controls
61 lines (49 loc) 路 2.07 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package frankenphp
import (
"bytes"
"net/http/httptest"
"runtime/pprof"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// leakedGoroutines runs a leak-detection GC cycle and returns how many goroutines
// it found blocked forever on a channel or mutex that no running goroutine can
// still reach, along with their stacks.
//
// Profile.Count only reports the result of the previous detection cycle, so the
// profile has to be written first even when only the count is wanted.
func leakedGoroutines(t *testing.T) (int, string) {
t.Helper()
profile := pprof.Lookup("goroutineleak")
require.NotNil(t, profile, "the goroutineleak profile is only available since Go 1.27")
var stacks bytes.Buffer
require.NoError(t, profile.WriteTo(&stacks, 1))
return profile.Count(), stacks.String()
}
// TestNoGoroutinesAreLeakedByAFullServerLifecycle boots PHP threads, serves requests
// through a worker and a regular thread, then shuts everything down.
//
// Shutdown has to unblock every goroutine it started, so a goroutine still parked on
// an unreachable channel afterwards means a thread, a scaling ticker or a watcher
// outlived Shutdown with nothing left to wake it. The count is compared against a
// baseline rather than against zero: tests share a process, so earlier tests may have
// left leaks of their own behind.
func TestNoGoroutinesAreLeakedByAFullServerLifecycle(t *testing.T) {
before, _ := leakedGoroutines(t)
require.NoError(t, Init(
WithNumThreads(2),
WithMaxThreads(4),
WithWorkers("worker", testDataPath+"/index.php", 1, WithWorkerMaxFailures(0)),
))
for range 5 {
r := httptest.NewRequest("GET", "http://localhost/index.php", nil)
req, err := NewRequestWithContext(r, WithRequestDocumentRoot(testDataPath, false))
require.NoError(t, err)
require.NoError(t, ServeHTTP(httptest.NewRecorder(), req))
}
Shutdown()
after, stacks := leakedGoroutines(t)
t.Logf("leaked goroutines: %d before, %d after", before, after)
assert.LessOrEqual(t, after, before, "goroutines leaked across an Init/Shutdown cycle:\n%s", stacks)
}