Skip to content

Commit 2ddffc2

Browse files
review: address dunglas's review batch
- BackgroundScope -> Scope (and NextScope / WithRequestScope / WithWorkerScope), keeping the type opaque + reusable for other per-server isolation contexts (Mercure hubs, future Prometheus labels). No behaviour change. - $_SERVER['FRANKENPHP_WORKER'] now carries the user-facing worker name instead of "1". The doc has always told callers to rely on presence, not value. Drops the parallel FRANKENPHP_WORKER_NAME injection. - backgroundWorkerReady.abortErr keeps the full error (not just its message), so the wrap chain survives into ensure() callers. - Cap-error wording: drop the Caddyfile-specific "max_threads on the catch-all" phrasing for "increase max threads or declare it as a named worker", to read cleanly when the user is on the JSON config. - Constants merged into a single block; getLookup folds the worker / background-worker handler cases via a small anonymous interface (Go's type switch can't fan out type-asserted access otherwise). - Stub doc nits: third-person godoc ("Declares" / "Returns"). - Test ergonomics: requireSentinelEventually wraps the duplicated Eventually glue; require.NoFileExists, require.ErrorContains, assert.WithinDuration replace ad-hoc combinations. - Renamed background_worker_*_test.go to bgworker*_test.go to match the bgworker.go source file (no underscores in file-name prefixes).
1 parent 03460db commit 2ddffc2

16 files changed

Lines changed: 111 additions & 108 deletions

bgworker.go

Lines changed: 34 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,16 @@ import (
1313
"unsafe"
1414
)
1515

16-
// defaultMaxBackgroundWorkers is the default safety cap for catch-all
17-
// background workers when the user doesn't set max_threads. Caps the
18-
// number of distinct lazy-started instances from a single catch-all.
19-
const defaultMaxBackgroundWorkers = 16
20-
21-
// defaultEnsureTimeout is the default deadline applied when ensure() is
22-
// called without an explicit timeout.
23-
const defaultEnsureTimeout = 30 * time.Second
16+
const (
17+
// defaultMaxBackgroundWorkers is the default safety cap for catch-all
18+
// background workers when the user doesn't set max_threads. Caps the
19+
// number of distinct lazy-started instances from a single catch-all.
20+
defaultMaxBackgroundWorkers = 16
21+
22+
// defaultEnsureTimeout is the default deadline applied when ensure() is
23+
// called without an explicit timeout.
24+
defaultEnsureTimeout = 30 * time.Second
25+
)
2426

