Skip to content

test(operators): make throttle supersession tests deterministic#98

Merged
glennawatson merged 2 commits into
mainfrom
fix/flaky-throttle-tests
Jun 25, 2026
Merged

test(operators): make throttle supersession tests deterministic#98
glennawatson merged 2 commits into
mainfrom
fix/flaky-throttle-tests

Conversation

@glennawatson

Copy link
Copy Markdown
Contributor

What kind of change does this PR introduce?

Test-only fix for two flaky throttle supersession tests. No product code changes.

What is the new behavior?

Both tests now drive ordering with explicit synchronization instead of racing wall-clock timing:

  • WhenThrottleReceivesRapidValues_ThenOnlyEmitsLatest (Async.Tests) now completes the DirectSource re-entrantly from inside the throttled emission handler. Because the emission and the completion then run on the same thread, the WitnessAsync concurrent-call guard is never tripped, so completion is always delivered. The redundant lastEmitted TCS is removed; the test now simply awaits completed.
  • WhenThrottleUntilTrueFastReplacements_ThenLatestWins (Extensions.Tests) now records every emission and waits on the later value (whose throttle timer is never superseded), then asserts the final observed value is the latest one — instead of latching whichever emission happened to arrive first into a single-value TCS.

What is the current behavior?

Both tests intermittently failed under CI load:

  • WhenThrottleReceivesRapidValues_ThenOnlyEmitsLatest failed with a 5s TimeoutException at await completed.Task.WaitAsync(...). Root cause: the throttled value-3 emission runs on a pooled timer continuation (the non-system TimeProvider path uses PooledDelaySource with RunContinuationsAsynchronously = true). The test then called source.Complete(...) from the test thread. When that raced the still-unwinding OnNextAsync call on the pool thread, WitnessAsync.TryEnterOnSomethingCall detected a cross-thread concurrent call and silently dropped the completion, so the completed TCS was never signalled. This is a test harness race, not a product bug — the product correctly rejects concurrent witness calls.
  • WhenThrottleUntilTrueFastReplacements_ThenLatestWins failed with "Expected to be 2 but found 1". Root cause: ThrottleUntilTrue uses a real wall-clock scheduler with a 50 ms window and exposes no scheduler seam. The two synchronous OnNext calls are adjacent, but under thread starvation the earlier value's timer could fire before the later value replaced it, and the single-value TrySetResult latched the earlier value permanently.

Reproduced the first flake locally under heavy CPU load (~1 failure in 30 runs with a TimeoutException); both fixes then ran green 160/160 (primary) and 60/60 (secondary) under the same load.

Not closing an issue.

What might this PR break?

None. Test-only change; no production code touched, no analyzer suppressions.

Checklist

  • I have read the Contribute guide
  • Tests have been added or updated (for bug fixes / features)
  • Docs have been added or updated (for bug fixes / features)
  • Changes target the main branch
  • PR title follows Conventional Commits

Additional information

Repetition-loop results on net11.0 Release under ~40 concurrent CPU burners (16 cores):

  • WhenThrottleReceivesRapidValues_ThenOnlyEmitsLatest: pre-fix 1/30 failed (TimeoutException); post-fix 60/60 then 100/100 passed.
  • WhenThrottleUntilTrueFastReplacements_ThenLatestWins: post-fix 60/60 passed.
  • Full ReactiveUI.Primitives.Async.Tests and ReactiveUI.Primitives.Extensions.Tests projects pass on net11.0.

- WhenThrottleReceivesRapidValues_ThenOnlyEmitsLatest completed the
  DirectSource from the test thread while the throttled emission was
  still unwinding on a pooled timer continuation. That overlap tripped
  WitnessAsync's concurrent-call guard, which silently drops the
  completion and left the test waiting on a never-signalled TCS until
  the 5s WaitAsync timed out. Drive completion re-entrantly from inside
  the emission handler so both notifications stay on one thread.
- WhenThrottleUntilTrueFastReplacements_ThenLatestWins latched the first
  emission into a single-value TCS, so a wall-clock race where the
  earlier value's timer fired before the later value replaced it
  poisoned the result. Record all emissions and wait for the later
  value (whose timer is never superseded), then assert the final
  observed value is the latest.

Test-only; no product behaviour changes.
@ChrisPulman ChrisPulman enabled auto-merge (squash) June 25, 2026 13:22
@codecov

codecov Bot commented Jun 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.90%. Comparing base (e3ab4fe) to head (fb48262).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main      #98      +/-   ##
==========================================
+ Coverage   90.79%   90.90%   +0.10%     
==========================================
  Files         651      651              
  Lines       20455    20464       +9     
  Branches     2454     2454              
==========================================
+ Hits        18573    18602      +29     
+ Misses       1507     1490      -17     
+ Partials      375      372       -3     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@sonarqubecloud

Copy link
Copy Markdown

@glennawatson glennawatson merged commit 84302b4 into main Jun 25, 2026
13 checks passed
@glennawatson glennawatson deleted the fix/flaky-throttle-tests branch June 25, 2026 13:35
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