Fix catastrophic CI flake: thread-safe FileWatcher subscriber lists - #65
Merged
Merged
Conversation
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.
|
🛰️ Docs preview: https://pr-65.pennington-dev.pages.dev Rebuilt on every push to this PR; torn down when it closes. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Symptom
CI intermittently fails the ubuntu Test job with a catastrophic (process-terminating) exception, while all test assemblies report
Passed!: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
FileWatcherholds its subscriber callbacks in two plainList<T>fields:NotifySubscribersenumerates them on the debounce timer's ThreadPool thread (fired ~100 ms after a file event).SubscribeToChangesappends to them lock-free from other threads —AuditRunnersubscribes at host start, andRedirectContentServicesubscribes lazily on the first request.Under
WebApplicationFactory/TestServer (envTesting), the watcher is armed unconditionally over a real inotifyFileSystemWatcheron the content root. On Linux, an inotify event during startup firesNotifySubscribersright as those late subscribers are still.Add()-ing. TheListis mutated mid-foreach→Collection was modifiedthrown on the unobserved timer thread → the whole test process is torn down.The per-callback
try/catchinNotifySubscriberswraps each invocation, not theforeach, so the enumerator'sMoveNext()throw escapes it entirely.Fix
Switch both subscriber lists to
ImmutableList<T>, appended atomically viaImmutableInterlocked.Update.NotifySubscriberssnapshots 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_DoesNotCorruptEnumerationreproduces the exact fault deterministically: a subscriber registers another subscriber from inside a notification, and the debounce is fired off aFakeTimeProviderso the enumeration runs on the test thread.System.InvalidOperationException : Collection was modified; enumeration operation may not execute.(verified by reverting the fix locally).Verification
Pennington.Tests: 1047 passed (incl. the new test).Pennington.IntegrationTests: 67 passed, 4 skipped.