Repository navigation
fix(di): key the Injector's circular-dependency guard per thread - #4449
Conversation
The guard tracked in-flight names in a request-scoped struct shared by a request's
cfthreads (on Lucee a cfthread shares its parent's request scope). While one thread was
inside a slow init() of a transient service X — holding X in the resolving stack — another
thread's getInstance("X") saw X already present and threw a spurious
Wheels.DI.CircularDependency ("X -> X"). No cycle existed (#4448).
$getResolvingStack() now keys the stack by thread id (java.lang.Thread.getId(), as Migrator
already does; "0" on a JVM-free engine) within the request-scoped outer struct. That keeps
the per-request semantics that fixed the cross-request cold-start false positive (#2331)
while giving each thread its own set. The lazy creation of the outer struct and each
thread's sub-struct is guarded with lock scope="request" so a request's threads don't race it.
A new InjectorCfthreadResolvingSpec drives the concurrency deterministically:
LatchedService.init() holds the first thread inside init() (via java.util.concurrent
latches) until the second has passed the guard. RED on develop ("Expected [ok] but received
[Wheels.DI.CircularDependency]"), green with the fix; it skips on a JVM-free engine.
Closes #4448
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Peter Amiri <peter@alurium.com>
|
Review: head bb5e02a. Code APPROVE; the final waits on the compat matrix. Cycle detection is unchanged; only the sharing changes.
The locks neither serialize nor deadlock. Both
The thread-id source works on every JVM engine. The RED spec is deterministic (no sleeps):
The RustCFML skip is justified. The spec needs INFO: PR CI and the full compat matrix (37391727495) are pending. The final follows once every leg is read and PR CI is green. |
Wheels Test Results228 724 tests 223 540 ✅ 1h 37m 52s ⏱️ Results for commit bb5e02a. |
|
FINAL: APPROVE. Head bb5e02a.
|
The bug
The Injector's circular-dependency guard tracked in-flight names in a request-scoped struct (
request.$wheelsDIResolving). On Lucee acfthreadshares its parent's request scope, so a request's threads shared one guard. While thread A was inside a slowinit()of transient service X — holdingXin the resolving stack — thread B'sgetInstance("X")sawXalready present and threw a spuriousWheels.DI.CircularDependency(Resolution chain: X -> X). No cycle existed (#4448).Fix
$getResolvingStack()now keys the stack by thread id within the request-scoped outer struct:java.lang.Thread.currentThread().getId()(the same techniqueMigrator.$migrationLockThreadId()uses), falling back to"0"on a JVM-free engine;lock scope="request"so a request's own threads don't race it (request-scoped, so no cross-request contention).Test
The core runner can't see
cfthreadconcurrency through an ordinary spec, soInjectorCfthreadResolvingSpecdrives it deterministically (no timing sleeps):LatchedService.init()holds the first thread insideinit()viajava.util.concurrentlatches until the second thread has passed the guard, then releases it.Expected [ok] but received [Wheels.DI.CircularDependency].Verification
Shared plumbing (DI is used everywhere) + thread-scoping differs per engine, so the cross-engine matrix is warranted.
Closes #4448
🤖 Generated with Claude Code