Skip to content

Commit 9fd6a5f

Browse files
review: address AlliBalliBaba's bg-worker thread accounting feedback
Two follow-ups on the per-php_server scoping batch: - Extract reserveBackgroundWorkerThreads from calculateMaxThreads. The bg-worker branch was a dense block of defaults + slot reservation + metric registration; lifting it into its own function keeps calculateMaxThreads focused on HTTP-worker accounting and makes the three lazy-start cases (named/named-with-cap/catch-all) read in one place. - Rename the leftover backgroundScope field to scope on worker, workerOpt, frankenPHPContext and FrankenPHPModule. The type was already shortened from BackgroundScope to Scope; the field names lag.
1 parent 1cd1ad6 commit 9fd6a5f

10 files changed

Lines changed: 79 additions & 62 deletions

bgworker.go

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -149,15 +149,15 @@ func buildBackgroundWorkerLookups(workers []*worker, opts []workerOpt) (map[Scop
149149
continue
150150
}
151151

152-
scope := o.backgroundScope
152+
scope := o.scope
153153
lookup, ok := lookups[scope]
154154
if !ok {
155155
lookup = newBackgroundWorkerLookup()
156156
lookups[scope] = lookup
157157
}
158158

159159
w := workers[i]
160-
w.backgroundScope = scope
160+
w.scope = scope
161161
// Pre-allocate so ensure() against an already-running eager worker
162162
// has a channel to wait on. Catch-all templates allocate it too but
163163
// it stays unused (each lazy-spawned name gets its own slot).
@@ -191,6 +191,43 @@ func buildBackgroundWorkerLookups(workers []*worker, opts []workerOpt) (map[Scop
191191
return lookups, nil
192192
}
193193

194+
// reserveBackgroundWorkerThreads resolves max_threads defaults for bg
195+
// workers and returns the worst-case thread footprint to add to the pool
196+
// so ensure() always has a free slot to schedule into. Mutates
197+
// opt.workers in place. Also pre-registers totalWorkers — without this,
198+
// a bg-worker-only deployment never initialises the metric and every
199+
// StartWorker/ReadyWorker call becomes a silent no-op.
200+
func reserveBackgroundWorkerThreads(opt *opt) int {
201+
reserved := 0
202+
for i, w := range opt.workers {
203+
if !w.isBackgroundWorker {
204+
continue
205+
}
206+
// num=0 max_threads=0 is the lazy-start opt-in. Distinguish a
207+
// catch-all (no name / name == file) — which can host up to
208+
// defaultMaxBackgroundWorkers distinct lazy-started names —
209+
// from a named lazy worker, which only needs one slot.
210+
if w.maxThreads == 0 && w.num == 0 {
211+
phpName := strings.TrimPrefix(w.name, "m#")
212+
if phpName == "" || phpName == w.fileName {
213+
opt.workers[i].maxThreads = defaultMaxBackgroundWorkers
214+
} else {
215+
opt.workers[i].maxThreads = 1
216+
}
217+
}
218+
extra := w.num
219+
if extra < 1 {
220+
extra = 1
221+
}
222+
if opt.workers[i].maxThreads > extra {
223+
extra = opt.workers[i].maxThreads
224+
}
225+
reserved += extra
226+
metrics.TotalWorkers(ScopeLabel(w.scope), w.name, extra)
227+
}
228+
return reserved
229+
}
230+
194231
// getLookup returns the background-worker lookup for the calling thread,
195232
// resolving the scope via worker handler -> request context -> global. A
196233
// scope with no declared workers falls through to scope 0 so embed-mode
@@ -205,9 +242,9 @@ func getLookup(thread *phpThread) *backgroundWorkerLookup {
205242
// via this anonymous interface; classic-mode threads don't and fall
206243
// through to the request-context path.
207244
if h, ok := thread.handler.(interface{ scopedWorker() *worker }); ok {
208-
scope = h.scopedWorker().backgroundScope
245+
scope = h.scopedWorker().scope
209246
} else if fc, ok := fromContext(thread.context()); ok {
210-
scope = fc.backgroundScope
247+
scope = fc.scope
211248
}
212249
}
213250
if scope != 0 {

bgworkerscope_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ func TestBackgroundWorkerCatchAllPerScope(t *testing.T) {
9292
// We use a request-scope tag (rather than a worker handler) because
9393
// no scope-specific handler is running in the test.
9494
fc := newFrankenPHPContext()
95-
fc.backgroundScope = scopeA
95+
fc.scope = scopeA
9696
ctx := context.WithValue(context.Background(), contextKey, fc)
9797
thread := &phpThread{}
9898
thread.handler = &fakeContextThread{ctx: ctx}

caddy/module.go

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ type FrankenPHPModule struct {
5252
preparedEnvNeedsReplacement bool
5353
logger *slog.Logger
5454
requestOptions []frankenphp.RequestOption
55-
backgroundScope frankenphp.Scope
55+
scope frankenphp.Scope
5656
scopeLabelOnce sync.Once
5757
}
5858

@@ -81,12 +81,11 @@ func (f *FrankenPHPModule) Provision(ctx caddy.Context) error {
8181

8282
f.assignMercureHub(ctx)
8383

84-
// Each php_server block gets its own background-worker scope so
85-
// workers declared with the same name in different blocks don't
86-
// collide. Provision can be called more than once for the same
87-
// module; only assign once.
88-
if f.backgroundScope == 0 {
89-
f.backgroundScope = frankenphp.NextScope()
84+
// Each php_server block gets its own scope so workers declared with
85+
// the same name in different blocks don't collide. Provision can be
86+
// called more than once for the same module; only assign once.
87+
if f.scope == 0 {
88+
f.scope = frankenphp.NextScope()
9089
}
9190

9291
loggerOpt := frankenphp.WithRequestLogger(f.logger)
@@ -103,7 +102,7 @@ func (f *FrankenPHPModule) Provision(ctx caddy.Context) error {
103102
}
104103

105104
wc.requestOptions = append(wc.requestOptions, loggerOpt)
106-
wc.options = append(wc.options, frankenphp.WithWorkerScope(f.backgroundScope))
105+
wc.options = append(wc.options, frankenphp.WithWorkerScope(f.scope))
107106
f.Workers[i] = wc
108107
}
109108

@@ -253,7 +252,7 @@ func (f *FrankenPHPModule) ServeHTTP(w http.ResponseWriter, r *http.Request, _ c
253252
return
254253
}
255254
if label := f.resolveScopeLabel(srv); label != "" {
256-
frankenphp.SetScopeLabel(f.backgroundScope, label)
255+
frankenphp.SetScopeLabel(f.scope, label)
257256
}
258257
})
259258

@@ -263,7 +262,7 @@ func (f *FrankenPHPModule) ServeHTTP(w http.ResponseWriter, r *http.Request, _ c
263262
opts,
264263
frankenphp.WithOriginalRequest(new(ctx.Value(caddyhttp.OriginalRequestCtxKey).(http.Request))),
265264
frankenphp.WithWorkerName(workerName),
266-
frankenphp.WithRequestScope(f.backgroundScope),
265+
frankenphp.WithRequestScope(f.scope),
267266
)...,
268267
)
269268

context.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,9 @@ type frankenPHPContext struct {
3838
handlerParameters any
3939
handlerReturn any
4040

41-
// backgroundScope selects the background-worker isolation boundary used
42-
// by ensure() calls made from this request. Zero is the global scope.
43-
backgroundScope Scope
41+
// scope selects the per-php_server isolation boundary used by ensure()
42+
// calls made from this request. Zero is the global scope.
43+
scope Scope
4444

4545
done chan any
4646
startedAt time.Time

frankenphp.go

Lines changed: 5 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -157,40 +157,21 @@ func Config() PHPConfig {
157157
func calculateMaxThreads(opt *opt) (numWorkers int, _ error) {
158158
maxProcs := runtime.GOMAXPROCS(0) * 2
159159
maxThreadsFromWorkers := 0
160-
reservedThreads := 0
160+
// Bg workers reserve thread budget separately from HTTP workers so
161+
// they don't double-count against the HTTP-worker admission check
162+
// further down.
163+
reservedThreads := reserveBackgroundWorkerThreads(opt)
161164

162165
for i, w := range opt.workers {
163166
if w.isBackgroundWorker {
164-
// Background workers default to one eager thread for an
165-
// explicit num. For a num=0 declaration, ensure() lazy-starts
166-
// it; we still need to reserve thread budget so getInactivePHPThread
167-
// has somewhere to put it. For catch-all declarations,
168-
// max_threads is the cap on lazy-started instances; reserve
169-
// room for each so ensure() has slots to schedule into.
170-
isCatchAll := w.name == "" || w.name == w.fileName
171-
if w.maxThreads == 0 && w.num == 0 && isCatchAll {
172-
opt.workers[i].maxThreads = defaultMaxBackgroundWorkers
173-
}
174-
extra := w.num
175-
if extra < 1 {
176-
extra = 1
177-
}
178-
if opt.workers[i].maxThreads > extra {
179-
extra = opt.workers[i].maxThreads
180-
}
181-
reservedThreads += extra
182-
// Register the expected worker count for metrics too: without
183-
// this, a bg-worker-only deployment never initialises
184-
// totalWorkers, and StartWorker calls become silent no-ops.
185-
metrics.TotalWorkers(ScopeLabel(w.backgroundScope), w.name, extra)
186167
continue
187168
}
188169

189170
if w.num <= 0 {
190171
// https://github.com/php/frankenphp/issues/126
191172
opt.workers[i].num = maxProcs
192173
}
193-
metrics.TotalWorkers(ScopeLabel(w.backgroundScope), w.name, w.num)
174+
metrics.TotalWorkers(ScopeLabel(w.scope), w.name, w.num)
194175

195176
numWorkers += opt.workers[i].num
196177

options.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ type workerOpt struct {
5151
onServerStartup func()
5252
onServerShutdown func()
5353
isBackgroundWorker bool
54-
backgroundScope Scope
54+
scope Scope
5555
}
5656

5757
// WithContext sets the main context to use.
@@ -273,14 +273,14 @@ func WithWorkerBackground() WorkerOption {
273273
}
274274
}
275275

276-
// EXPERIMENTAL: WithWorkerScope assigns this worker to a given
277-
// background-worker scope. Workers in the same scope share a background
278-
// lookup; each php_server block gets its own scope so workers with the
279-
// same name in different blocks don't collide. The zero value is the
280-
// global/embed scope and is the default.
276+
// EXPERIMENTAL: WithWorkerScope assigns this worker to a given scope.
277+
// Workers in the same scope share a background lookup; each php_server
278+
// block gets its own scope so workers with the same name in different
279+
// blocks don't collide. The zero value is the global/embed scope and is
280+
// the default.
281281
func WithWorkerScope(scope Scope) WorkerOption {
282282
return func(w *workerOpt) error {
283-
w.backgroundScope = scope
283+
w.scope = scope
284284

285285
return nil
286286
}

requestoptions.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -154,13 +154,13 @@ func WithRequestLogger(logger *slog.Logger) RequestOption {
154154
}
155155
}
156156

157-
// WithRequestScope selects the background-worker scope for
158-
// ensure() calls made from this request. Web-server integrations (such
159-
// as the Caddy module) can call NextScope per
160-
// independently-configured block to isolate its set of background workers.
157+
// WithRequestScope selects the scope for ensure() calls made from this
158+
// request. Web-server integrations (such as the Caddy module) can call
159+
// NextScope per independently-configured block to isolate its set of
160+
// background workers.
161161
func WithRequestScope(scope Scope) RequestOption {
162162
return func(o *frankenPHPContext) error {
163-
o.backgroundScope = scope
163+
o.scope = scope
164164

165165
return nil
166166
}

threadbackgroundworker.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ func (handler *backgroundWorkerThread) setupScript() {
136136
if handler.runtimeName == "" {
137137
handler.runtimeName = handler.worker.name
138138
}
139-
metrics.StartWorker(ScopeLabel(handler.worker.backgroundScope), handler.runtimeName)
139+
metrics.StartWorker(ScopeLabel(handler.worker.scope), handler.runtimeName)
140140

141141
opts := append([]RequestOption(nil), handler.worker.requestOptions...)
142142
C.frankenphp_set_background_worker(C._Bool(true))
@@ -185,7 +185,7 @@ func (handler *backgroundWorkerThread) afterScriptExecution(exitStatus int) {
185185
return
186186
}
187187

188-
server := ScopeLabel(worker.backgroundScope)
188+
server := ScopeLabel(worker.scope)
189189

190190
// Cooperative exit: re-run, reset backoff.
191191
if exitStatus == 0 {

threadworker.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ func (handler *workerThread) scopedWorker() *worker { return handler.worker }
110110
func (handler *workerThread) drain() {}
111111

112112
func setupWorkerScript(handler *workerThread, worker *worker) {
113-
metrics.StartWorker(ScopeLabel(worker.backgroundScope), worker.name)
113+
metrics.StartWorker(ScopeLabel(worker.scope), worker.name)
114114

115115
// Create a dummy request to set up the worker
116116
fc, err := newDummyContext(
@@ -149,7 +149,7 @@ func tearDownWorkerScript(handler *workerThread, exitStatus int) {
149149
handler.thread.contextMu.Unlock()
150150
}
151151

152-
server := ScopeLabel(worker.backgroundScope)
152+
server := ScopeLabel(worker.scope)
153153

154154
// on exit status 0 we just run the worker script again
155155
if exitStatus == 0 && !handler.isBootingScript {
@@ -224,7 +224,7 @@ func (handler *workerThread) waitForWorkerRequest() (bool, any) {
224224
}
225225

226226
// worker is truly ready only after reaching frankenphp_handle_request()
227-
metrics.ReadyWorker(ScopeLabel(handler.worker.backgroundScope), handler.worker.name)
227+
metrics.ReadyWorker(ScopeLabel(handler.worker.scope), handler.worker.name)
228228
}
229229

230230
// max_requests reached: signal reboot for full ZTS cleanup

worker.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ type worker struct {
3434
onThreadShutdown func(int)
3535
queuedRequests atomic.Int32
3636
isBackgroundWorker bool
37-
backgroundScope Scope
37+
scope Scope
3838
// backgroundReady is the bg-worker readiness signal shared across the
3939
// worker's threads. Unused on the catch-all template (each lazy-
4040
// spawned name gets its own slot in catchAllNames).
@@ -199,7 +199,7 @@ func newWorker(o workerOpt) (*worker, error) {
199199
onThreadReady: o.onThreadReady,
200200
onThreadShutdown: o.onThreadShutdown,
201201
isBackgroundWorker: o.isBackgroundWorker,
202-
backgroundScope: o.backgroundScope,
202+
scope: o.scope,
203203
}
204204

205205
w.configureMercure(&o)
@@ -350,7 +350,7 @@ func (worker *worker) isAtThreadLimit() bool {
350350
}
351351

352352
func (worker *worker) handleRequest(ch contextHolder) error {
353-
server := ScopeLabel(worker.backgroundScope)
353+
server := ScopeLabel(worker.scope)
354354
metrics.StartWorkerRequest(server, worker.name)
355355

356356
runtime.Gosched()

0 commit comments

Comments
 (0)