Repository navigation
docs(#3432): the sync/ hub population is the SynchronizationStream population - #3636
Conversation
…pulation A sync/ hub is one field of a SynchronizationStream — the constructor mints one unconditionally — and one cross-hub subscription builds two streams (client plus the owner's twin, same ClientId). The dump's own arithmetic agrees: 6,925 Started + 1,495 Dead = 8,420 against 8,461 streams. So there is no separate sync-hub population to explain, and #3432's "what holds them" is answered. Of the three things that retire one, only the MeshNodeStreamCache idle sweep reaches a Started hub under a live parent, and DetachUpstreams is reachable from nowhere else. Its predicate is a conjunction whose window is refreshed by every Update on the path — while EvictForPath, driven by the change feed that same Update raises, parks the entry's leased stream that only that sweep can collect. The act that creates the garbage postpones its only collector. Narrowing, not a diagnosis: whether those streams are garbage at all is still open, and the page ends with a discriminator that reads off the histogram the existing plan already collects — SynchronizationStream count against MeshNodeStreamCache+Entry count — instead of the referrer walk that needs a dump which restarts the replica. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🟢 Approval recommended
Documentation-only change with internally consistent citations and correct registration in the architecture index.
Pull request overview
Adds a new architecture documentation page that narrows the observed sync/ hub population (#3432) to SynchronizationStream<T> instances and documents the (code-defined) retirement paths—explicitly positioning the result as a narrowing and leaving the remaining “is this actually garbage?” question open.
Changes:
- Add
Doc/Architecture/SyncHubPopulation, explaining whysync/hubs are “one per stream” and why a cross-hub subscription yields two. - Document the three retirement paths for
sync/hubs and highlight that theMeshNodeStreamCacheidle sweep is the only collector forStartedhubs under a live parent. - Register the new page in the Architecture topic map.
File summaries
| File | Description |
|---|---|
| src/MeshWeaver.Documentation/Data/Architecture/SyncHubPopulation.md | New architecture page capturing the stream↔hub relationship and the idle-sweep-based retirement constraints, with code citations. |
| src/MeshWeaver.Documentation/Data/Architecture.md | Adds the new page to the “Reading & writing nodes” topic map entry. |
Review details
- Files reviewed: 2/2 changed files
- Comments generated: 0
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Test Results (shard 4)1 666 tests 1 472 ✅ 2m 5s ⏱️ Results for commit 8c2eecd. ♻️ This comment has been updated with latest results. |
Test Results (shard 0)251 tests 251 ✅ 2m 17s ⏱️ Results for commit 8c2eecd. ♻️ This comment has been updated with latest results. |
Test Results (shard 1)492 tests 492 ✅ 38s ⏱️ Results for commit 8c2eecd. ♻️ This comment has been updated with latest results. |
Test Results (shard 3)1 061 tests 1 061 ✅ 1m 12s ⏱️ Results for commit 8c2eecd. ♻️ This comment has been updated with latest results. |
Test Results (shard 5) 5 files 5 suites 2m 39s ⏱️ Results for commit 8c2eecd. ♻️ This comment has been updated with latest results. |
Test Results (shard 2)1 149 tests 1 149 ✅ 4m 19s ⏱️ Results for commit 8c2eecd. ♻️ This comment has been updated with latest results. |
Test Results 16 files 16 suites 13m 13s ⏱️ Results for commit 8c2eecd. ♻️ This comment has been updated with latest results. |
…b-population-is-the-stream-population # Conflicts: # src/MeshWeaver.Documentation/Data/Architecture.md
What holds the 6,925
Startedsync/hubs — narrowed to a named mechanism, fromsrc/alone#3432 is a measured population with an unestablished cause. This PR is the narrowing, committed
as
Doc/Architecture/SyncHubPopulation. No code changes, no dump taken, no replica touched — thelast attempt on this issue established that dumping a fat replica restarts it, and everything here is
read out of the tree with file citations and checked against the issue's own arithmetic.
1. One of the issue's two open questions is now closed
"What holds them. Unknown. Nothing establishes that the
Startedhalf has a single holder, or thesame one."
It does, and it is one field of a stream.
SynchronizationStream<T>'s constructor mints exactly onesync/hub unconditionally, and one cross-hub subscription builds two streams — the client's(
CreateExternalClient) and the owner's twin (SubscribeToClient→ReduceStream(… WithClientId(request.StreamId))), sameClientId, different parents.The check that makes this more than a story is the issue's own numbers:
So there is no independent sync-hub population; the question reduces to "why are there 8,461 live
SynchronizationStreams", and ~390 KB/hub is a multiplier on that count.2. The issue's actual question — missing reaper, or one that does not fire?
Present, and structurally unable to fire on exactly the paths that generate the garbage.
Three things retire a
sync/hub; only theMeshNodeStreamCacheidle sweep reaches aStartedhub under a live parent (
Dispose()reached 11; the #3427 parent-teardown hook is what producedthe 1,485 Dead).
MeshNodeStreamHandle.DetachUpstreams()is reachable from nowhere else, and itis also the only collector of change-feed-parked predecessors.
Its predicate is
!evicted && subscribers == 0 && now - lastActiveAt >= 10 min, andMeshNodeStreamCacheOptions' own summary names what refreshes it: "EveryGetStreamsubscription,unsubscription and
Updateon the path refreshes the window."🚨 The loop:
EvictForPathruns on every change-feed event and parks the entry's stream;ReclaimIfUnhelddeclines to dispose it because the cache's hydration is a declared holder for theentry's whole life; nothing re-points that hydration ("the sweep only ever CLOSES idle entries; it
never re-subscribes anything"); the next reader builds a fresh one. Two live streams — four
sync/hubs — where the design intends one, and the sameUpdatethat orphaned the first alsoTouch()es the entry and pushes the 10-minute window out.3. A correction this page exists to record
Workspace.cswarns that counting Rx subscribers "CANNOT answer 'is anyone still reading this' …never reaches zero — implemented, measured and reverted". That is a different, abandoned
mechanism at the workspace layer.
MeshNodeStreamCache.Entry.subscribersis a purpose-built refcountmoved only from
SharedView, which the internal hydration bypasses — it genuinely can reachzero. Reasoning from the
Workspacecomment alone blames the wrong conjunct, and the next readerof this issue would have.
4. Deliberately NOT a fix, and what stays open
Whether those 8,461 are garbage at all is still open — thousands of open circuits holding live
layout-area streams is what a healthy portal this size looks like. #3321's history is that two
sessions proposed a fix from a histogram and both were wrong until a referrer walk named the holder,
so this stops at the narrowing.
What it adds is a cheaper discriminator than the one the issue currently turns on. The plan calls
for a ClrMD referrer walk, which needs the dump that restarts the replica. §1's identity makes a
one-line reading decisive, off the
dumpheap -statthat plan already takes:streams ≈ 2× entries ⇒ the orphan floor dominates, and the fix has a named code path (release the
entry's lease on eviction so
ReclaimIfUnheldcollects it). ≈ 1× ⇒ the leak framing isfalsified and this is per-hub DI cost — the issue's own third outcome row. ≫ 2× ⇒ look at the
Identitydimension of_remoteStreamCache's key first.Verification
DocumentationLinkIntegrityTest1/1 (MeshWeaver.Documentation.Test, Release-warnaserror,0 Warning(s) / 0 Error(s), exit read directly).
summary; the two load-bearing ones (the constructor's
GetHostedHub, andUpdaterefreshing theidle window) are quoted verbatim on the page.
../.Pairs-with: none — documentation only, no
src/code touched.Refs #3432
🤖 Generated with Claude Code