Skip to content

fix(content): evaluate the ingest trigger-order claim atomically with the stream apply (#1692) - #1695

Merged
rbuergi merged 1 commit into
mainfrom
fix/1692-contentcollections-empty-render
Aug 16, 2026
Merged

rbuergi merged 1 commit into
mainfrom
fix/1692-contentcollections-empty-render

Conversation

@rbuergi

@rbuergi rbuergi commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Root-causes and fixes #1692 — the recurrence of the CollectionNamedArea empty-render flake (CollectionNamedArea_RendersBrowserForFolder_AndContentForFile, run 31972489250: last emission MarkdownControl { 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:

  1. The watcher's Created-event ingest triggers while the just-created file is still 0 bytes (share-tolerant reads are by design — see FileSystemStreamProvider.WriteStreamAsync); it parses an EMPTY article and passes its claim on its pool thread.
  2. Before its markdownStream.Update post enqueues (a window fattened by Update()'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.
  3. The hub applies complete-then-torn: apply order inverts claim order, the empty article lands last and sticks — and on Linux, inotify drops events for files written into a just-created subdirectory, so no repair event ever comes. The render serves the empty MarkdownControl forever.

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

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 SetCurrent skips as a no-op. The merge is extracted as the protected virtual MergeIngestedArticle boundary.

Deterministic red-proven pin

ContentCollectionClaimApplyRaceTest removes 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.

  • RED under pre-fix semantics (claim in callback, unconditional apply — verified locally against that variant): the released empty article overwrites the complete one — assertion fails with found "", the exact CI signature.
  • GREEN with the fix: the claim, evaluated at apply time under the stream's serialization, drops it.

Verification

  • ContentCollectionClaimApplyRaceTest: red on pre-fix variant, green on fix (fresh .trx each)
  • Full MeshWeaver.ContentCollections.Test: 30/30 passed (includes the originally-failing NodeHubContentCollectionTest)
  • dotnet build -c Release -warnaserror: clean for MeshWeaver.ContentCollections, its test project, and MeshWeaver.Documentation
  • What's New entry included (Category: Fix) — users historically saw "upload a file, open it, page is empty"

Closes #1692

🤖 Generated with Claude Code

… 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>
Copilot AI lite review requested due to automatic review settings August 16, 2026 21:58

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

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 MergeIngestedArticle boundary, 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.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

920 tests   919 ✅  11m 2s ⏱️
 10 suites    1 💤
 10 files      0 ❌

Results for commit 0220e35.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

1 371 tests   1 370 ✅  5m 45s ⏱️
   11 suites      1 💤
   11 files        0 ❌

Results for commit 0220e35.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

1 606 tests   1 600 ✅  7m 19s ⏱️
   11 suites      6 💤
   11 files        0 ❌

Results for commit 0220e35.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

   11 files     11 suites   5m 54s ⏱️
2 197 tests 2 006 ✅ 191 💤 0 ❌
2 567 runs  2 376 ✅ 191 💤 0 ❌

Results for commit 0220e35.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

2 164 tests   2 061 ✅  7m 19s ⏱️
   11 suites    103 💤
   11 files        0 ❌

Results for commit 0220e35.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

2 447 tests   2 443 ✅  7m 41s ⏱️
   11 suites      4 💤
   11 files        0 ❌

Results for commit 0220e35.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results

    65 files      65 suites   45m 2s ⏱️
10 705 tests 10 399 ✅ 306 💤 0 ❌
11 075 runs  10 769 ✅ 306 💤 0 ❌

Results for commit 0220e35.

@rbuergi
rbuergi merged commit 77c1ad9 into main Aug 16, 2026
23 checks passed
@rbuergi
rbuergi deleted the fix/1692-contentcollections-empty-render branch August 19, 2026 10: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.

ContentCollections 30s render-timeout flake is BACK — CollectionNamedArea_RendersBrowserForFolder_AndContentForFile (pre-#238 signature)

2 participants