@@ -7,17 +7,33 @@ import (
77 "log/slog"
88 "strings"
99 "sync"
10+ "sync/atomic"
1011)
1112
1213// defaultMaxBackgroundWorkers is the default safety cap for catch-all
1314// background workers when the user doesn't set max_threads. Caps the
1415// number of distinct lazy-started instances from a single catch-all.
1516const defaultMaxBackgroundWorkers = 16
1617
17- // backgroundLookup is the registry that resolves a worker name to either
18- // a named declaration or the catch-all. Single global scope in this step;
19- // follow-ups replace this with a per-php_server scope map.
20- var backgroundLookup * backgroundWorkerLookup
18+ // BackgroundScope identifies an isolation boundary for background workers.
19+ // Each php_server block uses a distinct scope so that two blocks can
20+ // declare workers with the same name without conflict. The zero value is
21+ // the global/embed scope (used when no per-block scope was assigned).
22+ // Representation is opaque; obtain values via NextBackgroundWorkerScope.
23+ type BackgroundScope int
24+
25+ var backgroundScopeCounter atomic.Uint64
26+
27+ // NextBackgroundWorkerScope returns a unique scope for background worker
28+ // isolation. Each php_server block should call this once during
29+ // provisioning.
30+ func NextBackgroundWorkerScope () BackgroundScope {
31+ return BackgroundScope (backgroundScopeCounter .Add (1 ))
32+ }
33+
34+ // backgroundLookups maps scopes to their background worker lookup.
35+ // Scope 0 is the global/embed scope; each php_server block gets its own.
36+ var backgroundLookups map [BackgroundScope ]* backgroundWorkerLookup
2137
2238// backgroundWorkerLookup maps worker names to their registry, with a
2339// separate slot for the catch-all (name-less) declaration.
@@ -53,11 +69,20 @@ type backgroundWorkerRegistry struct {
5369 workers map [string ]* worker
5470
5571 // Template options preserved so lazy-started workers inherit the same
56- // env/watch/failure policy as their eagerly-started siblings.
72+ // scope/env/watch/failure policy as their eagerly-started siblings.
73+ scope BackgroundScope
5774 env PreparedEnv
5875 watch []string
5976 maxConsecutiveFailures int
6077 requestOptions []RequestOption
78+
79+ // declared is the pre-existing *worker for a named declaration. It
80+ // lets lazy-start (num=0 named) reuse the already-built worker
81+ // struct without scanning the global workersByName map (which is not
82+ // scope-aware: scoped bg workers sharing a user-facing name would
83+ // otherwise collide). nil for catch-all registries (each lazy-
84+ // started name gets a fresh worker).
85+ declared * worker
6186}
6287
6388func newBackgroundWorkerRegistry (entrypoint string ) * backgroundWorkerRegistry {
@@ -68,18 +93,30 @@ func newBackgroundWorkerRegistry(entrypoint string) *backgroundWorkerRegistry {
6893 }
6994}
7095
71- // buildBackgroundWorkerLookup constructs the name->registry map + catch-all
72- // slot from declared worker options. Each declaration gets its own registry
73- // so shared-entrypoint declarations keep their own template options.
74- func buildBackgroundWorkerLookup (workers []* worker , opts []workerOpt ) * backgroundWorkerLookup {
75- var lookup * backgroundWorkerLookup
96+ // buildBackgroundWorkerLookups constructs a scope->lookup map from declared
97+ // worker options. Each scope (php_server block, or 0 for global/embed)
98+ // gets its own lookup so workers declared with the same name in different
99+ // blocks don't collide. Each declaration gets its own registry so shared-
100+ // entrypoint declarations keep their own template options.
101+ //
102+ // Per-scope namespace collisions are caught here (rather than via the
103+ // global workersByName check in newWorker) because bg workers
104+ // intentionally skip workersByName so two scopes can share a user-facing
105+ // name. That gap means same-scope duplicates wouldn't otherwise be
106+ // detected; same goes for two catch-alls in one scope.
107+ func buildBackgroundWorkerLookups (workers []* worker , opts []workerOpt ) (map [BackgroundScope ]* backgroundWorkerLookup , error ) {
108+ lookups := make (map [BackgroundScope ]* backgroundWorkerLookup )
76109
77110 for i , o := range opts {
78111 if ! o .isBackgroundWorker {
79112 continue
80113 }
81- if lookup == nil {
114+
115+ scope := o .backgroundScope
116+ lookup , ok := lookups [scope ]
117+ if ! ok {
82118 lookup = newBackgroundWorkerLookup ()
119+ lookups [scope ] = lookup
83120 }
84121
85122 w := workers [i ]
@@ -90,14 +127,23 @@ func buildBackgroundWorkerLookup(workers []*worker, opts []workerOpt) *backgroun
90127 // require/__FILE__ identity tests behave differently between the
91128 // two paths.
92129 registry := newBackgroundWorkerRegistry (w .fileName )
130+ registry .scope = scope
93131 registry .env = o .env
94132 registry .watch = o .watch
95133 registry .maxConsecutiveFailures = o .maxConsecutiveFailures
96134 registry .requestOptions = o .requestOptions
97135
136+ w .backgroundScope = scope
98137 phpName := strings .TrimPrefix (w .name , "m#" )
99138 if phpName != "" && phpName != w .fileName {
139+ if _ , exists := lookup .byName [phpName ]; exists {
140+ return nil , fmt .Errorf ("duplicate background worker name %q in the same scope" , phpName )
141+ }
100142 lookup .byName [phpName ] = registry
143+ // Named declaration: remember the *worker so lazy-start can
144+ // reuse it instead of scanning workersByName (which is not
145+ // scope-aware for bg workers).
146+ registry .declared = w
101147 // Eagerly-started named workers (num > 0) register themselves
102148 // so a later ensure() observes the live instance instead of
103149 // trying to spawn a duplicate. Lazy named workers (num == 0)
@@ -106,6 +152,9 @@ func buildBackgroundWorkerLookup(workers []*worker, opts []workerOpt) *backgroun
106152 registry .workers [phpName ] = w
107153 }
108154 } else {
155+ if lookup .catchAll != nil {
156+ return nil , fmt .Errorf ("duplicate catch-all background worker in the same scope" )
157+ }
109158 maxW := defaultMaxBackgroundWorkers
110159 if o .maxThreads > 1 {
111160 maxW = o .maxThreads
@@ -117,22 +166,61 @@ func buildBackgroundWorkerLookup(workers []*worker, opts []workerOpt) *backgroun
117166 w .backgroundRegistry = registry
118167 }
119168
120- return lookup
169+ if len (lookups ) == 0 {
170+ return nil , nil
171+ }
172+ return lookups , nil
173+ }
174+
175+ // getLookup returns the background-worker lookup for the given thread.
176+ // The scope is resolved from the thread's handler (for worker threads
177+ // inheriting their worker's scope) or from the request context (for
178+ // regular HTTP threads with WithRequestBackgroundScope).
179+ //
180+ // If the resolved scope has no workers declared (its lookup is nil), the
181+ // caller falls through to the global/embed scope (0) so that globally-
182+ // declared workers remain reachable from scoped requests. Scopes that
183+ // declared their own workers stay strictly isolated because their lookup
184+ // is non-nil.
185+ func getLookup (thread * phpThread ) * backgroundWorkerLookup {
186+ if backgroundLookups == nil {
187+ return nil
188+ }
189+ var scope BackgroundScope
190+ if thread != nil {
191+ switch handler := thread .handler .(type ) {
192+ case * workerThread :
193+ scope = handler .worker .backgroundScope
194+ case * backgroundWorkerThread :
195+ scope = handler .worker .backgroundScope
196+ default :
197+ if fc , ok := fromContext (thread .context ()); ok {
198+ scope = fc .backgroundScope
199+ }
200+ }
201+ }
202+ if scope != 0 {
203+ if l := backgroundLookups [scope ]; l != nil {
204+ return l
205+ }
206+ }
207+ return backgroundLookups [0 ]
121208}
122209
123210// ensureBackgroundWorker lazy-starts the named worker if it is not already
124211// running. Fire-and-forget: returns once the bg worker thread has been
125212// launched, without waiting for the PHP script to reach any particular
126213// state. Safe to call concurrently; only the first caller actually
127214// allocates the instance, the rest see the existing one.
128- func ensureBackgroundWorker (bgWorkerName string ) error {
215+ func ensureBackgroundWorker (thread * phpThread , bgWorkerName string ) error {
129216 if bgWorkerName == "" {
130217 return fmt .Errorf ("background worker name must not be empty" )
131218 }
132- if backgroundLookup == nil {
219+ lookup := getLookup (thread )
220+ if lookup == nil {
133221 return fmt .Errorf ("no background worker configured" )
134222 }
135- registry := backgroundLookup .resolve (bgWorkerName )
223+ registry := lookup .resolve (bgWorkerName )
136224 if registry == nil || registry .entrypoint == "" {
137225 return fmt .Errorf ("no background worker configured for name %q" , bgWorkerName )
138226 }
@@ -149,12 +237,16 @@ func ensureBackgroundWorker(bgWorkerName string) error {
149237 }
150238
151239 // A num=0 named declaration already created a worker struct at init
152- // time; reuse it instead of creating a duplicate. Catch-all instances
153- // (different names, different worker structs) get freshly built.
240+ // time; reuse it instead of creating a duplicate. Named bg workers
241+ // across distinct scopes share the user-facing PHP name but each has
242+ // its own *worker struct, so we look it up via registry.declared
243+ // rather than the global workersByName map (which is not scope-aware
244+ // for bg workers). For catch-all (registry.declared == nil), each
245+ // lazy-started name gets a fresh *worker.
154246 var w * worker
155247 freshlyBuilt := false
156- if existing := workersByName [ bgWorkerName ]; existing != nil && existing . isBackgroundWorker {
157- w = existing
248+ if registry . declared != nil {
249+ w = registry . declared
158250 } else {
159251 // Clone env and slices: newWorker mutates env (writes
160252 // FRANKENPHP_WORKER) and appends to requestOptions, so sharing
@@ -173,6 +265,7 @@ func ensureBackgroundWorker(bgWorkerName string) error {
173265 fileName : registry .entrypoint ,
174266 num : 1 ,
175267 isBackgroundWorker : true ,
268+ backgroundScope : registry .scope ,
176269 env : env ,
177270 watch : watch ,
178271 maxConsecutiveFailures : registry .maxConsecutiveFailures ,
@@ -185,6 +278,7 @@ func ensureBackgroundWorker(bgWorkerName string) error {
185278 freshlyBuilt = true
186279 }
187280 w .backgroundRegistry = registry
281+ w .backgroundScope = registry .scope
188282 registry .workers [bgWorkerName ] = w
189283 registry .mu .Unlock ()
190284
@@ -200,7 +294,9 @@ func ensureBackgroundWorker(bgWorkerName string) error {
200294 scalingMu .Lock ()
201295 workers = append (workers , w )
202296 scalingMu .Unlock ()
203- workersByName [bgWorkerName ] = w
297+ // Intentionally NOT registered in workersByName: bg workers are
298+ // resolved per-scope via backgroundLookups, not via the global
299+ // name map, so two scopes can share a user-facing name.
204300 }
205301
206302 convertToBackgroundWorkerThread (t , w )
@@ -219,11 +315,11 @@ func ensureBackgroundWorker(bgWorkerName string) error {
219315// this step, so callers cannot block on the worker reaching any state.
220316//
221317//export go_frankenphp_ensure_background_worker
222- func go_frankenphp_ensure_background_worker (name * C.char , nameLen C.size_t ) * C.char {
318+ func go_frankenphp_ensure_background_worker (threadIndex C.uintptr_t , name * C.char , nameLen C.size_t ) * C.char {
319+ thread := phpThreads [threadIndex ]
223320 goName := C .GoStringN (name , C .int (nameLen ))
224- if err := ensureBackgroundWorker (goName ); err != nil {
321+ if err := ensureBackgroundWorker (thread , goName ); err != nil {
225322 return C .CString (err .Error ())
226323 }
227324 return nil
228325}
229-
0 commit comments