Skip to content

Commit 4ed787e

Browse files
Review by AlliBalliBaba
1 parent 5c53ffb commit 4ed787e

8 files changed

Lines changed: 130 additions & 275 deletions

File tree

frankenphp.go

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,9 @@ func Init(options ...Option) error {
318318
}
319319

320320
regularRequestChan = make(chan contextHolder)
321+
regularThreadMu.Lock()
321322
regularThreads = make([]*phpThread, 0, opt.numThreads-workerThreadCount)
323+
regularThreadMu.Unlock()
322324
for i := 0; i < opt.numThreads-workerThreadCount; i++ {
323325
convertToRegularThread(getInactivePHPThread())
324326
}
@@ -371,13 +373,6 @@ func Shutdown() {
371373

372374
drainWatchers()
373375
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-
381376
drainPHPThreads()
382377

383378
metrics.Shutdown()
@@ -796,6 +791,5 @@ func resetGlobals() {
796791
watcherIsEnabled = false
797792
maxIdleTime = defaultMaxIdleTime
798793
maxRequestsPerThread = 0
799-
shutdownInProgress.Store(false)
800794
globalMu.Unlock()
801795
}

internal/state/state.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ const (
3030
TransitionRequested
3131
TransitionInProgress
3232
TransitionComplete
33+
34+
// thread is exiting the C loop for a full ZTS restart (max_requests)
35+
Rebooting
3336
)
3437

3538
func (s State) String() string {
@@ -58,6 +61,8 @@ func (s State) String() string {
5861
return "transition in progress"
5962
case TransitionComplete:
6063
return "transition complete"
64+
case Rebooting:
65+
return "rebooting"
6166
default:
6267
return "unknown"
6368
}

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.

phpthread.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ func (thread *phpThread) boot() {
6464

6565
// shutdown the underlying PHP thread
6666
func (thread *phpThread) shutdown() {
67+
// if rebooting, wait for the reboot goroutine to finish (it will bail if server is shutting down)
68+
if thread.state.Is(state.Rebooting) {
69+
thread.state.WaitFor(state.Ready, state.Inactive, state.Done, state.Reserved)
70+
}
71+
6772
if !thread.state.RequestSafeStateChange(state.ShuttingDown) {
6873
// already shutting down or done, wait for the C thread to finish
6974
thread.state.WaitFor(state.Done, state.Reserved)

0 commit comments

Comments
 (0)