2527
// backgroundWorkerReady is the per-instance readiness signal ensure()
2628
// blocks on. ready closes once the worker calls
@@ -32,7 +34,7 @@ type backgroundWorkerReady struct {
3234

3335
aborted chan struct{}
3436
abortOnce sync.Once
35-
abortErr string
37+
abortErr error
3638

3739
// bootFailure carries the most recent pre-readiness crash so a timing-
3840
// out ensure() can report it instead of a generic "didn't call
@@ -67,26 +69,26 @@ func (r *backgroundWorkerReady) markReady() {
6769
// (max_consecutive_failures hit before readiness). Idempotent.
6870
func (r *backgroundWorkerReady) abort(err error) {
6971
r.abortOnce.Do(func() {
70-
r.abortErr = err.Error()
72+
r.abortErr = err
7173
close(r.aborted)
7274
})
7375
}
7476

75-
// BackgroundScope is an opaque per-php_server isolation boundary; the
77+
// Scope is an opaque per-php_server isolation boundary; the
7678
// zero value is the global/embed scope. Obtain values via
77-
// NextBackgroundWorkerScope.
78-
type BackgroundScope uint64
79+
// NextScope.
80+
type Scope uint64
7981

80-
var backgroundScopeCounter atomic.Uint64
82+
var scopeCounter atomic.Uint64
8183

82-
// NextBackgroundWorkerScope returns a fresh scope value. Each php_server
84+
// NextScope returns a fresh scope value. Each php_server
8385
// block should call this once during provisioning.
84-
func NextBackgroundWorkerScope() BackgroundScope {
85-
return BackgroundScope(backgroundScopeCounter.Add(1))
86+
func NextScope() Scope {
87+
return Scope(scopeCounter.Add(1))
8688
}
8789

8890
// backgroundLookups maps scope -> lookup. Scope 0 is the global/embed scope.
89-
var backgroundLookups map[BackgroundScope]*backgroundWorkerLookup
91+
var backgroundLookups map[Scope]*backgroundWorkerLookup
9092

9193
// backgroundWorkerLookup resolves a user-facing worker name to its *worker;
9294
// catchAll is the fallback when byName misses.
@@ -113,8 +115,8 @@ func (l *backgroundWorkerLookup) resolve(name string) *worker {
113115
// lookup. Per-scope name collisions are caught here because bg workers
114116
// intentionally skip the global workersByName map (so two scopes can share
115117
// a user-facing name).
116-
func buildBackgroundWorkerLookups(workers []*worker, opts []workerOpt) (map[BackgroundScope]*backgroundWorkerLookup, error) {
117-
lookups := make(map[BackgroundScope]*backgroundWorkerLookup)
118+
func buildBackgroundWorkerLookups(workers []*worker, opts []workerOpt) (map[Scope]*backgroundWorkerLookup, error) {
119+
lookups := make(map[Scope]*backgroundWorkerLookup)
118120

119121
for i, o := range opts {
120122
if !o.isBackgroundWorker {
@@ -171,17 +173,15 @@ func getLookup(thread *phpThread) *backgroundWorkerLookup {
171173
if backgroundLookups == nil {
172174
return nil
173175
}
174-
var scope BackgroundScope
176+
var scope Scope
175177
if thread != nil {
176-
switch handler := thread.handler.(type) {
177-
case *workerThread:
178-
scope = handler.worker.backgroundScope
179-
case *backgroundWorkerThread:
180-
scope = handler.worker.backgroundScope
181-
default:
182-
if fc, ok := fromContext(thread.context()); ok {
183-
scope = fc.backgroundScope
184-
}
178+
// Both *workerThread and *backgroundWorkerThread expose their *worker
179+
// via this anonymous interface; classic-mode threads don't and fall
180+
// through to the request-context path.
181+
if h, ok := thread.handler.(interface{ scopedWorker() *worker }); ok {
182+
scope = h.scopedWorker().backgroundScope
183+
} else if fc, ok := fromContext(thread.context()); ok {
184+
scope = fc.backgroundScope
185185
}
186186
}
187187
if scope != 0 {
@@ -228,13 +228,13 @@ func ensureBackgroundWorker(thread *phpThread, bgWorkerName string, timeout time
228228

229229
if catchAll.catchAllCap > 0 && len(catchAll.catchAllNames) >= catchAll.catchAllCap {
230230
catchAll.catchAllMu.Unlock()
231-
return fmt.Errorf("cannot start background worker %q: limit of %d reached (increase max_threads on the catch-all background worker or declare it as a named worker)", bgWorkerName, catchAll.catchAllCap)
231+
return fmt.Errorf("cannot start background worker %q: limit of %d reached (increase max threads or declare it as a named worker)", bgWorkerName, catchAll.catchAllCap)
232232
}
233233

234234
t := getInactivePHPThread()
235235
if t == nil {
236236
catchAll.catchAllMu.Unlock()
237-
return fmt.Errorf("no available PHP thread for background worker (increase max_threads)")
237+
return fmt.Errorf("no available PHP thread for background worker (increase max threads)")
238238
}
239239

240240
r := newBackgroundWorkerReady()
@@ -264,7 +264,7 @@ func lazyStartNamedWorker(w *worker) error {
264264
}
265265
t := getInactivePHPThread()
266266
if t == nil {
267-
return fmt.Errorf("no available PHP thread for background worker (increase max_threads)")
267+
return fmt.Errorf("no available PHP thread for background worker (increase max threads)")
268268
}
269269
convertToBackgroundWorkerThread(t, w, w.name, w.backgroundReady)
270270
w.bgLazyStarted = true
@@ -287,7 +287,7 @@ func waitForBackgroundWorkerReady(name string, r *backgroundWorkerReady, timeout
287287
case <-r.ready:
288288
return nil
289289
case <-r.aborted:
290-
return fmt.Errorf("background worker %q failed to start: %s", name, r.abortErr)
290+
return fmt.Errorf("background worker %q failed to start: %w", name, r.abortErr)
291291
case <-timer.C:
292292
return formatEnsureTimeoutError(name, r, timeout)
293293
}
Lines changed: 31 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,19 @@ import (
1212
"github.com/stretchr/testify/require"
1313
)
1414

15-
// sentinelExists is a predicate suitable for assert.Eventually that returns
16-
// true once the named file appears on disk.
17-
func sentinelExists(path string) func() bool {
18-
return func() bool {
15+
// requireSentinelEventually asserts that `path` appears on disk before the
16+
// deadline. Wraps require.Eventually so call sites stay short.
17+
func requireSentinelEventually(t testing.TB, path string, msgAndArgs ...any) {
18+
t.Helper()
19+
require.Eventually(t, func() bool {
1920
_, err := os.Stat(path)
2021
return err == nil
21-
}
22+
}, 5*time.Second, 10*time.Millisecond, msgAndArgs...)
2223
}
2324

2425
// TestEnsureBackgroundWorkerNamedLazy declares a num=0 named worker, then
2526
// calls ensure() to lazy-start it. The fixture writes a sentinel named
26-
// after FRANKENPHP_WORKER_NAME so we can confirm the right instance ran.
27+
// after FRANKENPHP_WORKER so we can confirm the right instance ran.
2728
func TestEnsureBackgroundWorkerNamedLazy(t *testing.T) {
2829
cwd, _ := os.Getwd()
2930
testDataDir := cwd + "/testdata/"
@@ -40,12 +41,10 @@ func TestEnsureBackgroundWorkerNamedLazy(t *testing.T) {
4041
t.Cleanup(Shutdown)
4142

4243
// num=0 means no eager start: the sentinel should not exist yet.
43-
_, err := os.Stat(filepath.Join(tmp, "bg-lazy"))
44-
require.True(t, os.IsNotExist(err), "lazy worker should not have started yet")
44+
require.NoFileExists(t, filepath.Join(tmp, "bg-lazy"), "lazy worker should not have started yet")
4545

4646
require.NoError(t, ensureBackgroundWorker(nil, "bg-lazy", 5*time.Second))
47-
assert.Eventually(t, sentinelExists(filepath.Join(tmp, "bg-lazy")),
48-
5*time.Second, 10*time.Millisecond,
47+
requireSentinelEventually(t, filepath.Join(tmp, "bg-lazy"),
4948
"ensure() should have lazy-started the named bg worker")
5049
}
5150

@@ -73,8 +72,7 @@ func TestEnsureBackgroundWorkerCatchAll(t *testing.T) {
7372
}
7473

7574
for _, name := range []string{"job-a", "job-b"} {
76-
assert.Eventually(t, sentinelExists(filepath.Join(tmp, name)),
77-
5*time.Second, 10*time.Millisecond,
75+
requireSentinelEventually(t, filepath.Join(tmp, name),
7876
"catch-all instance %q should have written its sentinel", name)
7977
}
8078
}
@@ -100,9 +98,10 @@ func TestEnsureBackgroundWorkerCatchAllCap(t *testing.T) {
10098
require.NoError(t, ensureBackgroundWorker(nil, "cap-a", 5*time.Second))
10199
require.NoError(t, ensureBackgroundWorker(nil, "cap-b", 5*time.Second))
102100

103-
err := ensureBackgroundWorker(nil, "cap-c", 5*time.Second)
104-
require.Error(t, err, "third ensure must hit the catch-all cap")
105-
assert.Contains(t, err.Error(), "limit of 2 reached")
101+
require.ErrorContains(t,
102+
ensureBackgroundWorker(nil, "cap-c", 5*time.Second),
103+
"limit of 2 reached",
104+
"third ensure must hit the catch-all cap")
106105
}
107106

108107
// TestEnsureBackgroundWorkerUndeclared confirms ensure() on an undeclared
@@ -119,9 +118,9 @@ func TestEnsureBackgroundWorkerUndeclared(t *testing.T) {
119118
))
120119
t.Cleanup(Shutdown)
121120

122-
err := ensureBackgroundWorker(nil, "other-name", 5*time.Second)
123-
require.Error(t, err)
124-
assert.Contains(t, err.Error(), "no background worker configured for name")
121+
require.ErrorContains(t,
122+
ensureBackgroundWorker(nil, "other-name", 5*time.Second),
123+
"no background worker configured for name")
125124
}
126125

127126
// TestEnsureBackgroundWorkerConcurrent confirms the doc claim that ensure()
@@ -189,15 +188,16 @@ func TestEnsureBackgroundWorkerTimeout(t *testing.T) {
189188

190189
start := time.Now()
191190
err := ensureBackgroundWorker(nil, "bg-no-handle", 1*time.Second)
192-
elapsed := time.Since(start)
191+
deadline := start.Add(1 * time.Second)
193192

194-
require.Error(t, err, "ensure() must time out when the worker never calls frankenphp_get_worker_handle()")
195-
assert.Contains(t, err.Error(), "did not call frankenphp_get_worker_handle()", "timeout error must point at the readiness boundary")
193+
require.ErrorContains(t, err,
194+
"did not call frankenphp_get_worker_handle()",
195+
"ensure() must time out at the readiness boundary")
196196
// Lower bound: timer must actually have run; allow a little slop for
197-
// timer scheduling. Upper bound: hard cap so a regression that lets
198-
// ensure() block forever doesn't hang the suite.
199-
assert.GreaterOrEqual(t, elapsed, 900*time.Millisecond, "ensure() must wait the full timeout")
200-
assert.Less(t, elapsed, 5*time.Second, "ensure() must return within a small slack window after the timeout")
197+
// timer scheduling.
198+
assert.GreaterOrEqual(t, time.Since(start), 900*time.Millisecond, "ensure() must wait the full timeout")
199+
// Upper bound: ensure() returned close to the deadline (didn't hang).
200+
assert.WithinDuration(t, deadline, time.Now(), 4*time.Second, "ensure() must return within a small slack window after the timeout")
201201
}
202202

203203
// TestEnsureBackgroundWorkerBootFailure declares a worker whose entrypoint
@@ -221,9 +221,10 @@ func TestEnsureBackgroundWorkerBootFailure(t *testing.T) {
221221
err := ensureBackgroundWorker(nil, "bg-boot-fail", 5*time.Second)
222222
require.Error(t, err, "ensure() must surface the boot failure")
223223
msg := err.Error()
224-
// One of two paths: either the abort fired (cap reached) and the
225-
// error mentions max_consecutive_failures, or the timeout fired with
226-
// the bootFailureInfo attached so the message mentions exit status.
227-
matches := strings.Contains(msg, "exit status") || strings.Contains(msg, "max_consecutive_failures") || strings.Contains(msg, "failed to start")
228-
assert.True(t, matches, "ensure() error must reflect the boot crash, got: %s", msg)
224+
// One of two paths: either the abort fired (cap reached) and the error
225+
// mentions max_consecutive_failures, or the timeout fired with the
226+
// bootFailureInfo attached so the message mentions exit status.
227+
assert.True(t,
228+
strings.Contains(msg, "exit status") || strings.Contains(msg, "max_consecutive_failures") || strings.Contains(msg, "failed to start"),
229+
"ensure() error must reflect the boot crash, got: %s", msg)
229230
}
Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,16 +46,12 @@ func TestBackgroundWorkerRestartForceKillsStuckThread(t *testing.T) {
4646
// when the drain fires - that's the only way to prove the
4747
// force-kill code path was exercised, not the stop-pipe EOF path
4848
// (the fixture doesn't open the stop pipe at all).
49-
require.Eventually(t, func() bool {
50-
_, err := os.Stat(sentinel)
51-
return err == nil
52-
}, 5*time.Second, 25*time.Millisecond, "bg worker never entered sleep()")
49+
requireSentinelEventually(t, sentinel, "bg worker never entered sleep()")
5350

5451
start := time.Now()
5552
RestartWorkers()
56-
elapsed := time.Since(start)
57-
58-
// Test grace period (2s) + slack for signal dispatch and drain completion.
59-
const budget = 5 * time.Second
60-
assert.Less(t, elapsed, budget, "drain must force-kill the stuck bg worker within the grace period")
53+
// Drain budget = grace period (2s) + slack for signal dispatch and
54+
// drain completion.
55+
assert.WithinDuration(t, start, time.Now(), 5*time.Second,
56+
"drain must force-kill the stuck bg worker within the grace period")
6157
}
Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ import (
1111
"github.com/stretchr/testify/require"
1212
)
1313

14-
// TestNextBackgroundWorkerScopeIsDistinct verifies the scope counter
14+
// TestNextScopeIsDistinct verifies the scope counter
1515
// hands out unique values on consecutive calls.
16-
func TestNextBackgroundWorkerScopeIsDistinct(t *testing.T) {
17-
a := NextBackgroundWorkerScope()
18-
b := NextBackgroundWorkerScope()
16+
func TestNextScopeIsDistinct(t *testing.T) {
17+
a := NextScope()
18+
b := NextScope()
1919
assert.NotEqual(t, a, b, "consecutive scopes must differ")
2020
assert.NotZero(t, a, "scopes must be non-zero (zero is the global scope)")
2121
assert.NotZero(t, b, "scopes must be non-zero (zero is the global scope)")
@@ -29,20 +29,20 @@ func TestBackgroundWorkerSameNameDifferentScope(t *testing.T) {
2929
cwd, _ := os.Getwd()
3030
testDataDir := cwd + "/testdata/"
3131

32-
scopeA := NextBackgroundWorkerScope()
33-
scopeB := NextBackgroundWorkerScope()
32+
scopeA := NextScope()
33+
scopeB := NextScope()
3434

3535
tmp := t.TempDir()
3636

3737
require.NoError(t, Init(
3838
WithWorkers("shared", testDataDir+"background-worker-named.php", 1,
3939
WithWorkerBackground(),
40-
WithWorkerBackgroundScope(scopeA),
40+
WithWorkerScope(scopeA),
4141
WithWorkerEnv(map[string]string{"BG_SENTINEL_DIR": tmp + "/a"}),
4242
),
4343
WithWorkers("shared", testDataDir+"background-worker-named.php", 1,
4444
WithWorkerBackground(),
45-
WithWorkerBackgroundScope(scopeB),
45+
WithWorkerScope(scopeB),
4646
WithWorkerEnv(map[string]string{"BG_SENTINEL_DIR": tmp + "/b"}),
4747
),
4848
WithNumThreads(4),
@@ -68,8 +68,8 @@ func TestBackgroundWorkerCatchAllPerScope(t *testing.T) {
6868
cwd, _ := os.Getwd()
6969
testDataDir := cwd + "/testdata/"
7070

71-
scopeA := NextBackgroundWorkerScope()
72-
scopeB := NextBackgroundWorkerScope()
71+
scopeA := NextScope()
72+
scopeB := NextScope()
7373

7474
tmp := t.TempDir()
7575
dirA := filepath.Join(tmp, "a")
@@ -80,12 +80,12 @@ func TestBackgroundWorkerCatchAllPerScope(t *testing.T) {
8080
require.NoError(t, Init(
8181
WithWorkers("", testDataDir+"background-worker-named.php", 0,
8282
WithWorkerBackground(),
83-
WithWorkerBackgroundScope(scopeA),
83+
WithWorkerScope(scopeA),
8484
WithWorkerEnv(map[string]string{"BG_SENTINEL_DIR": dirA}),
8585
),
8686
WithWorkers("", testDataDir+"background-worker-named.php", 0,
8787
WithWorkerBackground(),
88-
WithWorkerBackgroundScope(scopeB),
88+
WithWorkerScope(scopeB),
8989
WithWorkerEnv(map[string]string{"BG_SENTINEL_DIR": dirB}),
9090
),
9191
WithNumThreads(4),
@@ -121,8 +121,7 @@ func TestBackgroundWorkerCatchAllPerScope(t *testing.T) {
121121

122122
// Wait for the worker to write its sentinel so we know the right
123123
// fixture ran (sanity check that env was inherited from scope A).
124-
assert.Eventually(t, sentinelExists(filepath.Join(dirA, "job-a")),
125-
3*time.Second, 10*time.Millisecond,
124+
requireSentinelEventually(t, filepath.Join(dirA, "job-a"),
126125
"scope A catch-all instance must write its sentinel under scope A's BG_SENTINEL_DIR")
127126
}
128127

caddy/module.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ type FrankenPHPModule struct {
5151
preparedEnvNeedsReplacement bool
5252
logger *slog.Logger
5353
requestOptions []frankenphp.RequestOption
54-
backgroundScope frankenphp.BackgroundScope
54+
backgroundScope frankenphp.Scope
5555
}
5656

5757
// CaddyModule returns the Caddy module information.
@@ -84,7 +84,7 @@ func (f *FrankenPHPModule) Provision(ctx caddy.Context) error {
8484
// collide. Provision can be called more than once for the same
8585
// module; only assign once.
8686
if f.backgroundScope == 0 {
87-
f.backgroundScope = frankenphp.NextBackgroundWorkerScope()
87+
f.backgroundScope = frankenphp.NextScope()
8888
}
8989

9090
loggerOpt := frankenphp.WithRequestLogger(f.logger)
@@ -101,7 +101,7 @@ func (f *FrankenPHPModule) Provision(ctx caddy.Context) error {
101101
}
102102

103103
wc.requestOptions = append(wc.requestOptions, loggerOpt)
104-
wc.options = append(wc.options, frankenphp.WithWorkerBackgroundScope(f.backgroundScope))
104+
wc.options = append(wc.options, frankenphp.WithWorkerScope(f.backgroundScope))
105105
f.Workers[i] = wc
106106
}
107107

@@ -251,7 +251,7 @@ func (f *FrankenPHPModule) ServeHTTP(w http.ResponseWriter, r *http.Request, _ c
251251
opts,
252252
frankenphp.WithOriginalRequest(new(ctx.Value(caddyhttp.OriginalRequestCtxKey).(http.Request))),
253253
frankenphp.WithWorkerName(workerName),
254-
frankenphp.WithRequestBackgroundScope(f.backgroundScope),
254+
frankenphp.WithRequestScope(f.backgroundScope),
255255
)...,
256256
)
257257

0 commit comments

Comments
 (0)