Under heavy parallel load (14 concurrent test processes + 14 CPU burners, with
DOTNET_ThreadPool_ForceMinWorkerThreads=1 as the reproducer) the Galaxy tests
with 1-3 s fixed waits failed 16/70 runs. Every failure was a background loop
started with Task.Run (alarm feed, DeployWatcher, EventPump, ReconnectSupervisor)
not being SCHEDULED within the ceiling - e.g. DeployWatcher's stream had not even
been opened after 2 s. No production race: the code under test was checked for
each (alarm feed dispatch is synchronous in-order, DeployWatcher/ReconnectSupervisor
state is lock- or loop-local, subscribe registers before returning).
- New shared TestWait (UntilAsync / ForAsync / HealthyAsync): 30 s presence
ceiling, failure reports elapsed ms + an observed-state snapshot (#856 shape;
GalaxyDriverFixRound1Tests.WaitUntilAsync now delegates to it).
- Converted the short waits in GatewayGalaxyAlarmFeedTests, DeployWatcherTests,
GalaxyDriverSubscribeTests, GalaxyDriverRawPathKeyingTests (its WaitForAsync
never threw on timeout), EventPumpStreamFaultTests, EventPumpReplayGapTests,
ReconnectSupervisorTests; FixRound2 InitBudget 2 s -> 10 s (the parked fetch
never completes, so any finite bound keeps the verdict).
- Absence by short sleep replaced with positive controls: DeployWatcher bootstrap
and same-deploy-time (follow with a real change, assert exactly that one raise -
verified red when suppression is removed), subscribe failed-tag / unsubscribed
handle (follow with a live handle's event), faulted pump (wait for its
onStreamFault instead of 100 ms).
- Fixed sleeps that raced scheduling: subscribe-once read tests wait for the
read's registration (TrackedSubscriptionCountForTests) instead of 50 ms;
supervisor Dispose test waits for reopen entry instead of 50 ms; the
already-Healthy WaitForHealthy test asserts synchronous completion instead of a
wall-clock bound. FakeSubscriber.Map is now a ConcurrentDictionary.
Loaded run (14 parallel x 5, 14 burners, ForceMinWorkerThreads=1):
before 16/70 failed runs; after 0/350 (70 + 210 + 70). Without the thread-pool
override: 0/70 before and after on this host. Serial: 472 passed, 8 skipped.
Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
PerPlatformProbeWatcher records a platform's handle and publishes the probe
binding into the driver registry as one step under _syncLock, but
WatchedPlatforms read the handle map lock-free - so it could report a
platform one step ahead of the registry Upsert. A ScanState event raised in
that window was dispatched by the EventPump, missed ResolveSubscribers and
was dropped as "stale after unsubscribe", leaving the host status empty.
That is the #856 flake: A_fetch_completing_after_init_populates_host_statuses
waited for WatchedPlatforms, emitted ScanState, and timed out at 10 s on the
final Running wait. Reproduced 11/700 (28 parallel runners x 25 + 14 CPU
burners), every failure at that step with the binding present by the time of
diagnosis and hostStatuses=[] - i.e. the event was dropped. 0/1400 after.
WatchedPlatforms now reads under _syncLock (test seam only; the dispatch path
already checks the map after the registry, the safe order). New
PerPlatformProbeWatcherVisibilityTests spins a reader on another thread and
asserts the registry entry exists the moment the platform is visible
(old code: torn in 1691/2000 iterations). WaitUntilAsync gains a describe
overload; the #856 test's waits now report elapsed time and the observed
seed/binding/pump/host-status chain on timeout.
Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
Review fix round 2 for lane F:
- N1: the init platform seed no longer runs inline under the init token. Galaxy's Tier-A
Initialize budget is 5 s, and a cold gateway DiscoverHierarchy can spend all of it, so the
OperationCanceledException rethrow failed init, opened the Initialize breaker and cost a full
rebuild per retry. InitializeAsync now only resolves the platform source (no network) and
queues a background seed tied to a per-runtime CTS that the teardown cancels. A result that
lands after its runtime was torn down is discarded.
- N2: the seed worker serves the runtime that is live when each pass STARTS, so a request made
under a new runtime while a stale pass is in flight always gets a pass of its own.
- N3: SubscribeAsync and the subscribe-once read capture the runtime generation before
SubscribeBulk and register only if it is unchanged, atomically with the teardown's registry
Clear (_registryGate). On a mismatch they release the handles and fail the call. Re-subscribe
Rebind uses the same gate, and the production rebuild clears the registry unconditionally.
- PerPlatformProbeWatcher re-checks its stop after the SubscribeBulk await, atomically with
the record and publish. A pass that straddles StopAsync releases the handles it acquired
instead of writing the probe entry back into a cleared registry.
Refs #735
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT