Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion caddy/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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())
}
}
}
Expand Down
1 change: 1 addition & 0 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions frankenphp.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -781,5 +785,6 @@ func resetGlobals() {
workersByName = nil
workersByPath = nil
watcherIsEnabled = false
maxIdleTime = defaultMaxIdleTime
globalMu.Unlock()
}
10 changes: 10 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type opt struct {
metrics Metrics
phpIni map[string]string
maxWaitTime time.Duration
maxIdleTime time.Duration
}

type workerOpt struct {
Expand Down Expand Up @@ -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 {
Expand Down
7 changes: 4 additions & 3 deletions scaling.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,14 @@ const (
downScaleCheckTime = 5 * time.Second
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't this the same as max_idle_time?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No

downScaleCheckTime is the polling interval — how often the downscaler checks for idle threads.
maxIdleTime is the threshold — how long a thread must be idle before it gets deactivated.

They're both 5s by default which makes them look the same, but they control different things.

// 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)
Expand Down Expand Up @@ -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:]...)
Expand Down
26 changes: 26 additions & 0 deletions scaling_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, &regularThread{}, 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()

Expand Down