MCPcopy Create free account
hub / github.com/php/frankenphp / TestRestartWorkersForceKillsStuckThread

Function TestRestartWorkersForceKillsStuckThread

worker_internal_test.go:18–78  ·  view source on GitHub ↗

TestRestartWorkersForceKillsStuckThread verifies the drain path does not hang when a worker is stuck in a blocking PHP call (sleep, etc.). macOS has no realtime signals so we can't unblock sleep() there; skip.

(t *testing.T)

Source from the content-addressed store, hash-verified

16// not hang when a worker is stuck in a blocking PHP call (sleep, etc.).
17// macOS has no realtime signals so we can't unblock sleep() there; skip.
18func TestRestartWorkersForceKillsStuckThread(t *testing.T) {
19 if runtime.GOOS != "linux" && runtime.GOOS != "freebsd" && runtime.GOOS != "windows" {
20 t.Skipf("force-kill cannot interrupt blocking syscalls on %s", runtime.GOOS)
21 }
22
23 prev := drainGracePeriod
24 drainGracePeriod = 500 * time.Millisecond
25 t.Cleanup(func() { drainGracePeriod = prev })
26
27 cwd, _ := os.Getwd()
28 testDataDir := cwd + "/testdata/"
29
30 require.NoError(t, Init(
31 WithWorkers("sleep-worker", testDataDir+"worker-sleep.php", 1),
32 WithNumThreads(2),
33 ))
34 t.Cleanup(Shutdown)
35
36 // Marker file the worker touches right before sleep(); per-run path
37 // so a stale file from a prior test can't fool the poll below.
38 markerFile := filepath.Join(t.TempDir(), "sleep-worker-in-sleep")
39
40 // Worker handles the request, then sleep(60). Recorder lets us
41 // assert post-sleep code never runs (would indicate the VM interrupt
42 // didn't fire and only drainChan got picked up).
43 recorder := httptest.NewRecorder()
44 served := make(chan struct{})
45 go func() {
46 defer close(served)
47 req := httptest.NewRequest("GET", "http://example.com/worker-sleep.php", nil)
48 req.Header.Set("Sleep-Marker", markerFile)
49 fr, err := NewRequestWithContext(req, WithRequestDocumentRoot(testDataDir, false))
50 if err != nil {
51 return
52 }
53 _ = ServeHTTP(recorder, fr)
54 }()
55
56 // Confirm the worker is parked in sleep() before triggering the
57 // restart, so we exercise the force-kill path and not drainChan.
58 require.Eventually(t, func() bool {
59 _, err := os.Stat(markerFile)
60 return err == nil
61 }, 5*time.Second, 10*time.Millisecond, "worker never entered sleep()")
62
63 start := time.Now()
64 RestartWorkers()
65 elapsed := time.Since(start)
66
67 // Test grace period (500ms) + 3s slack for signal dispatch, VM tick, restart loop.
68 const budget = 4 * time.Second
69 assert.Less(t, elapsed, budget, "drain must force-kill the stuck thread within the grace period")
70
71 select {
72 case <-served:
73 case <-time.After(2 * time.Second):
74 t.Fatal("server request goroutine did not complete after drain")
75 }

Callers

nothing calls this directly

Calls 9

InitFunction · 0.85
WithWorkersFunction · 0.85
WithNumThreadsFunction · 0.85
NewRequestWithContextFunction · 0.85
WithRequestDocumentRootFunction · 0.85
ServeHTTPFunction · 0.85
RestartWorkersFunction · 0.85
SetMethod · 0.80
StringMethod · 0.45

Tested by

no test coverage detected