test(server): fix unit tests that sometimes capture no log events - #895
elyasmnvidian wants to merge 1 commit into
Conversation
Signed-off-by: Elyas Mehtabuddin <emehtabuddin@nvidia.com>
e14ff7f to
998abd8
Compare
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository: NVIDIA-NeMo/Switchyard/.coderabbit.yaml Review profile: CHILL Plan: Enterprise Run ID: 📒 Files selected for processing (4)
Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 9 remain after this review. WalkthroughThe server adds a test-only tracing subscriber helper. Three tests use it instead of ChangesTracing test support
Priority: ⬇️ Low Estimated code review effort: 2 (Simple) | ~10 minutes Merge Risk: ⚪ Minimal · up to No actionable merge-blocking risk is identified; this test-only change is mergeable after normal checks. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
I’m a rabbit with a tracing tune, Comment |
What
sse::tests::stream_client_error_warn_redacts_upstream_bodyfails in about 2% of runs of the server unit tests onmain(31 of 1,450 runs at 47bba06).cargo testruns these tests in parallel by default. In a failing run, the test captures no log events, so it panics at the check that thestream iteration failedwarning was logged:The test never reaches the check it exists for: that the warning leaves out the upstream error body. With this PR, the test failed in 0 of 2,100 runs. The method is in the evidence section below.
This PR adds one test helper,
with_subscriber, incrates/switchyard-server/src/testing.rs. The first time a test calls it, it sets a globaltracing_subscriber::Registrywith no layers for the rest of the test binary. Then it runs the test's code with the test's own subscriber throughtracing::subscriber::with_default, as before. All three places in this crate's unit tests that capture tracing output now call the helper:captured_eventsinlib.rs, which three request-log tests userequest_span_continues_incoming_w3c_trace_contextinobservability.rsProduction code does not change.
Why
tracingkeeps a cache for each call site, which is one logging macro in the source, such as onewarn!line. The cache records whether any subscriber wants that call site's events.tracingfills the cache the first time any thread reaches the call site. When exactly one subscriber is registered,tracingdoes not ask that subscriber. Instead, it asks the default subscriber of the thread that reached the call site (seeDispatchers::rebuilderin tracing-core 0.1.36,callsite.rs). A subscriber set withwith_defaultcounts as registered, even though only one thread uses it. On a thread with no subscriber, the default subscriber does nothing and answers "never".The SSE test sets its capturing subscriber only on its own thread. Two other tests in the same module,
stream_error_terminates_without_done_markerandanthropic_stream_error_uses_api_error_type, reach the samewarn!("stream iteration failed")line inframe_streamwith no subscriber. Here is one bad ordering (an illustration, not a log):stream_error_terminates_without_done_markerreacheswarn!("stream iteration failed")on thread 2 for the first time.tracingasks thread 2's default subscriber, which answers "never", and caches that answer for this line.tracingskips the event, and the test's list of captured events stays empty.The global
Registrystays registered for the life of the process, and it wants every call site. While it is the only subscriber, a thread without its own subscriber uses it. Once the test's subscriber is also registered,tracingasks both subscribers. In either case, the cache cannot say "never". TheRegistryhas no layers, so it writes no output and keeps no event data.The request-log tests and the request-span test do not fail today. No other unit test in this crate reaches their call sites without a subscriber. But they capture output the same way, and a test that forces the bad ordering makes each of them fail like the SSE test (see the evidence below). With one helper, the explanation stays in one place, and new capture tests get the same protection.
Two other changes would also prevent the failure, but each costs more.
--test-threads=1would run every test in the binary one at a time. A shared lock would have to go into every test that reaches the same log line, including tests that do not capture logs.Notes for reviewers
Start with
crates/switchyard-server/src/testing.rs. In the other three files, one call changes fromtracing::subscriber::with_defaultto the helper.lib.rsalso declares thetestingmodule.Costs and limits:
Registryexists only in the unit test binary, because the module is#[cfg(test)]. The integration tests incrates/switchyard-server/tests/build separate binaries and do not change.Registry. TheRegistrykeeps the span's data until the span closes.initialize_observability, that call returns an error whenwith_subscriberran first. No unit test does this today. In the opposite order,with_subscriberignores the error, because any global subscriber keeps the cache from saying "never".Evidence: failure counts before and after
Method: I built the server unit test binary with
cargo test -p switchyard-server --lib --no-run, once onmain(47bba06) and once with this change. I copied both binaries and ran them fromcrates/switchyard-server. Each run executes all 29 unit tests with the default number of parallel test threads. The machine was an Apple M3 Pro with 12 cores, and other builds were running at the same time (load average 11 to 48). "10 busy CPU workers" means 10yes > /dev/nullprocesses running during the loop.Every failure before the change was the same panic at
sse.rs:247:9with[]. No other test failed in any run. The busy CPU workers made the failure rarer, not more common.Evidence: tests that force the bad ordering
I wrote three throwaway tests, one per call site; they are not part of this PR. Each test first reaches the call site on a new thread that has no subscriber. Then it reaches the call site again on the test thread, inside the capturing subscriber. I ran each test alone with
--exact: 20 times withtracing::subscriber::with_default, and 20 times withwith_subscriber.tracing::subscriber::with_defaultwith_subscriberwarn!("stream iteration failed")insse.rs[]"LLM request cancelled"event inlib.rsrequest_spaninobservability.rs00000000000000000000000000000000The SSE version, added to the
testsmodule insse.rs:This test fails on every run. If you replace
tracing::subscriber::with_defaultwithcrate::testing::with_subscriber, it passes on every run.Summary by CodeRabbit