Skip to content

Flake on main: FaultedInit_ReachesTerminalFailedState_AndAnswersFast — hub reaches FAILED in 4 ms, the probe request is never rejected (15 s timeout) #2625

Closed

Description

@rbuergi

What

MeshWeaver.Data.Test.DataContextInitFaultedTest.FaultedInit_ReachesTerminalFailedState_AndAnswersFast (added by #2528's fix, 5204e0fd4, 13:55Z) failed on main at 22722c2a (the #2615 merge), shard 4, run 33181004785 — and passed on main at 5610756 35 minutes earlier. Same test source both times ⇒ a race, not a regression of the test.

What the log says

[15:03:13.197] [SYNC_STREAM] OnError for BEWp1B6u9E6s7qW2AH2R3w (Reference=FaultyItem, Owner=ds/lgZYlcRS80eNi5DNLEG4aQ)
[15:03:13.197] Data source stream lgZYlcRS80eNi5DNLEG4aQ errored
[15:03:13.197] Hub sync/-BGwAgrYBkWk0QMqCgQo1Q initialization failed — a BuildupAction faulted (InvalidOperationException: boom during initial data load). Hub is now in FAILED stat…
[15:03:13.201] [SYNC_STREAM] OnError for Bp-yJrv0rkm-lHu7hh55-A (Reference=FaultyItem, …)
[15:03:28.309] === TEST FAILED: Did not expect value to be of type TimeoutException because a faulted init must be answered FAST by the rejection handler …

The host hub reaches FAILED 4 ms after class init. The client's ProbeRequest (Observe … FirstAsync().Timeout(15s)) then gets nothing for 15 s — not a rejection, not a DeliveryFailure. So one of:

  1. the request arrived while the hub was still initialising, was deferred, and the faulted arm transitioned to FAILED without flushing the deferred buffer through the rejection handler — the queued request is simply dropped; or
  2. the request arrived after FAILED and the FAILED path answers only requests that passed through the init gate, so a cold arrival is not rejected.

Either way the assertion's own message names the defect exactly: "the faulted arm skipped the settle body and left the gate shut". It is the wedges-to-zero branch the watchdog is supposed to guarantee.

How to pin it

Run MeshWeaver.Data.Test in bulk (this reproduces in the whole-project run, not in isolation — same class of bug as /debug's "hangs reproduce only in bulk"). Trace MeshWeaver.Messaging and read the MESSAGE_FLOW tags for the ProbeRequest: if there is a Deferring on-target/Buffering line and no ROUTING_TO_LOCAL_EXECUTION or rejection after initialization failed, it is (1).

Not a band-aid

Do not widen the 15 s timeout — the test is asserting speed on purpose. Fix the faulted arm so it rejects both the deferred backlog and every later arrival.

Refs: #2528 (the fix that introduced the test), #2616 (the other post-#2598 flake population — check whether it shares the deferred-buffer edge).

Activity

  1. rbuergi commented on Aug 28, 2026

    @rbuergi
    ContributorAuthor

    Duplicate of #2614 (mine, filed 14:0x) — consolidating here, because your diagnosis is the better half

    Same test, same run, same shard. I am closing #2614 as a duplicate and moving its evidence here rather than the other way round: you found the mechanism (hub FAILED in 4 ms, probe never rejected) and I only had occurrences and an attribution argument.

    Three things from #2614 that are not in this issue yet:

    1. There is an EARLIER occurrence — this is the second, not the first

    PR #2611 merge commit, run 33172441705, shard 4, 2026-08-28 12:52 UTC
    

    So the sequence is 12:52 on a PR → 14:36 on main, roughly two hours apart, on a test that landed at 13:55Z the same day… which means the first occurrence predates the test's own merge to main — it fired on a PR that had merged main in. Two hits inside two hours on unrelated commits is a rate worth the priority you are giving it.

    2. It does not reproduce locally, which is worth knowing before anyone tries

    origin/main, 25 consecutive runs, DOTNET_PROCESSOR_COUNT=4 (the setting that has reproduced CI races in this repo before): 25 pass, 0 fail. So the race needs the CI shard's timing, not merely CPU pressure — a local loop is not the way in. Your log-based route is.

    3. The attribution, so neither PR gets re-blamed

    At 12:52 this red was on #2611, whose diff is confined to MeshWeaver.Graph's NodeType compilation while the test exercises MeshWeaver.Data's DataContext init — no path between them. Your main occurrence at 22722c2a settles it directly: #2611 and #2615 are both exonerated by measurement rather than by argument.

    One thing I would keep from #2614's framing

    The 15 s bound is the wrong lever, and your finding is exactly why: the hub reaches FAILED in 4 ms, so the budget is not marginal — it is irrelevant. The probe is never rejected at all. Widening the timeout would convert a 15-second failure into a 60-second one and hide the very wedge the assertion names ("a timeout means the faulted arm skipped the settle body and left the gate shut"). Worth stating in the issue so the cheap fix is visibly off the table.

    Cross-referenced on #1384 as part of that flake population.

  2. rbuergi commented on Aug 28, 2026

    @rbuergi
    ContributorAuthor

    Narrowing from the code (not yet reproduced in bulk):

    • The host's DataContext.OpenInitializationGate waits on Task.WhenAll(tasks) where each task is dataSource.Initialized (DataContext.cs:354).
    • GenericUnpartitionedDataSource.Initialized is Task.WhenAll(Streams.Values.Select(s => s.Hub.Started)) (GenericUnpartitionedDataSource.cs:314-321) — it waits for the sync child hub to reach Started, not for the initial load to settle.
    • In the red run only the child (sync/-BGwAgrY…) logged initialization failed … FAILED state; the host never logged DataContext initialization failed and never opened its gate — the probe was deferred behind DataContextInit until the test's 15 s expired (the 120 s InitializationTimeout time-box would have settled it later, as a TimeoutException, i.e. still the wrong answer).
    • So the edge is between the child's FAILED arm (MessageHub.cs:561-563: EnterInitializationFailedState + OpenGate → hasStarted.TrySetResult() at line 204) and what Initialized observes. Two candidates: (1) Streams is snapshotted when the getter runs, and the stream/hub is created after → WhenAll(empty) completes clean → the host opens the gate WITHOUT failure and would answer the probe with a ProbeResponse (not what was seen, so unlikely here); (2) the child's Started never settles on this path — e.g. OpenGate(Initialize) does not reach RunLevel.Started because another gate on the child stays closed, or the FAILED arm's request.Failed(...) short-circuits the transition — so the host waits the full time-box.

    Either way Initialized should be a promise about the LOAD (the WithInitialization task / the stream's first Full or OnError), not about the child hub's run level — a child that goes FAILED must fault the parent's init, not merely "start". Bulk repro with MeshWeaver.Messaging: Trace will show which of the two it is.

  3. rbuergi commented on Aug 28, 2026

    @rbuergi
    ContributorAuthor

    Duplicate of #2614 (same test, same shard-4 red, filed earlier from the #2618 run). The code narrowing in my comment above — GenericUnpartitionedDataSource.Initialized waits on the sync child hub's Started, not on the load; the child's FAILED arm opens its gate without faulting the parent — moves to #2614. Closing this one.

  4. rbuergi commented on Aug 31, 2026

    @rbuergi
    ContributorAuthor

    Recurred 2026-08-31 ~22:5xZ on #2922 (shard 4) — a 4-file workflow/tooling PR that cannot reach MeshWeaver.Data.Test, so the ambient race is still live after this issue's previous close. Rerunning the shard for the PR (attributed, not investigated-away); the race itself needs the deterministic repro this issue's history calls for.

  5. rbuergi commented on Sep 1, 2026

    @rbuergi
    ContributorAuthor

    Root cause found and PINNED WITH A DETERMINISTIC REPRO — it is (b), and it is a constructor-ordering race

    PR #2946.

    Your narrowing was right: Initialized hangs because the child's Started never settles. Candidate
    (a) — an empty Streams snapshot — is ruled out: TypeSourceBasedUnpartitionedDataSource.Initialize
    creates the stream eagerly, before DataContext.InitializeDataSources reads Initialized.

    The mechanism

    MessageHubConfiguration.Build ends with StartMessageProcessing(), and that method posts
    InitializeHubRequest
    . So the sync/… sub-hub is already draining its own init turn — on its own
    turn scheduler, another thread — while SynchronizationStream's constructor is still inside
    GetHostedHub. And that constructor bound Hub only on the return path:

    var syncHub = Host.GetHostedHub(SynchronizationAddress.Create(ClientId), ConfigureSynchronizationHub, …);
    …
    Hub = syncHub;              // ← too late

    Observable.Throw faults synchronously on subscribe, so the data source's initial load reaches
    SynchronizationStream.OnError inside that window. There, if (Hub is not null) skipped both
    Hub.FailStartup(error) and Hub.OpenGate(SynchronizationGate) — with no log, no exception:

    • SynchronizationGate never opens ⇒ the sub-hub never reaches Started
    • ⇒ Hub.Started never settles ⇒ IDataSource.Initialized (WhenAll over those tasks) hangs
    • ⇒ DataContext's DataContextInit gate is never given its answer
    • ⇒ the probe defers until an unrelated deadline expires.

    That is exactly the log you quoted: sync/… initialization failed — a BuildupAction faulted at 4 ms,
    no DataContext initialization failed for …, and nothing answering the probe.

    Why 25 local runs found nothing, and how to stage it instead

    The window is a few instructions wide, so a repeat loop is the wrong instrument. HostedHubsCollection
    publishes HubAdded twice per hosted hub and the two emissions straddle the window exactly:

    1. from HostedHubsCollection.Add — inside Build, before SyncBuildupActions, so nothing can have
      faulted yet;
    2. from GetHub's creation Lazy — after Build posted the init request, before GetHostedHub
      returns to the constructor.

    Subscribing on the second emission and holding that thread until the sub-hub records its
    InitializationError pins the interleaving with no timing luck at all.
    DataContextFaultedInitBeforeStreamHubBoundTest: 30.02 s FAIL before the change (fell through to
    the hub's own deferral timeout; RunLevel=Starting at park release), 69 ms PASS after
    (RunLevel=Started).

    The fix

    ConfigureSynchronizationHub now starts with .WithInitialization(BindHub) — the synchronous
    overload, which Build runs BEFORE StartMessageProcessing. No window remains: every BuildupAction
    and every handler on the sub-hub sees a bound Hub. The same window was a latent
    NullReferenceException on the success path too (OwnerVersion() reads Hub.Version).

    The residual null-Hub branch stays for the constructor's refusal path but is now loud — skipping
    FailStartup is a wedge, never a no-op, and the silent version is the whole reason this took two
    months to see.

    No bound was widened, no watchdog added, no retry, no catch {}.

    Written up in Doc/Architecture/HubInitializationFailure → "A hub's init runs BEFORE its creator's
    constructor has returned"
    , including the two safe binding points and the HubAdded-twice recipe.

    /cc #1384 — this is the mechanism for this member of that flake population; #2616 is worth re-checking
    against it, since a deferred-forever request is the shared symptom.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions