Skip to content

Commit cc473ee

Browse files
fix: better max_threads calculation (#1446)
1 parent 93266df commit cc473ee

File tree

2 files changed

+83
-8
lines changed

2 files changed

+83
-8
lines changed

frankenphp.go

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,6 @@ var (
5757
InvalidRequestError = errors.New("not a FrankenPHP request")
5858
AlreadyStartedError = errors.New("FrankenPHP is already started")
5959
InvalidPHPVersionError = errors.New("FrankenPHP is only compatible with PHP 8.2+")
60-
NotEnoughThreads = errors.New("the number of threads must be superior to the number of workers")
6160
MainThreadCreationError = errors.New("error creating the main thread")
6261
RequestContextCreationError = errors.New("error during request context creation")
6362
ScriptExecutionError = errors.New("error during PHP script execution")
@@ -163,23 +162,52 @@ func calculateMaxThreads(opt *opt) (int, int, int, error) {
163162
numWorkers += opt.workers[i].num
164163
}
165164

166-
if opt.numThreads <= 0 {
165+
numThreadsIsSet := opt.numThreads > 0
166+
maxThreadsIsSet := opt.maxThreads != 0
167+
maxThreadsIsAuto := opt.maxThreads < 0 // maxthreads < 0 signifies auto mode (see phpmaintread.go)
168+
169+
if numThreadsIsSet && !maxThreadsIsSet {
170+
opt.maxThreads = opt.numThreads
171+
if opt.numThreads <= numWorkers {
172+
err := fmt.Errorf("num_threads (%d) must be greater than the number of worker threads (%d)", opt.numThreads, numWorkers)
173+
return 0, 0, 0, err
174+
}
175+
176+
return opt.numThreads, numWorkers, opt.maxThreads, nil
177+
}
178+
179+
if maxThreadsIsSet && !numThreadsIsSet {
180+
opt.numThreads = numWorkers + 1
181+
if !maxThreadsIsAuto && opt.numThreads > opt.maxThreads {
182+
err := fmt.Errorf("max_threads (%d) must be greater than the number of worker threads (%d)", opt.maxThreads, numWorkers)
183+
return 0, 0, 0, err
184+
}
185+
186+
return opt.numThreads, numWorkers, opt.maxThreads, nil
187+
}
188+
189+
if !numThreadsIsSet && !maxThreadsIsSet {
167190
if numWorkers >= maxProcs {
168191
// Start at least as many threads as workers, and keep a free thread to handle requests in non-worker mode
169192
opt.numThreads = numWorkers + 1
170193
} else {
171194
opt.numThreads = maxProcs
172195
}
173-
} else if opt.numThreads <= numWorkers {
174-
return opt.numThreads, numWorkers, opt.maxThreads, NotEnoughThreads
196+
opt.maxThreads = opt.numThreads
197+
198+
return opt.numThreads, numWorkers, opt.maxThreads, nil
175199
}
176200

177-
if opt.maxThreads < opt.numThreads && opt.maxThreads > 0 {
178-
opt.maxThreads = opt.numThreads
201+
// both num_threads and max_threads are set
202+
if opt.numThreads <= numWorkers {
203+
err := fmt.Errorf("num_threads (%d) must be greater than the number of worker threads (%d)", opt.numThreads, numWorkers)
204+
return 0, 0, 0, err
179205
}
180206

181-
metrics.TotalThreads(opt.numThreads)
182-
MaxThreads = opt.numThreads
207+
if !maxThreadsIsAuto && opt.maxThreads < opt.numThreads {
208+
err := fmt.Errorf("max_threads (%d) must be greater than or equal to num_threads (%d)", opt.maxThreads, opt.numThreads)
209+
return 0, 0, 0, err
210+
}
183211

184212
return opt.numThreads, numWorkers, opt.maxThreads, nil
185213
}
@@ -226,6 +254,9 @@ func Init(options ...Option) error {
226254
return err
227255
}
228256

257+
metrics.TotalThreads(totalThreadCount)
258+
MaxThreads = totalThreadCount
259+
229260
config := Config()
230261

231262
if config.Version.MajorVersion < 8 || (config.Version.MajorVersion == 8 && config.Version.MinorVersion < 2) {

phpmainthread_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"math/rand/v2"
66
"net/http/httptest"
77
"path/filepath"
8+
"runtime"
89
"sync"
910
"sync/atomic"
1011
"testing"
@@ -214,3 +215,46 @@ func allPossibleTransitions(worker1Path string, worker2Path string) []func(*phpT
214215
convertToInactiveThread,
215216
}
216217
}
218+
219+
func TestCorrectThreadCalculation(t *testing.T) {
220+
maxProcs := runtime.GOMAXPROCS(0) * 2
221+
oneWorkerThread := []workerOpt{workerOpt{num: 1}}
222+
223+
// default values
224+
testThreadCalculation(t, maxProcs, maxProcs, &opt{})
225+
testThreadCalculation(t, maxProcs, maxProcs, &opt{workers: oneWorkerThread})
226+
227+
// num_threads is set
228+
testThreadCalculation(t, 1, 1, &opt{numThreads: 1})
229+
testThreadCalculation(t, 2, 2, &opt{numThreads: 2, workers: oneWorkerThread})
230+
231+
// max_threads is set
232+
testThreadCalculation(t, 1, 10, &opt{maxThreads: 10})
233+
testThreadCalculation(t, 2, 10, &opt{maxThreads: 10, workers: oneWorkerThread})
234+
testThreadCalculation(t, 5, 10, &opt{numThreads: 5, maxThreads: 10, workers: oneWorkerThread})
235+
236+
// automatic max_threads
237+
testThreadCalculation(t, 1, -1, &opt{maxThreads: -1})
238+
testThreadCalculation(t, 2, -1, &opt{maxThreads: -1, workers: oneWorkerThread})
239+
testThreadCalculation(t, 2, -1, &opt{numThreads: 2, maxThreads: -1})
240+
241+
// not enough num threads
242+
testThreadCalculationError(t, &opt{numThreads: 1, workers: oneWorkerThread})
243+
testThreadCalculationError(t, &opt{numThreads: 1, maxThreads: 1, workers: oneWorkerThread})
244+
245+
// not enough max_threads
246+
testThreadCalculationError(t, &opt{numThreads: 2, maxThreads: 1})
247+
testThreadCalculationError(t, &opt{maxThreads: 1, workers: oneWorkerThread})
248+
}
249+
250+
func testThreadCalculation(t *testing.T, expectedNumThreads int, expectedMaxThreads int, o *opt) {
251+
totalThreadCount, _, maxThreadCount, err := calculateMaxThreads(o)
252+
assert.NoError(t, err, "no error should be returned")
253+
assert.Equal(t, expectedNumThreads, totalThreadCount, "num_threads must be correct")
254+
assert.Equal(t, expectedMaxThreads, maxThreadCount, "max_threads must be correct")
255+
}
256+
257+
func testThreadCalculationError(t *testing.T, o *opt) {
258+
_, _, _, err := calculateMaxThreads(o)
259+
assert.Error(t, err, "configuration must error")
260+
}

0 commit comments

Comments
 (0)