Skip to content

EDDN uploader: fix a lost-wakeup race that could strand a queued message (flaky bridge test) - #221

Open
demortes wants to merge 1 commit into
mainfrom
fix/flaky-eddn-batching-test
Open

demortes wants to merge 1 commit into
mainfrom
fix/flaky-eddn-batching-test

Conversation

@demortes

Copy link
Copy Markdown
Contributor

What was flaky, and what it really was

EddnBridgeBatchingTests.A_run_of_signals_is_sent_as_one_message_when_the_next_event_arrives failed occasionally (one upload where two were expected). I first suspected the fixed Task.Delay windows elsewhere in that file, but this test contains no sleeps at all, and it was not a test-timing problem. It is a real race in production code, in EddnUploader's background worker.

The race

WorkerAsync decided "queue empty, I'm done" under the lock, but only marked itself idle (_running = false) in a second lock acquisition, in its finally block. An Enqueue that landed between the two saw a worker still running, started none, and the worker then exited without the new message. In a real session that is an EDDN upload left unsent until the next upload happens, and lost outright if the app closes first. The bridge test just happens to hit the window: the first message is finishing as the second is queued.

Reproduction (before changing anything)

The same scenario run 3000 times in-process on 16 threads: 2 failures in 3000, always CallCount == 1 (the signals message lost), reproduced in 2 of 3 runs.

The fix

"queue empty" and "I am idle" are now one decision under one lock. The finally only clears the flag for an exit that did not go through that branch (cancellation mid-send, or a fault). After a normal exit the flag already belongs to whichever worker an Enqueue has since started; clearing it there (the obvious fix) would let a second worker send concurrently, which would break upload ordering. DisposeAsync's snapshot of the worker is unaffected: any message enqueued before disposal is either picked up by the running worker or starts a new one whose task is stored under the same lock.

Tests (new: EddnUploaderHandOffTests)

A race this narrow cannot be pinned by one run, so these make the window busy: thousands of uploaders on 16 threads.

  • A_message_enqueued_as_the_worker_goes_idle_is_never_stranded: fails on the original code 4/4 runs.
  • Only_one_worker_sends_at_a_time: fails on the original code and on the naive fix (flag cleared unconditionally in finally) about half the runs, which is why it is a guard and not the primary test. (I first wrote a version of this that did not catch the naive fix and said nothing; that one was reworked and re-checked against the mutation.)
  • Messages_queued_after_the_worker_has_gone_idle_start_it_again: a plain baseline.

Verified

  • The original bridge-level scenario, 60,000 iterations on the fixed code: 0 failures (vs ~2 per 3000 before).
  • The new tests: 25 consecutive runs, 0 failures; all Eddn tests 10 runs, 0 failures.
  • Whole solution twice: 24 + 28 + 186 + 421 + 1110 tests, all passing, 0 build warnings.

Limits

  • The two stress tests are probabilistic by nature. The first caught the original bug in every run I tried; the second catches the naive fix about half the time. Neither can fail on correct code, since they assert only invariants (nothing lost, never two senders).
  • I did not touch the Task.Delay windows in the other tests in that file; they are not what failed.

🤖 Generated with Claude Code

The background worker decided 'queue empty, I am done' under the lock but only
marked itself idle in a second lock acquisition (its finally block). An Enqueue
landing between the two saw a worker still running and started none, and the
worker then exited without the new message. In a real session that is an EDDN
message left unsent until the next upload, and lost if the app closes first.

The empty-queue decision and the idle flag are now one step under one lock. The
finally block only clears the flag for an exit that did not go through that
branch (cancellation or a fault): after a normal exit the flag already belongs to
whichever worker an Enqueue has since started, and clearing it there would let a
second worker send concurrently.

This is what made EddnBridgeBatchingTests.A_run_of_signals_is_sent_as_one_message_
when_the_next_event_arrives flaky (one upload where two were expected, about 1 in
1500 under load): not a timing problem in the test but a real race in production
code. Reproduced before the fix, then 60,000 iterations of the same scenario on
the fixed code with no failures.

Adds EddnUploaderHandOffTests: a stress test that fails on the original code 4/4
runs, and a concurrency test that fails on the original code and on the obvious
naive fix (clear the flag in finally) about half the time, and passes on the fix
(25 consecutive runs). EDNexus.Tests 1110 passed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 18, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 46 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 8f64ba5b-4792-4588-bca7-c17120e4652d
📥 Commits

Reviewing files that changed from the base of the PR and between 7008a05 and c41ccf8.

📒 Files selected for processing: 2
  • src/EliteDangerous.Eddn/EddnUploader.cs
  • tests/EDNexus.Tests/Reporting/EddnUploaderHandOffTests.cs
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

This branch has not been deployed

No deployments
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