Skip to content

Commit 9cca128

Browse files
feat: maximum wait times (#1445)
1 parent cc473ee commit 9cca128

File tree

6 files changed

+107
-7
lines changed

6 files changed

+107
-7
lines changed

caddy/caddy.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"path/filepath"
1212
"strconv"
1313
"strings"
14+
"time"
1415

1516
"github.com/dunglas/frankenphp/internal/fastabs"
1617

@@ -63,6 +64,8 @@ type FrankenPHPApp struct {
6364
Workers []workerConfig `json:"workers,omitempty"`
6465
// Overwrites the default php ini configuration
6566
PhpIni map[string]string `json:"php_ini,omitempty"`
67+
// The maximum amount of time a request may be stalled waiting for a thread
68+
MaxWaitTime time.Duration `json:"max_wait_time,omitempty"`
6669

6770
metrics frankenphp.Metrics
6871
logger *zap.Logger
@@ -93,6 +96,7 @@ func (f *FrankenPHPApp) Start() error {
9396
frankenphp.WithLogger(f.logger),
9497
frankenphp.WithMetrics(f.metrics),
9598
frankenphp.WithPhpIni(f.PhpIni),
99+
frankenphp.WithMaxWaitTime(f.MaxWaitTime),
96100
}
97101
for _, w := range f.Workers {
98102
opts = append(opts, frankenphp.WithWorkers(repl.ReplaceKnown(w.FileName, ""), w.Num, w.Env, w.Watch))
@@ -119,6 +123,7 @@ func (f *FrankenPHPApp) Stop() error {
119123
// reset configuration so it doesn't bleed into later tests
120124
f.Workers = nil
121125
f.NumThreads = 0
126+
f.MaxWaitTime = 0
122127

123128
return nil
124129
}
@@ -156,6 +161,17 @@ func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
156161
}
157162

158163
f.MaxThreads = int(v)
164+
case "max_wait_time":
165+
if !d.NextArg() {
166+
return d.ArgErr()
167+
}
168+
169+
v, err := time.ParseDuration(d.Val())
170+
if err != nil {
171+
return errors.New("max_wait_time must be a valid duration (example: 10s)")
172+
}
173+
174+
f.MaxWaitTime = v
159175
case "php_ini":
160176
parseIniLine := func(d *caddyfile.Dispenser) error {
161177
key := d.Val()
@@ -266,7 +282,7 @@ func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
266282

267283
f.Workers = append(f.Workers, wc)
268284
default:
269-
allowedDirectives := "num_threads, max_threads, php_ini, worker"
285+
allowedDirectives := "num_threads, max_threads, php_ini, worker, max_wait_time"
270286
return wrongSubDirectiveError("frankenphp", allowedDirectives, d.Val())
271287
}
272288
}

caddy/caddy_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"path/filepath"
99
"strings"
1010
"sync"
11+
"sync/atomic"
1112
"testing"
1213

1314
"github.com/dunglas/frankenphp"
@@ -748,3 +749,53 @@ func TestOsEnv(t *testing.T) {
748749
"ENV1=value1,ENV2=value2",
749750
)
750751
}
752+
753+
func TestMaxWaitTime(t *testing.T) {
754+
tester := caddytest.NewTester(t)
755+
tester.InitServer(`
756+
{
757+
skip_install_trust
758+
admin localhost:2999
759+
http_port `+testPort+`
760+
761+
frankenphp {
762+
num_threads 1
763+
max_wait_time 1ns
764+
}
765+
}
766+
767+
localhost:`+testPort+` {
768+
route {
769+
root ../testdata
770+
php
771+
}
772+
}
773+
`, "caddyfile")
774+
775+
// send 10 requests simultaneously, at least one request should be stalled longer than 1ns
776+
// since we only have 1 thread, this will cause a 504 Gateway Timeout
777+
wg := sync.WaitGroup{}
778+
success := atomic.Bool{}
779+
wg.Add(10)
780+
for i := 0; i < 10; i++ {
781+
go func() {
782+
statusCode := getStatusCode("http://localhost:"+testPort+"/sleep.php?sleep=10", t)
783+
if statusCode == http.StatusGatewayTimeout {
784+
success.Store(true)
785+
}
786+
wg.Done()
787+
}()
788+
}
789+
wg.Wait()
790+
791+
require.True(t, success.Load(), "At least one request should have failed with a 504 Gateway Timeout status")
792+
}
793+
794+
func getStatusCode(url string, t *testing.T) int {
795+
req, err := http.NewRequest("GET", url, nil)
796+
require.NoError(t, err)
797+
resp, err := http.DefaultClient.Do(req)
798+
require.NoError(t, err)
799+
defer resp.Body.Close()
800+
return resp.StatusCode
801+
}

frankenphp.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import (
4141
"strings"
4242
"sync"
4343
"syscall"
44+
"time"
4445
"unsafe"
4546

4647
"go.uber.org/zap"
@@ -68,6 +69,8 @@ var (
6869
logger *zap.Logger
6970

7071
metrics Metrics = nullMetrics{}
72+
73+
maxWaitTime time.Duration
7174
)
7275

7376
type syslogLevel int
@@ -249,6 +252,8 @@ func Init(options ...Option) error {
249252
metrics = opt.metrics
250253
}
251254

255+
maxWaitTime = opt.maxWaitTime
256+
252257
totalThreadCount, workerThreadCount, maxThreadCount, err := calculateMaxThreads(opt)
253258
if err != nil {
254259
return err
@@ -654,3 +659,11 @@ func executePHPFunction(functionName string) bool {
654659

655660
return C.frankenphp_execute_php_function(cFunctionName) == 1
656661
}
662+
663+
func timeoutChan(timeout time.Duration) <-chan time.Time {
664+
if timeout == 0 {
665+
return nil
666+
}
667+
668+
return time.After(timeout)
669+
}

options.go

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package frankenphp
22

33
import (
4+
"time"
5+
46
"go.uber.org/zap"
57
)
68

@@ -11,12 +13,13 @@ type Option func(h *opt) error
1113
//
1214
// If you change this, also update the Caddy module and the documentation.
1315
type opt struct {
14-
numThreads int
15-
maxThreads int
16-
workers []workerOpt
17-
logger *zap.Logger
18-
metrics Metrics
19-
phpIni map[string]string
16+
numThreads int
17+
maxThreads int
18+
workers []workerOpt
19+
logger *zap.Logger
20+
metrics Metrics
21+
phpIni map[string]string
22+
maxWaitTime time.Duration
2023
}
2124

2225
type workerOpt struct {
@@ -76,3 +79,12 @@ func WithPhpIni(overrides map[string]string) Option {
7679
return nil
7780
}
7881
}
82+
83+
// WithMaxWaitTime configures the max time a request may be stalled waiting for a thread.
84+
func WithMaxWaitTime(maxWaitTime time.Duration) Option {
85+
return func(o *opt) error {
86+
o.maxWaitTime = maxWaitTime
87+
88+
return nil
89+
}
90+
}

threadregular.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,10 @@ func handleRequestWithRegularPHPThreads(fc *frankenPHPContext) {
116116
return
117117
case scaleChan <- fc:
118118
// the request has triggered scaling, continue to wait for a thread
119+
case <-timeoutChan(maxWaitTime):
120+
// the request has timed out stalling
121+
fc.reject(504, "Gateway Timeout")
122+
return
119123
}
120124
}
121125
}

worker.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,10 @@ func (worker *worker) handleRequest(fc *frankenPHPContext) {
198198
return
199199
case scaleChan <- fc:
200200
// the request has triggered scaling, continue to wait for a thread
201+
case <-timeoutChan(maxWaitTime):
202+
// the request has timed out stalling
203+
fc.reject(504, "Gateway Timeout")
204+
return
201205
}
202206
}
203207
}

0 commit comments

Comments
 (0)