Skip to content

fix(#2756): a transient unserved verdict must not evict the owner's server-side stream - #2758

Merged
rbuergi merged 2 commits into
mainfrom
fix/2756-evict-only-on-terminal
Aug 30, 2026
Merged

rbuergi merged 2 commits into
mainfrom
fix/2756-evict-only-on-terminal

Conversation

@rbuergi

@rbuergi rbuergi commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Closes #2756. Main is red on 3376da2a2 and CD is publishing nothing, so this is the unblock.

What broke

#2745 (mine) widened DataExtensions.HandleTargetUnservedFailure's gate from TargetUnserved && ErrorType == NotFound to the stamp alone, reasoning that keeping the ErrorType test would make the router's new verdict inert here and re-open the #2426/#2546 fan-out-to-a-corpse leak.

That reasoning is backwards for this verdict. An inert eviction is the correct answer for a transient one:

  • ShuttingDown is the platform's ride-it-out classification — the router now stamps TargetUnserved beside it when the owner is mid-roll or its pod-hub claim has not landed;
  • JsonSynchronizationStream handles ShuttingDown by RE-ARMING, not re-subscribing (JsonSynchronizationStream.cs:473-484, :648-652);
  • so an owner that evicts on it disposes the server-side half of a subscription whose other half is deliberately sitting still. The owner throws the stream away while the subscriber waits for it.

ObservableQueryTests.ObserveQuery_EmitsRemovedOnDeletedNode is where it surfaced — both halves share a process there, so the eviction wins the race — but co-location is only what made it deterministic, not the defect.

The fix

The stamp still decides whether the delivery is ours to act on — that half of #2745 is its real insight and stays. The verdict now decides whether the right action is to evict or to leave the stream alone: an explicit ErrorType.ShuttingDown early-return, commented so a future transient type must be added there rather than falling through. The default stays evict, and eviction is the irreversible branch for a subscriber that never re-asks.

The leak #2426/#2546 closes is a subscriber whose process is gone — the terminal verdict — and only that one may evict.

Tests

UnservedVerdictEvictionTest's transient case inverts to ATransientUnservedVerdict_LeavesTheOwnersServerSideStreamAlone, and its assertion is a positive reactive signal rather than an elapsed window: after the verdict the owner writes a change and the test waits for it to reach the client's mirror — which can only happen over the stream the eviction must not have destroyed. The previous helper bridged Observable.Timer with .ToTask() and would have passed before the verdict was even delivered. ATerminalUnservedVerdict_StillEvicts is untouched.

Non-vacuity: removing the guard fails 1 of 3; restoring it passes 3/3. dotnet build -c Release -warnaserror → 0 Warning(s), 0 Error(s).

Reported by a peer session as #2756 with the correct mechanism; the JsonSynchronizationStream re-arm evidence is what settles which side is right.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EPCWramjFYquX5SaMdeJKW

…erver-side stream

#2745 widened HandleTargetUnservedFailure's gate to the TargetUnserved stamp alone, on the
reasoning that keeping the ErrorType test would make the router's new verdict inert. An inert
eviction is the CORRECT answer for that verdict: ShuttingDown is the platform's transient,
ride-it-out classification, and JsonSynchronizationStream handles it by RE-ARMING rather than
re-subscribing (JsonSynchronizationStream.cs:473-484, :648-652). So an owner that evicts on it
disposes the server-side half of a subscription whose other half is deliberately waiting for it.

Main went red on 3376da2 with ObservableQueryTests.ObserveQuery_EmitsRemovedOnDeletedNode
timing out, where both halves share a process and the eviction wins the race — but the defect is
not co-location, it is the two sides disagreeing about what the verdict means.

The stamp still decides whether the delivery is OURS to act on (that half of #2745 stays and is
its real insight); the verdict now decides whether the action is to evict or to leave the stream
alone. The guard is an explicit ShuttingDown early-return, commented so a future transient
ErrorType is added there rather than falling through — the default is to evict, and eviction is
irreversible for a subscriber that never re-asks.

UnservedVerdictEvictionTest's transient case inverts accordingly, and its assertion is now a
POSITIVE reactive signal instead of an elapsed window: after the verdict the owner writes a
change, and the test waits for that change to reach the client's mirror — which can only happen
over the stream the eviction must not have destroyed. The previous helper bridged
Observable.Timer with .ToTask() and would have passed before the verdict was even handled.
ATerminalUnservedVerdict_StillEvicts is untouched.

Non-vacuity: removing the guard fails 1 of 3; restoring it passes 3/3.

Reported by meshweaver-61 as #2756.

Closes #2756

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EPCWramjFYquX5SaMdeJKW
Copilot AI lite review requested due to automatic review settings August 30, 2026 10:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adjusts owner-side handling of DeliveryFailure.TargetUnserved so that transient “unserved” verdicts (ErrorType.ShuttingDown) no longer evict the owner’s server-side stream, preventing subscribers that re-arm (rather than re-subscribe) from getting stuck waiting on a stream the owner destroyed.

Changes:

  • Update HandleTargetUnservedFailure to treat ErrorType.ShuttingDown as transient and avoid eviction.
  • Update UnservedVerdictEvictionTest transient case to assert survivability via a positive “change reaches mirror” signal rather than a timing window.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/MeshWeaver.Data/DataExtensions.cs Avoids owner-side stream eviction on transient TargetUnserved verdicts (ShuttingDown).
test/MeshWeaver.Data.Test/UnservedVerdictEvictionTest.cs Reworks the transient-verdict test to prove the stream remains usable by verifying a post-verdict update reaches the client mirror.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/MeshWeaver.Data/DataExtensions.cs Resolved
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

   10 files  ±0     10 suites  ±0   5m 44s ⏱️ +5s
3 476 tests ±0  3 476 ✅ ±0  0 💤 ±0  0 ❌ ±0 
4 128 runs  ±0  4 128 ✅ ±0  0 💤 ±0  0 ❌ ±0 

Results for commit afec19f. ± Comparison against base commit 6551e64.

This pull request removes 1 and adds 1 tests. Note that renamed tests count towards both.
MeshWeaver.Data.Test.UnservedVerdictEvictionTest ‑ ATransientUnservedVerdict_EvictsTheOwnersServerSideStream
MeshWeaver.Data.Test.UnservedVerdictEvictionTest ‑ ATransientUnservedVerdict_LeavesTheOwnersServerSideStreamAlone

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

1 540 tests  ±0   1 540 ✅ ±0   5m 27s ⏱️ -44s
    8 suites ±0       0 💤 ±0 
    8 files   ±0       0 ❌ ±0 

Results for commit afec19f. ± Comparison against base commit 6551e64.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

972 tests  ±0   972 ✅ +1   5m 44s ⏱️ - 1m 32s
  6 suites ±0     0 💤 ±0 
  6 files   ±0     0 ❌  - 1 

Results for commit afec19f. ± Comparison against base commit 6551e64.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

721 tests  ±0   529 ✅ ±0   4m 51s ⏱️ +14s
  5 suites ±0   192 💤 ±0 
  5 files   ±0     0 ❌ ±0 

Results for commit afec19f. ± Comparison against base commit 6551e64.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

793 tests  ±0   789 ✅ ±0   6m 55s ⏱️ +4s
  6 suites ±0     4 💤 ±0 
  6 files   ±0     0 ❌ ±0 

Results for commit afec19f. ± Comparison against base commit 6551e64.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

917 tests  ±0   917 ✅ ±0   5m 27s ⏱️ +16s
  6 suites ±0     0 💤 ±0 
  6 files   ±0     0 ❌ ±0 

Results for commit afec19f. ± Comparison against base commit 6551e64.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results

   41 files  ±0     41 suites  ±0   34m 11s ⏱️ - 1m 36s
8 419 tests ±0  8 223 ✅ +1  196 💤 ±0  0 ❌  - 1 
9 071 runs  ±0  8 875 ✅ +1  196 💤 ±0  0 ❌  - 1 

Results for commit afec19f. ± Comparison against base commit 6551e64.

This pull request removes 1 and adds 1 tests. Note that renamed tests count towards both.
MeshWeaver.Data.Test.UnservedVerdictEvictionTest ‑ ATransientUnservedVerdict_EvictsTheOwnersServerSideStream
MeshWeaver.Data.Test.UnservedVerdictEvictionTest ‑ ATransientUnservedVerdict_LeavesTheOwnersServerSideStreamAlone

@rbuergi
rbuergi merged commit b7628fc into main Aug 30, 2026
24 checks passed
@rbuergi
rbuergi deleted the fix/2756-evict-only-on-terminal branch August 30, 2026 10:57
rbuergi added a commit that referenced this pull request Aug 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants