Skip to content

Commit cf16368

Browse files
Review by AlliBalliBaba
1 parent f7e5886 commit cf16368

File tree

11 files changed

+157
-318
lines changed

11 files changed

+157
-318
lines changed

docs/config.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,6 @@ php_server [<matcher>] {
191191
watch <path> # Sets the path to watch for file changes. Can be specified more than once for multiple paths.
192192
env <key> <value> # Sets an extra environment variable to the given value. Can be specified more than once for multiple environment variables. Environment variables for this worker are also inherited from the php_server parent, but can be overwritten here.
193193
match <path> # match the worker to a path pattern. Overrides try_files and can only be used in the php_server directive.
194-
max_requests <num> # Sets the maximum number of requests a worker thread will handle before restarting, useful for mitigating memory leaks. Default: 0 (unlimited).
195194
}
196195
worker <other_file> <num> # Can also use the short form like in the global frankenphp block.
197196
}
@@ -285,7 +284,7 @@ The `max_requests` setting in the global `frankenphp` block applies to all PHP t
285284
```
286285

287286
When a thread reaches the limit, the underlying C thread is fully restarted,
288-
cleaning up all ZTS thread-local storage, including any memory leaked by PHP extensions.
287+
cleaning up all memory and state, including any memory leaked by PHP extensions.
289288
Other threads continue to serve requests during the restart, so there is no downtime.
290289

291290
Set to `0` (default) to disable the limit and let threads run indefinitely.

frankenphp.go

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -371,13 +371,6 @@ func Shutdown() {
371371

372372
drainWatchers()
373373
drainAutoScaling()
374-
375-
// signal restart goroutines to stop spawning and wait for in-flight ones
376-
shutdownInProgress.Store(true)
377-
for restartingThreads.Load() > 0 {
378-
runtime.Gosched()
379-
}
380-
381374
drainPHPThreads()
382375

383376
metrics.Shutdown()
@@ -796,6 +789,5 @@ func resetGlobals() {
796789
watcherIsEnabled = false
797790
maxIdleTime = defaultMaxIdleTime
798791
maxRequestsPerThread = 0
799-
shutdownInProgress.Store(false)
800792
globalMu.Unlock()
801793
}

frankenphp_test.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ import (
2323
"os"
2424
"os/exec"
2525
"os/user"
26-
"runtime"
2726
"path/filepath"
27+
"runtime"
2828
"strconv"
2929
"strings"
3030
"sync"
@@ -46,7 +46,6 @@ type testOptions struct {
4646
realServer bool
4747
logger *slog.Logger
4848
initOpts []frankenphp.Option
49-
workerOpts []frankenphp.WorkerOption
5049
requestOpts []frankenphp.RequestOption
5150
phpIni map[string]string
5251
}
@@ -68,7 +67,6 @@ func runTest(t *testing.T, test func(func(http.ResponseWriter, *http.Request), *
6867
frankenphp.WithWorkerEnv(opts.env),
6968
frankenphp.WithWorkerWatchMode(opts.watch),
7069
}
71-
workerOpts = append(workerOpts, opts.workerOpts...)
7270
initOpts = append(initOpts, frankenphp.WithWorkers("workerName", testDataDir+opts.workerScript, opts.nbWorkers, workerOpts...))
7371
}
7472
initOpts = append(initOpts, opts.initOpts...)

internal/state/state.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ const (
3030
TransitionRequested
3131
TransitionInProgress
3232
TransitionComplete
33+
34+
// thread is exiting the C loop for a full ZTS restart (max_requests)
35+
Rebooting
36+
// C thread has exited and ZTS state is cleaned up, ready to spawn a new C thread
37+
RebootReady
3338
)
3439

3540
func (s State) String() string {
@@ -58,6 +63,10 @@ func (s State) String() string {
5863
return "transition in progress"
5964
case TransitionComplete:
6065
return "transition complete"
66+
case Rebooting:
67+
return "rebooting"
68+
case RebootReady:
69+
return "reboot ready"
6170
default:
6271
return "unknown"
6372
}

maxrequests_regular_test.go

Lines changed: 12 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,15 @@
11
package frankenphp_test
22

33
import (
4-
"io"
54
"log/slog"
65
"net/http"
76
"net/http/httptest"
87
"strings"
98
"sync"
109
"testing"
11-
"time"
1210

1311
"github.com/dunglas/frankenphp"
1412
"github.com/stretchr/testify/assert"
15-
"github.com/stretchr/testify/require"
1613
)
1714

1815
// TestModuleMaxRequests verifies that regular (non-worker) PHP threads restart
@@ -24,30 +21,19 @@ func TestModuleMaxRequests(t *testing.T) {
2421
var buf syncBuffer
2522
logger := slog.New(slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug}))
2623

27-
runTest(t, func(_ func(http.ResponseWriter, *http.Request), ts *httptest.Server, _ int) {
28-
require.NotNil(t, ts)
29-
client := &http.Client{Timeout: 5 * time.Second}
30-
24+
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, _ int) {
3125
for i := 0; i < totalRequests; i++ {
32-
resp, err := client.Get(ts.URL + "/index.php")
33-
require.NoError(t, err, "request %d should succeed", i)
34-
35-
body, err := io.ReadAll(resp.Body)
36-
require.NoError(t, err)
37-
_ = resp.Body.Close()
38-
39-
assert.Equal(t, 200, resp.StatusCode, "request %d should return 200, got body: %s", i, string(body))
40-
assert.Contains(t, string(body), "I am by birth a Genevese",
41-
"request %d should return correct body", i)
26+
body, resp := testGet("http://example.com/index.php", handler, t)
27+
assert.Equal(t, 200, resp.StatusCode)
28+
assert.Contains(t, body, "I am by birth a Genevese")
4229
}
4330

4431
restartCount := strings.Count(buf.String(), "max requests reached, restarting thread")
4532
t.Logf("Thread restarts observed: %d", restartCount)
4633
assert.GreaterOrEqual(t, restartCount, 2,
4734
"with maxRequests=%d and %d requests on 2 threads, at least 2 restarts should occur", maxRequests, totalRequests)
4835
}, &testOptions{
49-
realServer: true,
50-
logger: logger,
36+
logger: logger,
5137
initOpts: []frankenphp.Option{
5238
frankenphp.WithNumThreads(2),
5339
frankenphp.WithMaxRequests(maxRequests),
@@ -60,69 +46,24 @@ func TestModuleMaxRequests(t *testing.T) {
6046
func TestModuleMaxRequestsConcurrent(t *testing.T) {
6147
const maxRequests = 10
6248
const totalRequests = 200
63-
const concurrency = 20
6449

65-
runTest(t, func(_ func(http.ResponseWriter, *http.Request), ts *httptest.Server, _ int) {
66-
require.NotNil(t, ts)
67-
client := &http.Client{Timeout: 10 * time.Second}
68-
69-
var successCount int
70-
var mu sync.Mutex
71-
sem := make(chan struct{}, concurrency)
50+
runTest(t, func(handler func(http.ResponseWriter, *http.Request), _ *httptest.Server, _ int) {
7251
var wg sync.WaitGroup
7352

7453
for i := 0; i < totalRequests; i++ {
7554
wg.Add(1)
76-
sem <- struct{}{}
77-
go func(i int) {
78-
defer func() { <-sem; wg.Done() }()
79-
80-
resp, err := client.Get(ts.URL + "/index.php")
81-
if err != nil {
82-
return
83-
}
84-
body, _ := io.ReadAll(resp.Body)
85-
_ = resp.Body.Close()
86-
87-
if resp.StatusCode == 200 && strings.Contains(string(body), "I am by birth a Genevese") {
88-
mu.Lock()
89-
successCount++
90-
mu.Unlock()
91-
}
92-
}(i)
55+
go func() {
56+
defer wg.Done()
57+
body, resp := testGet("http://example.com/index.php", handler, t)
58+
assert.Equal(t, 200, resp.StatusCode)
59+
assert.Contains(t, body, "I am by birth a Genevese")
60+
}()
9361
}
9462
wg.Wait()
95-
96-
t.Logf("Success: %d/%d", successCount, totalRequests)
97-
assert.GreaterOrEqual(t, successCount, int(totalRequests*0.95),
98-
"at least 95%% of requests should succeed despite regular thread restarts")
9963
}, &testOptions{
100-
realServer: true,
10164
initOpts: []frankenphp.Option{
10265
frankenphp.WithNumThreads(8),
10366
frankenphp.WithMaxRequests(maxRequests),
10467
},
10568
})
10669
}
107-
108-
// TestModuleMaxRequestsZeroIsUnlimited verifies that max_requests=0 (default)
109-
// means threads never restart.
110-
func TestModuleMaxRequestsZeroIsUnlimited(t *testing.T) {
111-
runTest(t, func(_ func(http.ResponseWriter, *http.Request), ts *httptest.Server, _ int) {
112-
require.NotNil(t, ts)
113-
client := &http.Client{Timeout: 5 * time.Second}
114-
115-
for i := 0; i < 50; i++ {
116-
resp, err := client.Get(ts.URL + "/index.php")
117-
require.NoError(t, err)
118-
body, _ := io.ReadAll(resp.Body)
119-
_ = resp.Body.Close()
120-
121-
assert.Equal(t, 200, resp.StatusCode)
122-
assert.Contains(t, string(body), "I am by birth a Genevese")
123-
}
124-
}, &testOptions{
125-
realServer: true,
126-
initOpts: []frankenphp.Option{frankenphp.WithNumThreads(2)},
127-
})
128-
}

maxrequests_test.go

Lines changed: 0 additions & 177 deletions
This file was deleted.

0 commit comments

Comments
 (0)