mirror of
https://github.com/php/frankenphp.git
synced 2026-03-24 00:52:11 +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"`
|
PhpIni map[string]string `json:"php_ini,omitempty"`
|
||||||
// The maximum amount of time a request may be stalled waiting for a thread
|
// The maximum amount of time a request may be stalled waiting for a thread
|
||||||
MaxWaitTime time.Duration `json:"max_wait_time,omitempty"`
|
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
|
opts []frankenphp.Option
|
||||||
metrics frankenphp.Metrics
|
metrics frankenphp.Metrics
|
||||||
@@ -150,6 +152,7 @@ func (f *FrankenPHPApp) Start() error {
|
|||||||
frankenphp.WithMetrics(f.metrics),
|
frankenphp.WithMetrics(f.metrics),
|
||||||
frankenphp.WithPhpIni(f.PhpIni),
|
frankenphp.WithPhpIni(f.PhpIni),
|
||||||
frankenphp.WithMaxWaitTime(f.MaxWaitTime),
|
frankenphp.WithMaxWaitTime(f.MaxWaitTime),
|
||||||
|
frankenphp.WithMaxIdleTime(f.MaxIdleTime),
|
||||||
)
|
)
|
||||||
|
|
||||||
for _, w := range f.Workers {
|
for _, w := range f.Workers {
|
||||||
@@ -190,6 +193,7 @@ func (f *FrankenPHPApp) Stop() error {
|
|||||||
f.Workers = nil
|
f.Workers = nil
|
||||||
f.NumThreads = 0
|
f.NumThreads = 0
|
||||||
f.MaxWaitTime = 0
|
f.MaxWaitTime = 0
|
||||||
|
f.MaxIdleTime = 0
|
||||||
|
|
||||||
optionsMU.Lock()
|
optionsMU.Lock()
|
||||||
options = nil
|
options = nil
|
||||||
@@ -242,6 +246,17 @@ func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
f.MaxWaitTime = v
|
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":
|
case "php_ini":
|
||||||
parseIniLine := func(d *caddyfile.Dispenser) error {
|
parseIniLine := func(d *caddyfile.Dispenser) error {
|
||||||
key := d.Val()
|
key := d.Val()
|
||||||
@@ -298,7 +313,7 @@ func (f *FrankenPHPApp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
|
|||||||
|
|
||||||
f.Workers = append(f.Workers, wc)
|
f.Workers = append(f.Workers, wc)
|
||||||
default:
|
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.
|
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_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_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.
|
php_ini <key> <value> # Set a php.ini directive. Can be used several times to set multiple directives.
|
||||||
worker {
|
worker {
|
||||||
file <path> # Sets the path to the worker script.
|
file <path> # Sets the path to the worker script.
|
||||||
|
|||||||
@@ -276,6 +276,10 @@ func Init(options ...Option) error {
|
|||||||
|
|
||||||
maxWaitTime = opt.maxWaitTime
|
maxWaitTime = opt.maxWaitTime
|
||||||
|
|
||||||
|
if opt.maxIdleTime > 0 {
|
||||||
|
maxIdleTime = opt.maxIdleTime
|
||||||
|
}
|
||||||
|
|
||||||
workerThreadCount, err := calculateMaxThreads(opt)
|
workerThreadCount, err := calculateMaxThreads(opt)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Shutdown()
|
Shutdown()
|
||||||
@@ -781,5 +785,6 @@ func resetGlobals() {
|
|||||||
workersByName = nil
|
workersByName = nil
|
||||||
workersByPath = nil
|
workersByPath = nil
|
||||||
watcherIsEnabled = false
|
watcherIsEnabled = false
|
||||||
|
maxIdleTime = defaultMaxIdleTime
|
||||||
globalMu.Unlock()
|
globalMu.Unlock()
|
||||||
}
|
}
|
||||||
|
|||||||
10
options.go
10
options.go
@@ -30,6 +30,7 @@ type opt struct {
|
|||||||
metrics Metrics
|
metrics Metrics
|
||||||
phpIni map[string]string
|
phpIni map[string]string
|
||||||
maxWaitTime time.Duration
|
maxWaitTime time.Duration
|
||||||
|
maxIdleTime time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
type workerOpt struct {
|
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
|
// WithWorkerEnv sets environment variables for the worker
|
||||||
func WithWorkerEnv(env map[string]string) WorkerOption {
|
func WithWorkerEnv(env map[string]string) WorkerOption {
|
||||||
return func(w *workerOpt) error {
|
return func(w *workerOpt) error {
|
||||||
|
|||||||
@@ -21,13 +21,14 @@ const (
|
|||||||
downScaleCheckTime = 5 * time.Second
|
downScaleCheckTime = 5 * time.Second
|
||||||
// max amount of threads stopped in one iteration of downScaleCheckTime
|
// max amount of threads stopped in one iteration of downScaleCheckTime
|
||||||
maxTerminationCount = 10
|
maxTerminationCount = 10
|
||||||
// autoscaled threads waiting for longer than this time are downscaled
|
// default time an autoscaled thread may be idle before being deactivated
|
||||||
maxThreadIdleTime = 5 * time.Second
|
defaultMaxIdleTime = 5 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrMaxThreadsReached = errors.New("max amount of overall threads reached")
|
ErrMaxThreadsReached = errors.New("max amount of overall threads reached")
|
||||||
|
|
||||||
|
maxIdleTime = defaultMaxIdleTime
|
||||||
scaleChan chan *frankenPHPContext
|
scaleChan chan *frankenPHPContext
|
||||||
autoScaledThreads = []*phpThread{}
|
autoScaledThreads = []*phpThread{}
|
||||||
scalingMu = new(sync.RWMutex)
|
scalingMu = new(sync.RWMutex)
|
||||||
@@ -221,7 +222,7 @@ func deactivateThreads() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// convert threads to inactive if they have been idle for too long
|
// 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)
|
convertToInactiveThread(thread)
|
||||||
stoppedThreadCount++
|
stoppedThreadCount++
|
||||||
autoScaledThreads = append(autoScaledThreads[:i], autoScaledThreads[i+1:]...)
|
autoScaledThreads = append(autoScaledThreads[:i], autoScaledThreads[i+1:]...)
|
||||||
|
|||||||
@@ -57,6 +57,32 @@ func TestScaleAWorkerThreadUpAndDown(t *testing.T) {
|
|||||||
assert.IsType(t, &inactiveThread{}, autoScaledThread.handler)
|
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) {
|
func setLongWaitTime(t *testing.T, thread *phpThread) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user