Skip to content

Fix catastrophic CI flake: thread-safe FileWatcher subscriber lists - #65

Merged
phil-scott-78 merged 1 commit into
mainfrom
fix/filewatcher-subscriber-race
Jul 7, 2026
Merged

phil-scott-78 merged 1 commit into
mainfrom
fix/filewatcher-subscriber-race

Conversation

@phil-scott-78

Copy link
Copy Markdown
Contributor

Symptom

CI intermittently fails the ubuntu Test job with a catastrophic (process-terminating) exception, while all test assemblies report Passed!:

[FATAL ERROR] System.InvalidOperationException
Catastrophic failure: ... Collection was modified; enumeration operation may not execute.

It's flaky (same commit passed on the feature branch, failed once on main) and Linux-only. A rerun clears it — but it will keep recurring.

Root cause

FileWatcher holds its subscriber callbacks in two plain List<T> fields:

  • NotifySubscribers enumerates them on the debounce timer's ThreadPool thread (fired ~100 ms after a file event).
  • SubscribeToChanges appends to them lock-free from other threads — AuditRunner subscribes at host start, and RedirectContentService subscribes lazily on the first request.

Under WebApplicationFactory/TestServer (env Testing), the watcher is armed unconditionally over a real inotify FileSystemWatcher on the content root. On Linux, an inotify event during startup fires NotifySubscribers right as those late subscribers are still .Add()-ing. The List is mutated mid-foreach → Collection was modified thrown on the unobserved timer thread → the whole test process is torn down.

The per-callback try/catch in NotifySubscribers wraps each invocation, not the foreach, so the enumerator's MoveNext() throw escapes it entirely.

Note: this is not related to the WASM-boot-manifest PR that happened to catch the flake — that change's build code doesn't even run in the host tests, and there's no WASM render mode anywhere in the suite. The race is long-standing; the merge just rolled the dice.

Fix

Switch both subscriber lists to ImmutableList<T>, appended atomically via ImmutableInterlocked.Update. NotifySubscribers snapshots each field into a local once, so a concurrent registration swaps the field reference without disturbing the in-flight enumeration. Lock-free on both paths, and readers always see a consistent immutable set. Subscriber ordering (insertion order) is preserved.

Test

SubscribeDuringNotification_DoesNotCorruptEnumeration reproduces the exact fault deterministically: a subscriber registers another subscriber from inside a notification, and the debounce is fired off a FakeTimeProvider so the enumeration runs on the test thread.

  • On the buggy code it fails with the identical System.InvalidOperationException : Collection was modified; enumeration operation may not execute. (verified by reverting the fix locally).
  • With the fix it passes; the trailing subscriber still fires.

Verification

  • Pennington.Tests: 1047 passed (incl. the new test).
  • Pennington.IntegrationTests: 67 passed, 4 skipped.
  • Core builds clean, 0 warnings.

FileWatcher.NotifySubscribers enumerated the _subscribers /
_pathAwareSubscribers List<T> fields on the debounce timer thread while
SubscribeToChanges appended to them lock-free from other threads
(AuditRunner at host start, RedirectContentService on first request). A
registration landing during an in-flight notification threw "Collection
was modified; enumeration operation may not execute" on the unobserved
timer thread and terminated the process — a catastrophic, Linux-only
flake in the integration tests, where a real inotify watcher fires the
100ms debounce squarely in the startup/first-request subscription window.

Switch both lists to ImmutableList<T> appended via
ImmutableInterlocked.Update; NotifySubscribers snapshots each field once,
so a concurrent registration swaps the reference without disturbing the
in-flight enumeration. The regression test drives the debounce off a
FakeTimeProvider so the modify-during-enumeration surfaces deterministically
on the test thread instead of as an unobservable process crash.
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

🛰️ Docs preview: https://pr-65.pennington-dev.pages.dev

Rebuilt on every push to this PR; torn down when it closes.

@phil-scott-78
phil-scott-78 merged commit c7129e7 into main Jul 7, 2026
4 checks passed
@phil-scott-78
phil-scott-78 deleted the fix/filewatcher-subscriber-race branch July 7, 2026 15:38
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.

1 participant