Repository navigation
Fix flaky metrics test caused by DateTime.UtcNow timer resolution race - #488
Merged
Merged
Conversation
niemyjski
force-pushed
the
fix/flaky-metrics-timestamp-race
branch
from
April 9, 2026 19:49
4291dc4 to
dca91b6
Compare
Contributor
There was a problem hiding this comment.
Pull request overview
Fixes an intermittent CI failure in InMemoryQueueTests.CanQueueAndDequeueMultipleWorkItemsAsync by making “latest measurement” selection deterministic even when DateTime.UtcNow timestamps collide (e.g., due to Windows timer resolution).
Changes:
- Replace timestamp-based ordering of
RecordedMeasurement<T>with a monotonically increasing sequence number (Interlocked.Increment). - Add a per-
Tstatic sequence counter inRecordedMeasurement<T>and assignSequenceduring measurement capture. - Update
Value<T>()to select the latest measurement viaSequenceinstead ofTimestamp.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Replace DateTime.UtcNow timestamp ordering in RecordedMeasurement<T> with a monotonically increasing sequence number using Interlocked.Increment. Under coarse timer resolution (~15.6ms on Windows CI), concurrent calls to RecordObservableInstruments() could produce measurements with identical timestamps, causing Value<T>() to return stale data due to LINQ's stable sort preserving insertion order for equal keys.
niemyjski
force-pushed
the
fix/flaky-metrics-timestamp-race
branch
from
April 9, 2026 19:55
dca91b6 to
6f090e8
Compare
This was referenced Apr 21, 2026
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.
Fix flaky
CanQueueAndDequeueMultipleWorkItemsAsynctestFixes the intermittent CI failure in
InMemoryQueueTests.CanQueueAndDequeueMultipleWorkItemsAsyncobserved in build run #24208176373:Root cause analysis
The failure chain
InMemoryMetricsuses aTimerinitialized withTimeSpan.Zerodelay — This means the timer callback fires almost immediately after construction, callingRecordObservableInstruments()before the test has enqueued any work items. This records a measurement with value0for the queue count gauge.The test then enqueues 25 items and explicitly calls
RecordObservableInstruments()— This records a second measurement with value25. Both measurements land in the sameConcurrentQueue<RecordedMeasurement<long>>.Value<T>()selects the "latest" measurement usingOrderByDescending(m => m.Timestamp)— Under normal conditions, the25measurement has a later timestamp and wins.On Windows CI,
DateTime.UtcNowhas ~15.6ms resolution — When the timer callback and the test's explicit call both execute within the same clock tick, both measurements get identical timestamps.LINQ's
OrderByDescendingis a stable sort — For equal keys, it preserves insertion order. Since the0measurement was enqueued first (by the timer) and the25measurement second (by the test), stable descending sort places0before25when timestamps are equal.FirstOrDefault()then returns0instead of25.Why this manifests intermittently
The race window is ~15.6ms on Windows (the OS timer interrupt period). On Linux CI runners or developer machines with higher-resolution clocks, the two
DateTime.UtcNowcalls almost always return different values, making the test pass. The failure requires the timer callback and test code to execute within the same timer tick — a narrow but reproducible window under CI load.5 Whys
Value<long>("foundatio.simpleworkitem.count")returned0instead of250?OrderByDescending(m => m.Timestamp).FirstOrDefault()selected the stale measurementDateTime.UtcNowtimestamps, and stable sort preserved FIFO orderDateTime.UtcNowresolution on Windows is ~15.6ms; both calls fell in the same tickInMemoryMetricstimer (initialized withTimeSpan.Zero) races with the test's explicitRecordObservableInstruments()Fix
Replace
DateTime.UtcNowtimestamp ordering with a monotonically increasing sequence number usingInterlocked.Incrementon astatic longcounter inRecordedMeasurement<T>. This guarantees strict total ordering regardless of clock resolution.Changes (1 file, 5 insertions, 3 deletions)
src/Foundatio.TestHarness/Utility/InMemoryMetrics.cs:static long s_sequenceCountertoRecordedMeasurement<T>Timestamp = DateTime.UtcNowwithSequence = Interlocked.Increment(ref s_sequenceCounter)Value<T>()ordering from.OrderByDescending(m => m.Timestamp)to.OrderByDescending(m => m.Sequence)Timestampproperty (no references anywhere in the workspace)Design notes
long? —Interlocked.Incrementnatively supportslong. At 1 billion increments/second, overflow would take ~292 years.static? —RecordedMeasurement<T>is a struct; there's no instance to attach state to. The counter is per closed generic type (e.g.,RecordedMeasurement<long>has its own counter), which is correct sinceValue<T>()only compares within a singleT.TimeSpan.Zerotimer is intentional for test responsiveness. The bug is in the ordering logic, not the timer behavior.Test plan
dotnet build Foundatio.slnx— 0 warnings, 0 errorsdotnet test Foundatio.slnx— 1837 passed, 13 skipped, 0 failedCanQueueAndDequeueMultipleWorkItemsAsyncpasses consistently