Skip to content

Commit 777ea43

Browse files
committed
reuse created-once channels so queued requests don't get stuck on replaced channels
1 parent edaffab commit 777ea43

3 files changed

Lines changed: 69 additions & 4 deletions

File tree

frankenphp.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import (
3636
"time"
3737
"unsafe"
3838
// debug on Linux
39-
//_ "github.com/ianlancetaylor/cgosymbolizer"
39+
// _ "github.com/ianlancetaylor/cgosymbolizer"
4040
)
4141

4242
type contextKeyStruct struct{}
@@ -317,7 +317,10 @@ func Init(options ...Option) error {
317317
return err
318318
}
319319

320-
regularRequestChan = make(chan contextHolder)
320+
// reused across reloads so queued requests aren't orphaned on a stale channel
321+
if regularRequestChan == nil {
322+
regularRequestChan = make(chan contextHolder)
323+
}
321324
regularThreads = make([]*phpThread, 0, opt.numThreads-workerThreadCount)
322325
for i := 0; i < opt.numThreads-workerThreadCount; i++ {
323326
convertToRegularThread(getInactivePHPThread())

phpmainthread_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,65 @@ func TestTransitionThreadsWhileDoingRequests(t *testing.T) {
166166
transitionsWG.Wait()
167167
}
168168

169+
// Test https://github.com/php/frankenphp/issues/2469
170+
// A request queued for a thread when the server reloads must be served
171+
// by the new threads, not rejected with ErrMaxWaitTimeExceeded.
172+
func TestQueuedRequestSurvivesReload(t *testing.T) {
173+
t.Cleanup(Shutdown)
174+
175+
initOpts := []Option{
176+
WithNumThreads(1),
177+
WithMaxThreads(1),
178+
WithMaxWaitTime(5 * time.Second),
179+
}
180+
assert.NoError(t, Init(initOpts...))
181+
182+
doRequest := func(query string) error {
183+
r := httptest.NewRequest("GET", "http://localhost/sleep.php?"+query, nil)
184+
w := httptest.NewRecorder()
185+
req, err := NewRequestWithContext(r, WithRequestDocumentRoot(testDataPath, false))
186+
if err != nil {
187+
return err
188+
}
189+
190+
return ServeHTTP(w, req)
191+
}
192+
193+
// Occupy the single PHP thread so the next request has to wait in the queue.
194+
started := make(chan struct{})
195+
go func() {
196+
close(started)
197+
_ = doRequest("sleep=800")
198+
}()
199+
<-started
200+
time.Sleep(100 * time.Millisecond)
201+
202+
queued := make(chan error, 1)
203+
go func() {
204+
queued <- doRequest("sleep=0")
205+
}()
206+
207+
// Wait until the second request is genuinely queued waiting for a thread.
208+
deadline := time.Now().Add(2 * time.Second)
209+
for queuedRegularThreads.Load() == 0 {
210+
if !assert.True(t, time.Now().Before(deadline), "request was never queued") {
211+
return
212+
}
213+
time.Sleep(time.Millisecond)
214+
}
215+
216+
// Reload while the request is queued, exactly like the Caddy module does.
217+
Shutdown()
218+
assert.NoError(t, Init(initOpts...))
219+
220+
select {
221+
case err := <-queued:
222+
assert.NoError(t, err, "a queued request must not be rejected by a reload")
223+
case <-time.After(15 * time.Second):
224+
t.Fatal("the queued request never completed after the reload")
225+
}
226+
}
227+
169228
func TestFinishBootingAWorkerScript(t *testing.T) {
170229
setupGlobals(t)
171230

scaling.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,19 @@ var (
3535
)
3636

3737
func initAutoScaling(mainThread *phpMainThread) {
38+
// reused across reloads so queued requests aren't orphaned on a stale channel
39+
if scaleChan == nil {
40+
scaleChan = make(chan *frankenPHPContext)
41+
}
42+
3843
if mainThread.maxThreads <= mainThread.numThreads {
39-
scaleChan = nil
4044
return
4145
}
4246

4347
done := mainThread.done
4448
mstate := mainThread.state
4549

4650
scalingMu.Lock()
47-
scaleChan = make(chan *frankenPHPContext)
4851
maxScaledThreads := mainThread.maxThreads - mainThread.numThreads
4952
autoScaledThreads = make([]*phpThread, 0, maxScaledThreads)
5053
scalingMu.Unlock()

0 commit comments

Comments
 (0)