Repository navigation
fix(content): evaluate the ingest trigger-order claim atomically with the stream apply (#1692) - #1695
Conversation
… the stream apply The CollectionNamedArea empty-render flake recurred (#1692) because #978's trigger-order claim was CHECKED on the pool read-completion thread while the merge APPLIED later, serialized on the sync hub via a posted UpdateStreamRequest — a check-then-act window. The watcher's Created-event ingest (triggered while the just-created file is still 0 bytes, so its share-tolerant read legally parses an EMPTY article) could pass its claim, then be overtaken — before its markdownStream.Update post enqueued — by SaveFile's later-triggered read-your-writes ingest claiming and posting the complete article. The hub then applied complete-then-torn: apply order inverted claim order, the empty article landed last and stuck, and (Linux inotify dropping events for files written into a just-created subdirectory) no repair event ever came. Exact CI signature: last emission MarkdownControl { Markdown = <empty>, Html = <empty> } (run 31972489250). ClaimIngest is now evaluated INSIDE the update transform, which the sync hub invokes strictly serially — claim and apply are atomic, so apply order can never invert claim order; a superseded ingest's transform returns null, which the stream skips as a no-op. The merge is extracted as the protected virtual MergeIngestedArticle boundary, which the deterministic pin (ContentCollectionClaimApplyRaceTest) holds at exactly the defect's window (post-parse/post-claim, pre-post): red under the pre-fix semantics (the empty article overwrites the complete one, found ""), green with the fix. Closes #1692 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes a race in ContentCollection where a stale/empty ingest (often from a watcher Created event) could incorrectly overwrite a later-triggered “read-your-writes” ingest, leading to permanent empty renders for newly uploaded markdown files. It makes the trigger-order “claim” check atomic with the stream apply by evaluating it inside the synchronization stream’s serialized update transform, and adds a deterministic test to pin the interleaving.
Changes:
- Move ingest-order claim evaluation into the stream update transform via a new
MergeIngestedArticleboundary, ensuring claim/apply atomicity. - Add a deterministic regression test that forces the pre-fix interleaving without timing dependence.
- Add a “What’s New” entry documenting the user-visible fix.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
src/MeshWeaver.ContentCollections/ContentCollection.cs |
Makes ingest claim evaluation atomic with stream application by performing it inside the serialized update transform (MergeIngestedArticle). |
test/MeshWeaver.ContentCollections.Test/ContentCollectionClaimApplyRaceTest.cs |
Adds a deterministic test that reproduces the claim/apply overtaking window and verifies stale ingests cannot overwrite newer content. |
src/MeshWeaver.Documentation/Data/WhatsNew/2026-08-16-content-upload-empty-render-race.md |
Documents the fix as a user-facing “Fix” entry in What’s New. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Test Results (shard 0)920 tests 919 ✅ 11m 2s ⏱️ Results for commit 0220e35. |
Test Results (shard 5)1 371 tests 1 370 ✅ 5m 45s ⏱️ Results for commit 0220e35. |
Test Results (shard 4)1 606 tests 1 600 ✅ 7m 19s ⏱️ Results for commit 0220e35. |
Test Results (shard 3) 11 files 11 suites 5m 54s ⏱️ Results for commit 0220e35. |
Test Results (shard 1)2 164 tests 2 061 ✅ 7m 19s ⏱️ Results for commit 0220e35. |
Test Results (shard 2)2 447 tests 2 443 ✅ 7m 41s ⏱️ Results for commit 0220e35. |
Test Results 65 files 65 suites 45m 2s ⏱️ Results for commit 0220e35. |
Root-causes and fixes #1692 — the recurrence of the CollectionNamedArea empty-render flake (
CollectionNamedArea_RendersBrowserForFolder_AndContentForFile, run 31972489250: last emissionMarkdownControl { Markdown = <empty>, Html = <empty> }).Root cause
#978 established the trigger-order rule (a monotonic sequence claimed at trigger time; an ingest may only merge if no later-triggered ingest of the same file already landed), but it checked the claim on the pool read-completion thread and posted the merge afterwards — the apply happens later, serialized on the sync hub via
UpdateStreamRequest. Claim and apply were not atomic:Created-event ingest triggers while the just-created file is still 0 bytes (share-tolerant reads are by design — seeFileSystemStreamProvider.WriteStreamAsync); it parses an EMPTY article and passes its claim on its pool thread.markdownStream.Updatepost enqueues (a window fattened byUpdate()'s context-capture fallback chain — DI lookups between claim and post), SaveFile's later-triggered read-your-writes ingest claims and posts the complete article.MarkdownControlforever.Both prior fixes (#238 read-your-writes, #978 trigger-order claim) are intact and necessary — this closes the residual check-then-act window between them.
Fix
ClaimIngestis now evaluated inside the update transform, which the sync hub invokes strictly serially — claim and apply are atomic, so apply order can never invert claim order. A superseded ingest's transform returnsnull, whichSetCurrentskips as a no-op. The merge is extracted as the protected virtualMergeIngestedArticleboundary.Deterministic red-proven pin
ContentCollectionClaimApplyRaceTestremoves the timing entirely: a collection subclass holds the torn ingest at exactly the defect's window (post-parse/post-claim, pre-post), lets the later-triggered complete article provably apply, then releases the held merge.found "", the exact CI signature.Verification
ContentCollectionClaimApplyRaceTest: red on pre-fix variant, green on fix (fresh .trx each)MeshWeaver.ContentCollections.Test: 30/30 passed (includes the originally-failingNodeHubContentCollectionTest)dotnet build -c Release -warnaserror: clean forMeshWeaver.ContentCollections, its test project, andMeshWeaver.DocumentationCategory: Fix) — users historically saw "upload a file, open it, page is empty"Closes #1692
🤖 Generated with Claude Code