Skip to content

fix(content): SaveFileAsync read-your-writes — kill the ContentCollections 30s-timeout flake - #240

Closed
rbuergi wants to merge 1 commit into
mainfrom
fix/content-collection-save-read-your-writes
Closed

rbuergi wants to merge 1 commit into
mainfrom
fix/content-collection-save-read-your-writes

Conversation

@rbuergi

@rbuergi rbuergi commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

The recurring CI flake, root-caused and killed deterministically

NodeHubContentCollectionTest.CollectionNamedArea_RendersBrowserForFolder_AndContentForFile fails intermittently on shard 2 with ObservableAssertionException : Expected the observable to emit a value matching the predicate within 30s, but it did not — it's been rerun-once past the merge gate repeatedly (including on #224 and #239 in this session).

Root cause (an "event miss")

A content collection is cached and its markdown files scanned once at init. ContentCollection.SaveFileAsync wrote the file to disk but did not update the collection's markdown stream — it relied entirely on the FileSystemWatcher to notice the new file and merge it (UpdateArticle). The file-content render (GetMarkdown → reduce that stream) therefore depends on a watcher event that is asynchronous and lossy: FileSystemWatcher coalesces events and overflows its OS buffer under load. When the event is dropped or delayed, the just-saved file never enters the stream → the render emits null → the 30s assertion times out.

This also bit production: an upload immediately followed by a view could show "not found" until (or unless) a watcher event arrived.

Fix

SaveFileAsync now merges the just-written markdown file into the stream itself (read-your-writes), through the same path the watcher uses. The watcher remains the mechanism for out-of-band (external) changes; a save the process performed is reflected deterministically.

Proven deterministically — both directions

The new SaveFileReadYourWritesTest runs the same save→render with the watcher turned off (via a new, genuinely-useful DisableFileWatcher config — the watcher is unreliable on network mounts / under load), so the file can reach the stream only through SaveFileAsync's merge:

  • Without the fix → times out at 30s (the exact CI failure reproduced locally).
  • With the fix → passes in ~370ms.

All 16 tests in the project pass; Release -warnaserror -p:CIRun=true clean.

🤖 Generated with Claude Code

… FileSystemWatcher

Root cause of the recurring CI flake in
NodeHubContentCollectionTest.CollectionNamedArea_RendersBrowserForFolder_AndContentForFile
(ObservableAssertionException: "did not emit within 30s"):

A content collection is cached and scanned ONCE at init; SaveFileAsync then wrote to disk
but did NOT update the collection's markdown stream — it relied entirely on the
FileSystemWatcher to notice the new file and merge it (UpdateArticle). The file-content
render reduces that stream, so if the watcher event is dropped or delayed — which it is
under load; FileSystemWatcher coalesces events and overflows its OS buffer — the just-saved
file never enters the stream, the render emits null, and the assertion times out at 30s.
This also bit production: an upload followed by a view could show "not found" until (or
unless) a watcher event arrived.

Fix: SaveFileAsync merges the just-written markdown file into the stream itself
(read-your-writes), via the same path the watcher uses. The watcher stays the mechanism for
OUT-OF-BAND (external) changes; a save WE performed is now reflected deterministically.

Proven deterministically (not a rerun): with the new DisableFileWatcher config the file can
reach the stream ONLY through SaveFileAsync's merge, so the added test
(SaveFileReadYourWritesTest) times out at 30s WITHOUT the fix and passes in ~370ms WITH it —
reproducing and closing the exact CI failure mode. DisableFileWatcher is also a genuine
option for backends where the watcher is unreliable (network mounts, high load).

Co-Authored-By: Claude Opus 4.8 (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

This PR addresses a recurring CI flake (and a real production read-after-write issue) in ContentCollections by ensuring a markdown file saved via ContentCollection.SaveFileAsync is merged into the collection’s markdown stream immediately, rather than relying on potentially dropped/delayed FileSystemWatcher events.

Changes:

  • Update ContentCollection.SaveFileAsync to proactively ingest just-written markdown files into markdownStream (read-your-writes).
  • Add DisableFileWatcher to ContentCollectionConfig to support deterministic behavior where file watching is unreliable.
  • Add a regression test that reproduces the prior failure mode by disabling the watcher and asserting save→render succeeds.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
test/MeshWeaver.ContentCollections.Test/SaveFileReadYourWritesTest.cs Adds a regression test that disables file watching and verifies save→render works deterministically.
src/MeshWeaver.ContentCollections/ContentCollectionConfig.cs Introduces DisableFileWatcher configuration with documentation explaining tradeoffs.
src/MeshWeaver.ContentCollections/ContentCollection.cs Ensures SaveFileAsync merges markdown writes into the stream immediately; makes AttachMonitor a no-op when watcher is disabled.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1 to +5
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using MeshWeaver.Data;
@github-actions

github-actions Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

865 tests  ±0   755 ✅ ±0   4m 13s ⏱️ -1s
 13 suites ±0   110 💤 ±0 
 13 files   ±0     0 ❌ ±0 

Results for commit a81b064. ± Comparison against base commit 4e1f3e1.

@rbuergi

rbuergi commented Jul 4, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #238 (merged, main green — "ingest a collection's own write immediately"), which lands the same read-your-writes fix (SaveFileAsync → proactive UpdateArticle) plus a watcher Created handler and path normalization, and its own deterministic regression test (ContentCollectionWriteIngestTest). This PR's core fix is now redundant and would conflict on merge.

Independent same-root-cause confirmation: I reproduced the failure deterministically by disabling the FileSystemWatcher (Linux inotify drops the create event for a file written into a just-created subdir; only the FILE-content render — which reduces the watcher-populated markdown stream — flaked; the folder listing reads the provider live, so it was never affected; CI-only because macOS FSEvents doesn't miss).

The only unique bit here is the optional ContentCollectionConfig.DisableFileWatcher knob (useful where the watcher is unreliable — network mounts / high load; the read-your-writes fix is what makes it viable). Happy to re-open a minimal PR with just that config if it's wanted; closing this to avoid redundant churn.

@rbuergi rbuergi closed this Jul 4, 2026
@rbuergi
rbuergi deleted the fix/content-collection-save-read-your-writes branch July 4, 2026 09:02
@rbuergi

rbuergi commented Jul 4, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #238, which merged first and is live on main (green): SaveFileAsync proactive UpdateArticle ingest so read-after-write no longer depends on the FileSystemWatcher, plus a Created handler for external files and a deterministic NoMonitorProvider regression test (ContentCollectionWriteIngestTest) that fails without the fix and passes with it. Same root cause and same core fix — closing as duplicate. (If the DisableFileWatcher config knob from #240 is wanted, it can be lifted onto main as a small follow-up.)

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