mirror of
https://github.com/php/frankenphp.git
synced 2026-03-23 16:42:13 +01:00
feat(caddy): configurable max_idle_time for autoscaled threads (#2225)
Add configurable max_idle_time for autoscaled threads
The idle timeout for autoscaled threads is currently hardcoded to 5
seconds. With bursty traffic patterns, this causes threads to be
deactivated too quickly, leading to repeated cold-start overhead when
the next burst arrives.
This PR replaces the hardcoded constant with a configurable
max_idle_time directive, allowing users to tune how long idle
autoscaled threads stay alive before deactivation. The default remains 5
seconds, preserving existing behavior.
Usage:
Caddyfile:
````
frankenphp {
max_idle_time 30s
}
````
JSON config:
```
{
"frankenphp": {
"max_idle_time": "30s"
}
}
````
Changes:
- New max_idle_time Caddyfile directive and JSON config option
- New WithMaxIdleTime functional option
- Replaced hardcoded maxThreadIdleTime constant with configurable
maxIdleTime variable
- Added tests for custom and default idle time behavior
- Updated docs
This commit is contained in:
17
caddy/app.go
17
caddy/app.go
@@ -55,6 +55,8 @@ type FrankenPHPApp struct {
|
||||
PhpIni map[string]string `json:"php_ini,omitempty"`
|
||||
// The maximum amount of time a request may be stalled waiting for a thread
|
||||
MaxWaitTime time.Duration `json:"max_wait_time,omitempty"`
|
||||
// The maximum amount of time an autoscaled thread may be idle before being deactivated
|
||||
MaxIdleTime time.Duration `json:"max_idle_time,omitempty"`
|
||||
|
||||
opts []frankenphp.Option
|
||||
metrics frankenphp.Metrics
|
||||
@@ -150,6 +152,7 @@ func (f *FrankenPHPApp) Start() error {
|
||||
frankenphp.WithMetrics(f.metrics),
|
||||
frankenphp.WithPhpIni(f.PhpIni),
|
||||
frankenphp.WithMaxWaitTime(f.MaxWaitTime),
|
||||
frankenphp.WithMaxIdleTime(f.MaxIdleTime),
|
||||
)
|
||||
|
||||
for _, w := range f.Workers {
|
||||
@@ -190,6 +193,7 @@ func (f *FrankenPHPApp) Stop() error {
|
||||
f.Workers = nil
|
||||
f.NumThreads = 0
|
||||
f.MaxWaitTime = 0
|
||||
f.MaxIdleTime = 0
|
||||
|
||||
optionsMU.Lock()
|
||||
options = nil
|
||||
@@ -242,6 +246,17 @@ func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
}
|
||||
|
||||
f.MaxWaitTime = v
|
||||
case "max_idle_time":
|
||||
if !d.NextArg() {
|
||||
return d.ArgErr()
|
||||
}
|
||||
|
||||
v, err := time.ParseDuration(d.Val())
|
||||
if err != nil {
|
||||
return d.Err("max_idle_time must be a valid duration (example: 30s)")
|
||||
}
|
||||
|
||||
f.MaxIdleTime = v
|
||||
case "php_ini":
|
||||
parseIniLine := func(d *caddyfile.Dispenser) error {
|
||||
key := d.Val()
|
||||
@@ -298,7 +313,7 @@ func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
||||
|
||||
f.Workers = append(f.Workers, wc)
|
||||
default:
|
||||
return wrongSubDirectiveError("frankenphp", "num_threads, max_threads, php_ini, worker, max_wait_time", d.Val())
|
||||
return wrongSubDirectiveError("frankenphp", "num_threads, max_threads, php_ini, worker, max_wait_time, max_idle_time", d.Val())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,6 +96,7 @@ You can also explicitly configure FrankenPHP using the [global option](https://c
|
||||
num_threads <num_threads> # Sets the number of PHP threads to start. Default: 2x the number of available CPUs.
|
||||
max_threads <num_threads> # Limits the number of additional PHP threads that can be started at runtime. Default: num_threads. Can be set to 'auto'.
|
||||
max_wait_time <duration> # Sets the maximum time a request may wait for a free PHP thread before timing out. Default: disabled.
|
||||
max_idle_time <duration> # Sets the maximum time an autoscaled thread may be idle before being deactivated. Default: 5s.
|
||||
php_ini <key> <value> # Set a php.ini directive. Can be used several times to set multiple directives.
|
||||
worker {
|
||||
file <path> # Sets the path to the worker script.
|
||||
|
||||
@@ -276,6 +276,10 @@ func Init(options ...Option) error {
|
||||
|
||||
maxWaitTime = opt.maxWaitTime
|
||||
|
||||
if opt.maxIdleTime > 0 {
|
||||
maxIdleTime = opt.maxIdleTime
|
||||
}
|
||||
|
||||
workerThreadCount, err := calculateMaxThreads(opt)
|
||||
if err != nil {
|
||||
Shutdown()
|
||||
@@ -781,5 +785,6 @@ func resetGlobals() {
|
||||
workersByName = nil
|
||||
workersByPath = nil
|
||||
watcherIsEnabled = false
|
||||
maxIdleTime = defaultMaxIdleTime
|
||||
globalMu.Unlock()
|
||||
}
|
||||
|
||||
10
options.go
10
options.go
@@ -30,6 +30,7 @@ type opt struct {
|
||||
metrics Metrics
|
||||
phpIni map[string]string
|
||||
maxWaitTime time.Duration
|
||||
maxIdleTime time.Duration
|
||||
}
|
||||
|
||||
type workerOpt struct {
|
||||
@@ -156,6 +157,15 @@ func WithMaxWaitTime(maxWaitTime time.Duration) Option {
|
||||
}
|
||||
}
|
||||
|
||||
// WithMaxIdleTime configures the max time an autoscaled thread may be idle before being deactivated.
|
||||
func WithMaxIdleTime(maxIdleTime time.Duration) Option {
|
||||
return func(o *opt) error {
|
||||
o.maxIdleTime = maxIdleTime
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithWorkerEnv sets environment variables for the worker
|
||||
func WithWorkerEnv(env map[string]string) WorkerOption {
|
||||
return func(w *workerOpt) error {
|
||||
|
||||
@@ -21,13 +21,14 @@ const (
|
||||
downScaleCheckTime = 5 * time.Second
|
||||
// max amount of threads stopped in one iteration of downScaleCheckTime
|
||||
maxTerminationCount = 10
|
||||
// autoscaled threads waiting for longer than this time are downscaled
|
||||
maxThreadIdleTime = 5 * time.Second
|
||||
// default time an autoscaled thread may be idle before being deactivated
|
||||
defaultMaxIdleTime = 5 * time.Second
|
||||
)
|
||||
|
||||
var (
|
||||
ErrMaxThreadsReached = errors.New("max amount of overall threads reached")
|
||||
|
||||
maxIdleTime = defaultMaxIdleTime
|
||||
scaleChan chan *frankenPHPContext
|
||||
autoScaledThreads = []*phpThread{}
|
||||
scalingMu = new(sync.RWMutex)
|
||||
@@ -221,7 +222,7 @@ func deactivateThreads() {
|
||||
}
|
||||
|
||||
// convert threads to inactive if they have been idle for too long
|
||||
if thread.state.Is(state.Ready) && waitTime > maxThreadIdleTime.Milliseconds() {
|
||||
if thread.state.Is(state.Ready) && waitTime > maxIdleTime.Milliseconds() {
|
||||
convertToInactiveThread(thread)
|
||||
stoppedThreadCount++
|
||||
autoScaledThreads = append(autoScaledThreads[:i], autoScaledThreads[i+1:]...)
|
||||
|
||||
@@ -57,6 +57,32 @@ func TestScaleAWorkerThreadUpAndDown(t *testing.T) {
|
||||
assert.IsType(t, &inactiveThread{}, autoScaledThread.handler)
|
||||
}
|
||||
|
||||
func TestMaxIdleTimePreventsEarlyDeactivation(t *testing.T) {
|
||||
t.Cleanup(Shutdown)
|
||||
|
||||
assert.NoError(t, Init(
|
||||
WithNumThreads(1),
|
||||
WithMaxThreads(2),
|
||||
WithMaxIdleTime(time.Hour),
|
||||
))
|
||||
|
||||
autoScaledThread := phpThreads[1]
|
||||
|
||||
// scale up
|
||||
scaleRegularThread()
|
||||
assert.Equal(t, state.Ready, autoScaledThread.state.Get())
|
||||
|
||||
// set wait time to 30 minutes (less than 1 hour max idle time)
|
||||
autoScaledThread.state.SetWaitTime(time.Now().Add(-30 * time.Minute))
|
||||
deactivateThreads()
|
||||
assert.IsType(t, ®ularThread{}, autoScaledThread.handler, "thread should still be active after 30min with 1h max idle time")
|
||||
|
||||
// set wait time to over 1 hour (exceeds max idle time)
|
||||
autoScaledThread.state.SetWaitTime(time.Now().Add(-time.Hour - time.Minute))
|
||||
deactivateThreads()
|
||||
assert.IsType(t, &inactiveThread{}, autoScaledThread.handler, "thread should be deactivated after exceeding max idle time")
|
||||
}
|
||||
|
||||
func setLongWaitTime(t *testing.T, thread *phpThread) {
|
||||
t.Helper()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user