Flaky under load: Galaxy EventPumpBoundedChannelTests.Drops_newest_when_channel_fills_and_records_metric #503
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found while running the full unit-test sweep on merged master (
28c28667). Pre-existing and unrelated to that merge —EventPumpBoundedChannelTests.csis untouched by it, and the file's last change (560b327e) predates the work.Test:
ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests.Runtime.EventPumpBoundedChannelTests.Drops_newest_when_channel_fills_and_records_metricFailure:
Shouldly.ShouldAssertException : counters.Received— the drop metric did not reach the expected count.Reproduction (measured):
Driver.Galaxy.Testsin isolation -> 0 failures.Runtime.Tests+AdminUI.Tests+Core.Testsrunning concurrently -> 1 failure (run 11).So it needs CPU contention, like the #500 family did. Recipe that catches it:
Always capture
trx— a summary-only run loses the test name and costs a full re-run to recover.Likely shape (not yet confirmed — I did not diagnose this one). The test drives a bounded channel to overflow and asserts a drop count. Counts like that are usually observed after a producer/consumer race, so the assertion can be evaluated before the drops have been recorded, or the consumer can drain fast enough under different scheduling that fewer drops occur than the test assumes. Worth checking which of those it is before changing anything — #500 had two hypotheses that looked obvious and were both wrong under measurement.
See #500 for the presence-vs-absence assertion rule, which resolved eight tests of a similar character in
Runtime.Tests.Fixed on
fix/issues-503-501-493-499-488, commit95295210.The "likely shape" in the issue was wrong, and so was my own first fix. It is not a producer/consumer timing race. It is cross-test measurement leakage.
EventPump'sMeteris static (one per process — the correct production shape, so a host-level listener catches every pump), andMeterListener.InstrumentPublishedcan only select by meter.StartMeterCapturetherefore collected measurements from everyEventPumpalive anywhere in the assembly. xUnit runs test classes in parallel and several build pumps —EventPumpStreamFaultTests, and theDriver-Xtest in this same file. A sibling's events landed in these counters. That is exactly why it needed a busy box: it needed two tests to overlap in time. Measured directly during the gate:Received = 11in a run that emitted 10.The third test in the file already filtered on the
galaxy.clienttag and even documents why. The broken one did not. Fixed by filtering the capture on a per-test unique tag (PumpTest-{guid}), not a literal.Why the failure surfaced as
counters.Received. TheReceived == Dispatched + Dropped + InFlightassertion could only ever fail spuriously:InFlightwas derived asReceived - Dispatched - Dropped, making the identity a tautology. A foreign increment landing between its four separateInterlocked.Readcalls is the only way to break it. Deleted, along with theInFlightmember; the three measured counters are asserted directly.Three more defects in the same test, all found on the way:
OnDataChangeisEventHandler<T>— void-returning — soasync (_, _) => await gateis async void: it returns to the loop at the firstawaitand blocks nothing. MeasuredDispatched = 2..3, never 0. Drops occurred only when the producer happened to outrun the consumer, so the arrangement the doc comment describes was fiction. Now a synchronousManualResetEventSlim.Wait()on the loop's own thread, pinned byDispatched.ShouldBe(0).Task.Delay(150)is now a presence poll on the drop count (#500's rule);Droppedstarts at 0 so it cannot be satisfied by the initial state.10 - 1 held - 2 buffered = 7) rather than scheduler-dependent.Gate release moved into
finally, ahead of disposal:DisposeAsyncawaits the dispatch loop, which is now genuinely parked, so a failed assertion would otherwise hang rather than fail.Verification: 15/15 clean under the exact recipe in this issue (full Galaxy project with Runtime + AdminUI + Core concurrent). Worth noting the intermediate state — with only the async-void/timing half fixed, the gate still failed on run 4, which is what exposed the leak. Falsifiability: restoring the async-void handler turns the new assertions red.
Test duration also dropped from ~220 ms to ~45 ms (no fixed sleep).