Skip to content

fix(#2641): a re-subscribe must declare the same wire capabilities as the first, and 'Frame loss detected' is a resync counter - #2744

Merged
rbuergi merged 1 commit into
mainfrom
docs/2641-frame-loss-is-a-resync-counter
Aug 30, 2026
Merged

rbuergi merged 1 commit into
mainfrom
docs/2641-frame-loss-is-a-resync-counter

Conversation

@rbuergi

@rbuergi rbuergi commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Settles the open half of #2641 and fixes the one real defect that reading its resync path turned up.

The defect: a re-subscribe silently withdrew a negotiated wire capability

SubscribeRequest.AcceptsStringSplice is the one negotiated capability of the owner→subscriber
fan-out (#1284 / #1414): a changed string leaf travels as the changed span instead of the whole new
string, but only for a subscriber that says it can apply one. grep -n AcceptsStringSplice src/
found = true at one of the three sites that post a SubscribeRequest:

site declared it
CreateExternalClient — the initial subscribe ✅
Resubscribe — owner recycle / StreamEndedEvent re-ask ❌
SynchronizationStream.RequestFreshSnapshot — the frame-loss resync ❌

When the owner cannot match a re-subscribe to a live server-side stream it builds a fresh one and
reads the capability off that request
(GetClientSubscription → miss → new reduced +
ToDataChanged(…, request.AcceptsStringSplice)). A miss is not exotic — it is exactly what an
eviction on the router's TargetUnserved verdict (#2620), an owner recycle, or a torn-down sync/
hub produces. So from the first such cycle the owner fans out whole-string replace frames to a
mirror that understands splices: the per-subscriber quadratic #1284/#1414 removed, reinstated
permanently and silently, on precisely the paths that fire during a resync storm.

Nothing fails — a replace is a shape every subscriber understands — so no assertion on the
frames could have caught it. The only place it is observable is the request.

Fixed at the root, not with a third literal. JsonSynchronizationStream.MintSubscribeRequest is
now the one place a C# subscriber mints a SubscribeRequest: a capability is a property of the
applier this assembly ships, not of the moment a particular post was written, so declaring it once
means there is no second site to forget. The three posts go through it; nothing else changes (the
resync still posts with identity: null and the same Subscriber, the recycle re-ask still with
SystemUserId).

ResubscribeKeepsNegotiatedCapabilitiesTest drives the real resync — the deterministic one-frame
wire loss StreamFrameLossResyncTest already uses — and asserts every SubscribeRequest the
subscriber posts carries the capability. Verified failing on the unfixed tree and passing on the
fixed one:

# src reverted to HEAD
Failed  MeshWeaver.Data.Test.ResubscribeKeepsNegotiatedCapabilitiesTest.TheFrameLossResubscribe_StillDeclaresStringSplice
  [SYNC_STREAM] Frame loss detected for XQ-G6YrJm0yb4a9CpXK64g: incoming Patch v3 chains onto v2 …
# with the fix
Passed!  - Failed: 0, Passed: 1

The documentation: how to read Frame loss detected

#2641 reported 847 of those lines in 30 minutes as data loss. They are not. Every line is a gap that
was detected and answered — that is what the BasedOnVersion chain exists for — so the raw count
diagnoses nothing on its own. Data Synchronization & CRDT §6 now
documents the chain and states the two numbers that do mean something (the per-stream count, and
whether a Full follows), plus the three log lines to correlate it with, because the driver is
almost always upstream: a latched stream attach (#2633 / #2692, fixed by #2645), the eviction cycle
(#2620), an owner recycle.

That matches the reading the merged design page already takes (#2724): the version chain is the
durable stream.

Issue disposition

#2641 is closed with a per-symptom table on the issue. In short: the UserIdentityCache half was
fixed by #2723; the frame-loss half is a duplicate of #2654, which owns the residual "does the
resync converge" question with per-stream counts and a user-visible surface; the no live subscriber

Verification

  • dotnet build src/MeshWeaver.Data/MeshWeaver.Data.csproj -c Release -warnaserror → 0 Warning(s), 0 Error(s)
  • dotnet build test/MeshWeaver.Data.Test/… -c Release -warnaserror → 0 Warning(s), 0 Error(s)
  • dotnet build test/MeshWeaver.Documentation.Test/… -c Release -warnaserror → 0 Warning(s), 0 Error(s)
  • MeshWeaver.Data.Test full project, Release → see below
  • WhatsNewEntryIntegrityTest → passed

🤖 Generated with Claude Code

… the first (#2654 owns the residual)

AcceptsStringSplice — the one negotiated capability of the owner→subscriber fan-out
(#1284/#1414) — was set at exactly ONE of the three sites that post a SubscribeRequest:
the initial subscribe. Resubscribe (owner recycle / StreamEndedEvent) and
SynchronizationStream.RequestFreshSnapshot (the frame-loss resync) both omitted it.

When the owner cannot match a re-subscribe to a live server-side stream it BUILDS a fresh
one and reads the capability off THAT request — which is exactly what an eviction on the
router's TargetUnserved verdict (#2620), an owner recycle or a torn-down sync hub produces.
From that cycle on, the owner fans out whole-string `replace` frames to a mirror that
understands splices: the per-subscriber quadratic #1284/#1414 removed, reinstated silently
and permanently, on precisely the paths that fire during a resync storm. Nothing fails — a
replace is a shape every subscriber understands — so no assertion on the FRAMES could catch
it; the only place it is visible is the request.

Fixed at the root, not with a third literal: JsonSynchronizationStream.MintSubscribeRequest
is now the one place a C# subscriber mints a SubscribeRequest, because a capability is a
property of the applier this assembly ships, not of the moment one post was written. Nothing
else changes — the resync still posts with no identity and the same Subscriber, the recycle
re-ask still with SystemUserId.

ResubscribeKeepsNegotiatedCapabilitiesTest drives the real resync (the deterministic
one-frame wire loss StreamFrameLossResyncTest already uses) and asserts every posted
SubscribeRequest declares the capability. Verified RED on the unfixed tree, GREEN with the
fix; MeshWeaver.Data.Test 394/394.

Docs: DataSyncAndCrdt §6 now documents the BasedOnVersion chain and states how to read
`[SYNC_STREAM] Frame loss detected` — a RESYNC counter, not a data-loss counter. The raw
total diagnoses nothing; the per-stream count and whether a Full follows do, and the driver
is almost always upstream (a latched stream attach #2633/#2692 → #2645, the #2620 eviction
cycle, an owner recycle). That is the recorded disposition of #2641's 847-lines-in-30-min.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

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

Ensures synchronization stream re-subscriptions keep the same negotiated wire capabilities as the initial subscription (specifically SubscribeRequest.AcceptsStringSplice), preventing silent fallback to heavier whole-string replace frames after resync/reconnect cycles, and documents how to interpret “Frame loss detected” log lines.

Changes:

  • Centralizes SubscribeRequest creation via JsonSynchronizationStream.MintSubscribeRequest(...) and routes all subscribe / resubscribe paths through it.
  • Adds a regression test that deterministically triggers a frame-loss resync and asserts every subscribe request declares AcceptsStringSplice.
  • Adds two What’s New entries and expands the Data Sync/CRDT architecture documentation to clarify that “Frame loss detected” is a recovery/resync counter.

Reviewed changes

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

Show a summary per file
File Description
test/MeshWeaver.Data.Test/ResubscribeKeepsNegotiatedCapabilitiesTest.cs New regression test asserting re-subscribe retains negotiated capability after deterministic frame loss.
src/MeshWeaver.Data/Serialization/JsonSynchronizationStream.cs Introduces MintSubscribeRequest and refactors subscribe/resubscribe posts to use the centralized factory.
src/MeshWeaver.Data/Serialization/SynchronizationStream.cs Updates frame-loss resync path (RequestFreshSnapshot) to mint subscribe request via the shared factory.
src/MeshWeaver.Documentation/Data/Architecture/DataSyncAndCrdt.md Documents the BasedOnVersion chain and how to read “Frame loss detected” logs as recovery signals.
src/MeshWeaver.Documentation/Data/WhatsNew/2026-08-30-reconnecting-no-longer-makes-streaming-heavier.md What’s New (Fix) entry describing the user-visible improvement from retaining splice capability on reconnection/resync.
src/MeshWeaver.Documentation/Data/WhatsNew/2026-08-30-frame-loss-in-the-log-is-a-resync-counter.md What’s New entry pointing readers to the updated doc guidance for interpreting frame-loss logs.

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

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

707 tests   515 ✅  4m 54s ⏱️
  5 suites  192 💤
  5 files      0 ❌

Results for commit a91bdf7.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

970 tests   970 ✅  5m 41s ⏱️
  6 suites    0 💤
  6 files      0 ❌

Results for commit a91bdf7.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

793 tests   789 ✅  6m 48s ⏱️
  6 suites    4 💤
  6 files      0 ❌

Results for commit a91bdf7.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

917 tests   917 ✅  5m 34s ⏱️
  6 suites    0 💤
  6 files      0 ❌

Results for commit a91bdf7.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

   10 files     10 suites   5m 32s ⏱️
3 470 tests 3 470 ✅ 0 💤 0 ❌
4 120 runs  4 120 ✅ 0 💤 0 ❌

Results for commit a91bdf7.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

1 534 tests   1 534 ✅  6m 41s ⏱️
    8 suites      0 💤
    8 files        0 ❌

Results for commit a91bdf7.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results

   41 files     41 suites   35m 12s ⏱️
8 391 tests 8 195 ✅ 196 💤 0 ❌
9 041 runs  8 845 ✅ 196 💤 0 ❌

Results for commit a91bdf7.

@rbuergi
rbuergi merged commit 16d59d8 into main Aug 30, 2026
25 checks passed
@rbuergi
rbuergi deleted the docs/2641-frame-loss-is-a-resync-counter branch August 30, 2026 09:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants