Skip to content

InstanceSyncPushTest DELETE-push test times out on main and unrelated branches — the change event is dropped at 5.5s, the wait gives up at 30s #1616

Closed

Description

@rbuergi

MeshWeaver.InstanceSync.Test.InstanceSyncPushTest.A_pending_content_change_pushes_a_DELETE_when_the_local_node_is_being_deleted fails on main and on unrelated branches, so every PR it lands on has to spend a fresh investigation proving it is not theirs. Second-largest bucket in the last-30-failed-runs sweep on #1384.

Occurrences (2026-08-14 alone)

run branch
31793987399 main
31823380161 main
31824306190 fix/clients-sdk
31834879042 fix/1444-install-attribution

Two of the four are main, and neither of the two branch runs touches InstanceSync's path — which is what makes this an attribution tax rather than a signal.

The failure

System.TimeoutException : The operation has timed out.
  at InstanceSyncTestBase.WaitForRemote(Func`2 predicate, Nullable`1 timeout)   InstanceSyncTestBase.cs:98
  at InstanceSyncPushTest.A_pending_content_change_pushes_a_DELETE_when_the_local_node_is_being_deleted()   InstanceSyncPushTest.cs:136

The test's own output is the interesting part — the give-up is at 30 s, but the thing it is waiting for was dropped at 5.5 s:

[19:59:48.387] === TEST START ===
[19:59:48.827] [Warning] InstanceSyncWorker  theta/_Sync/partner: remote unreachable
               (Connection refused (fake remote is unreachable)) — accumulating; next probe in 00:00:00.1
[19:59:48.934] [Warning] InstanceSyncWorker  … next probe in 00:00:00.2
[19:59:53.902] [Warning] MeshWeaver.Data.DataExtensions
               Dropping DataChangedEvent for stream FPJp7O20i0exSl4PyXP-Qw on hub cache/J9hKUio…:
               no synchronization hub found on this hub or any parent — the target stream is gone
               (disposed circuit, released read stream, or never-created sync hub).
               Sender: theta/_…
[20:00:18.995] === TEST FAILED: The operation has timed out.

So the push does happen; its change event is delivered to a stream whose sync hub is gone, and the remaining 25 seconds are spent waiting for something already discarded. 🚨 A larger timeout buys nothing here and would make the drop invisible — the bound is not the defect.

Where to look

  • DataExtensions's "no synchronization hub found on this hub or any parent" is the one line that names a cause. It fires on the cache hub for a stream the sender (theta/_…) still believes in — i.e. the two sides disagree about whether the stream is alive. Establish which of the three cases in its own message this is (disposed circuit / released read stream / never-created sync hub) before anything else; they have different fixes.
  • Timing. The drop lands ~5 s in, right after the worker's unreachable-probe backoff has run twice (0.1 s, 0.2 s). Whether the drop is caused by that backoff path releasing the stream, or merely coincides with it, is the first thing to settle.
  • WaitForRemote (InstanceSyncTestBase.cs:98) is the observer. It was rewritten in feat(#1508): the hub's lifecycle becomes a SOURCE, not a property to sample #1598 and reverted in revert: restore WaitForRemote's poll — my #1508 test-infra half reddened main #1605 — the revert restored the original, so the current failures are on the ORIGINAL observer. Do not re-attempt that rewrite as a fix; it made a second ordering trap and did not address this.

Not to be fixed by

Raising the 30 s bound, a retry, or a Task.Delay. Per AGENTS.md the question is why the sync hub is gone while the sender still has the stream — the drop is the evidence, and it is already logged.

How to measure a fix

flake-repro.yml in rate mode against test/MeshWeaver.InstanceSync.Test with
FullyQualifiedName~A_pending_content_change_pushes_a_DELETE_when_the_local_node_is_being_deleted,
≥100 iterations, A/B main vs the fix branch. It does not reproduce locally on an unloaded Mac (26/26 clean), so a local pass proves nothing.

Related: #1384 (the population this belongs to), #1599 (the largest bucket).

Activity

  1. rbuergi commented on Aug 14, 2026

    @rbuergi
    ContributorAuthor

    An isolated loop does not reproduce it — the instrument is a full shard.

    flake-repro.yml in rate mode, 40 iterations each, same filter, run on both arms:

    arm result
    main (31836446824) 0 failures in 40
    a branch that had just failed on it in CI (31836444718) 0 failures in 40

    So the failing condition is not in the test alone. It needs the shard around it — the other projects, the load, the contention — which matches where it actually fails: shard 0 of the full run, after MeshWeaver.Hosting.Orleans.Test (160 tests, 163 s) and MeshWeaver.Autocomplete.Test have just run in the same job.

    Two consequences worth writing down before anyone chases this:

    • A green loop is not evidence of a fix here. Anything measured on flake-repro alone will read clean whether it works or not. Until the repro moves into the loop, the only instrument is the shard, and the only honest measure is occurrences across real runs.
    • Making it reproduce is the first piece of work, not the fix. The most likely lever is the load the shard supplies: DOTNET_PROCESSOR_COUNT=4 on the 2-vCPU runner has reproduced this class before. Getting it to fail on demand is what turns everything after it into measurement instead of guessing.

    Same shape as #890, where a filtered loop stayed green and only a full shard reproduced.

  2. rbuergi commented on Aug 14, 2026

    @rbuergi
    ContributorAuthor

    Folding in #1617 (same test, filed independently 4 minutes later)

    Two findings from there that this issue did not have, both worth keeping:

    The timing-margin explanation is refuted, with numbers. InstanceSyncTestBase overrides every interval to be tight — DrainDebounce = 50 ms, PullInterval = 200 ms, RetryInitial = 100 ms, RetryMax = 400 ms — so the 30 s wait covers roughly 150 pull attempts and 75 retry probes. The drain did not arrive late; it never produced a DELETE at all. That kills "widen the bound" on arithmetic rather than on principle.

    A concrete hypothesis for the drain path (stated there as unverified, and repeated here as such):

    The test's sequence is: go offline → accumulate a content change → RecentlyDeletedRegistry.MarkDeleted("theta/doc") (synchronous, emits no change event) → reconnect → expect the drain to convert the pending push into a DELETE.

    A drain attempt that read the local node before MarkDeleted landed could complete after Remote.Unreachable = false, push a stale UPDATE, and clear the pending entry — leaving the remote node present with nothing left to drain.

    That fits everything observed here, including the Dropping DataChangedEvent … the target stream is gone at 5.5 s, and it is load-sensitive in the right way: it needs an in-flight attempt straddling the reconnect flip, which is exactly what a loaded shard supplies and an idle loop does not.

    …and it answers that issue's suggested next step in the negative

    flake-repro.yml at ≥100 iterations on this single test would settle both the rate and the hypothesis

    It does not — measured before it was suggested: 0/40 on main and 0/40 on the branch that had just failed (31836446824, 31836444718). The isolated loop runs one process per iteration with none of the shard's contention, so it cannot reach this. Making it reproduce is the first piece of work; DOTNET_PROCESSOR_COUNT=4 inside a full shard is the lever most likely to get there.

    So the order is: instrument the drain to prove or kill the stale-UPDATE hypothesis (does the drain push an UPDATE where a DELETE was expected?), rather than loop it and read a 0/N as evidence of anything.

    Closing #1617 as a duplicate of this one; nothing from it is lost.

  3. rbuergi commented on Aug 17, 2026

    @rbuergi
    ContributorAuthor

    Rate measurement on main — 8 failures in 100 iterations (8%)

    Measured today with flake-repro.yml in rate mode, 100 iterations, on main (not on a feature branch), so the number is independent of any PR:

    RESULT: 8 failure(s) in 100 iteration(s) run (requested 100);
    failing iterations: 12 33 39 47 50 61 64 96
    

    Run: https://github.com/Systemorph/MeshWeaver/actions/runs/32024085067 (test/MeshWeaver.InstanceSync.Test, filter FullyQualifiedName~InstanceSyncPushTest.A_pending_content_change_pushes_a_DELETE_when_the_local_node_is_being_deleted, loglevel=Debug; the failing iterations' trx + blame dumps are on the run's artifacts).

    Two things this pins down:

    1. It is live on main right now, at roughly 1-in-12. That is frequent enough that any PR touching anything has a material chance of catching it — it red-flagged a docs-only PR (docs: which routes ride a module, and which stay in the host #1731, 26 lines of markdown in one embedded .md) an hour ago, which is what prompted the measurement.
    2. The failures are spread evenly across the 100 (12, 33, 39, 47, 50, 61, 64, 96) rather than clustered at the start or end, which argues against a warm-up or resource-exhaustion-over-time explanation and for the per-run race the issue title already describes.

    Posting the number rather than a rerun, per the repo's "measure before attributing" rule — and because a rate measurement is the only form in which a future before/after comparison of a fix will mean anything.

  4. added a commit that references this issue on Aug 17, 2026
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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions