Skip to content

Commit c412f66

Browse files
fix(test): await tracing span closure before assertions
Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
1 parent 560bb3a commit c412f66

5 files changed

Lines changed: 126 additions & 25 deletions

File tree

‎crates/openshell-server/src/compute/mod.rs‎

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14118,7 +14118,6 @@ mod tests {
1411814118
/// Driver watch events arrive on a background stream, so the store writes
1411914119
/// they trigger land outside the request that caused them.
1412014120
#[tokio::test]
14121-
#[ignore = "flaky under concurrent test execution"]
1412214121
async fn driver_watch_events_are_roots_and_store_operations_have_parents() {
1412314122
use crate::otel_tracing::test_exporter;
1412414123

@@ -14133,20 +14132,12 @@ mod tests {
1413314132
.await
1413414133
.unwrap();
1413514134

14135+
let root = traced.wait_for_span("driver_watch.sandbox_deleted").await;
1413614136
let spans = traced.finished_spans();
14137-
let root = spans
14138-
.iter()
14139-
.find(|s| s.name == "driver_watch.sandbox_deleted")
14140-
.unwrap_or_else(|| {
14141-
panic!(
14142-
"the event records a span of its own, got {:?}",
14143-
spans.iter().map(|s| &s.name).collect::<Vec<_>>()
14144-
)
14145-
});
1414614137

14147-
test_exporter::assert_is_root(root);
14138+
test_exporter::assert_is_root(&root);
1414814139
assert_eq!(
14149-
test_exporter::attribute(root, "sandbox.id").as_deref(),
14140+
test_exporter::attribute(&root, "sandbox.id").as_deref(),
1415014141
Some("sb-1"),
1415114142
"the span names which sandbox the driver reported on"
1415214143
);
@@ -14164,7 +14155,6 @@ mod tests {
1416414155
/// The reconciler runs on a timer with no inbound request, so without a
1416514156
/// span of its own each store call becomes its own anonymous trace.
1416614157
#[tokio::test]
14167-
#[ignore = "flaky under concurrent test execution"]
1416814158
async fn reconcile_sweeps_are_roots_and_operations_have_parents() {
1416914159
use crate::otel_tracing::test_exporter;
1417014160

@@ -14179,9 +14169,8 @@ mod tests {
1417914169
.await
1418014170
.unwrap();
1418114171

14182-
// Other tests drive their own reconcile loops into the shared
14183-
// exporter, so match on the shape of a sweep rather than assuming
14184-
// there is exactly one.
14172+
// A closed sweep has no remaining worker-held child spans.
14173+
traced.wait_for_span("reconcile.sandboxes").await;
1418514174
let spans = traced.finished_spans();
1418614175
let roots = traced.spans_named("reconcile.sandboxes");
1418714176
assert!(

‎crates/openshell-server/src/grpc/sandbox.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4543,7 +4543,6 @@ mod tests {
45434543
}
45444544

45454545
#[tokio::test]
4546-
#[ignore = "flaky under concurrent test execution"]
45474546
async fn watch_producer_releases_request_span_when_client_disconnects() {
45484547
use crate::otel_tracing::test_exporter;
45494548
use tokio_stream::StreamExt as _;
@@ -4582,6 +4581,7 @@ mod tests {
45824581

45834582
drop(request_span);
45844583
stream.disconnect_and_wait().await;
4584+
traced.wait_for_span("disconnected_watch_request").await;
45854585

45864586
assert_eq!(
45874587
traced.spans_named("disconnected_watch_request").len(),

‎crates/openshell-server/src/otel_tracing.rs‎

Lines changed: 110 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ where
146146
/// Isolated in-memory span exporters for tracing tests.
147147
#[cfg(test)]
148148
pub mod test_exporter {
149-
use std::sync::OnceLock;
149+
use std::sync::{Arc, OnceLock};
150150

151151
use tracing::{Dispatch, Subscriber, dispatcher::WeakDispatch};
152152

@@ -159,6 +159,7 @@ pub mod test_exporter {
159159
struct CloseWithDispatch<S> {
160160
inner: S,
161161
dispatch: OnceLock<WeakDispatch>,
162+
closed: Arc<tokio::sync::Notify>,
162163
}
163164

164165
impl<S: Subscriber> Subscriber for CloseWithDispatch<S> {
@@ -224,7 +225,14 @@ pub mod test_exporter {
224225
.get()
225226
.and_then(WeakDispatch::upgrade)
226227
.expect("a live span keeps its test dispatcher alive");
227-
tracing::dispatcher::with_default(&dispatch, || self.inner.try_close(id))
228+
let closed = tracing::dispatcher::with_default(&dispatch, || self.inner.try_close(id));
229+
if closed {
230+
// The simple exporter has finished before try_close returns.
231+
// Wake assertions only after the owning registry and layers
232+
// have completed cleanup, including recursive parent closure.
233+
self.closed.notify_waiters();
234+
}
235+
closed
228236
}
229237

230238
fn current_span(&self) -> tracing_core::span::Current {
@@ -273,14 +281,17 @@ pub mod test_exporter {
273281
.with_simple_exporter(exporter.clone())
274282
.build();
275283
let subscriber = tracing_subscriber::registry().with(super::layer(&provider, None));
284+
let closed = Arc::new(tokio::sync::Notify::new());
276285
let dispatch = Dispatch::new(CloseWithDispatch {
277286
inner: subscriber,
278287
dispatch: OnceLock::new(),
288+
closed: Arc::clone(&closed),
279289
});
280290
TracingTestGuard {
281291
_default: tracing::dispatcher::set_default(&dispatch),
282292
_provider: provider,
283293
exporter,
294+
closed,
284295
_lock: lock,
285296
}
286297
}
@@ -291,6 +302,52 @@ pub mod test_exporter {
291302
self.exporter.get_finished_spans().expect("in-memory spans")
292303
}
293304

305+
/// Wait for expected spans to finish before taking an assertion snapshot.
306+
///
307+
/// A completed `SQLx` query may still have its span held by a `SQLite`
308+
/// worker. Export is synchronous once the span closes, but flushing
309+
/// cannot close that live span. Await closure notifications instead of
310+
/// assuming the query result also means tracing cleanup has completed.
311+
pub async fn wait_for_spans(
312+
&self,
313+
predicate: impl Fn(&[opentelemetry_sdk::trace::SpanData]) -> bool + Send + Sync,
314+
) -> Vec<opentelemetry_sdk::trace::SpanData> {
315+
tokio::time::timeout(std::time::Duration::from_secs(5), async {
316+
loop {
317+
// notify_waiters wakes futures created before notification,
318+
// even before polling. Subscribe before reading so closure
319+
// between the snapshot and await cannot lose a wakeup.
320+
let notified = self.closed.notified();
321+
let spans = self.finished_spans();
322+
if predicate(&spans) {
323+
return spans;
324+
}
325+
notified.await;
326+
}
327+
})
328+
.await
329+
.unwrap_or_else(|_| {
330+
panic!(
331+
"timed out waiting for expected spans, got {:?}",
332+
self.finished_spans()
333+
.iter()
334+
.map(|span| &span.name)
335+
.collect::<Vec<_>>()
336+
)
337+
})
338+
}
339+
340+
/// Wait for the completed span named `name`.
341+
pub async fn wait_for_span(&self, name: &str) -> opentelemetry_sdk::trace::SpanData {
342+
let spans = self
343+
.wait_for_spans(|spans| spans.iter().any(|span| span.name == name))
344+
.await;
345+
spans
346+
.into_iter()
347+
.find(|span| span.name == name)
348+
.expect("the awaited snapshot contains the expected span")
349+
}
350+
294351
/// Spans named `name`.
295352
pub fn spans_named(&self, name: &str) -> Vec<opentelemetry_sdk::trace::SpanData> {
296353
self.finished_spans()
@@ -378,6 +435,7 @@ pub mod test_exporter {
378435
_default: tracing::dispatcher::DefaultGuard,
379436
_provider: opentelemetry_sdk::trace::SdkTracerProvider,
380437
exporter: opentelemetry_sdk::trace::InMemorySpanExporter,
438+
closed: Arc<tokio::sync::Notify>,
381439
_lock: std::sync::MutexGuard<'static, ()>,
382440
}
383441

@@ -690,6 +748,56 @@ mod tests {
690748
);
691749
}
692750

751+
#[tokio::test]
752+
async fn tracing_waits_for_worker_held_spans_to_close() {
753+
let traced = test_exporter::install_traced();
754+
let parent = tracing::info_span!("awaited_parent");
755+
let child = tracing::info_span!(parent: &parent, "awaited_child");
756+
drop(parent);
757+
758+
let (release, released) = std::sync::mpsc::channel();
759+
let worker = std::thread::spawn(move || {
760+
released.recv().expect("test releases the worker's span");
761+
drop(child);
762+
});
763+
764+
assert!(traced.finished_spans().is_empty());
765+
let waiting = traced.wait_for_span("awaited_parent");
766+
tokio::pin!(waiting);
767+
assert!(futures::poll!(waiting.as_mut()).is_pending());
768+
769+
release.send(()).unwrap();
770+
let parent = waiting.await;
771+
worker.join().expect("worker closes child and parent");
772+
let child = traced.span_named("awaited_child");
773+
test_exporter::assert_is_root(&parent);
774+
assert_eq!(child.parent_span_id, parent.span_context.span_id());
775+
}
776+
777+
#[tokio::test]
778+
async fn tracing_wait_does_not_lose_closure_between_snapshot_and_await() {
779+
let traced = test_exporter::install_traced();
780+
let span = std::sync::Mutex::new(Some(tracing::info_span!("close_before_await")));
781+
let spans = traced
782+
.wait_for_spans(|spans| {
783+
// The first snapshot is empty. Close its span before polling
784+
// the notification future, as a worker could do concurrently.
785+
drop(span.lock().unwrap().take());
786+
spans.iter().any(|span| span.name == "close_before_await")
787+
})
788+
.await;
789+
assert_eq!(spans.len(), 1);
790+
test_exporter::assert_is_root(&spans[0]);
791+
}
792+
793+
#[tokio::test(start_paused = true)]
794+
#[should_panic(expected = "timed out waiting for expected spans")]
795+
async fn tracing_wait_times_out_when_a_span_never_closes() {
796+
let traced = test_exporter::install_traced();
797+
let _span = tracing::info_span!("still_open");
798+
traced.wait_for_span("still_open").await;
799+
}
800+
693801
#[tokio::test]
694802
async fn tracing_events_are_not_exported() {
695803
let traced = test_exporter::install_traced();

‎crates/openshell-server/src/persistence/tests.rs‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ async fn failed_store_calls_are_marked_on_the_span() {
3535
/// the span must stay clean — otherwise every lease a replica does not win, and
3636
/// every gateway restart, exports as a failure.
3737
#[tokio::test]
38-
#[ignore = "flaky under concurrent test execution"]
3938
async fn expected_conflicts_leave_the_span_unmarked() {
4039
use crate::otel_tracing::test_exporter;
4140

@@ -66,6 +65,7 @@ async fn expected_conflicts_leave_the_span_unmarked() {
6665
.await
6766
.expect_err("the name is already taken");
6867

68+
traced.wait_for_span("store.put_if").await;
6969
let span = traced.span_with("store.put_if", "object.id", "expected-conflict-second");
7070

7171
assert_eq!(
@@ -79,7 +79,6 @@ async fn expected_conflicts_leave_the_span_unmarked() {
7979
/// Span names stay low-cardinality so they group across object types; what
8080
/// each call touched is carried as attributes.
8181
#[tokio::test]
82-
#[ignore = "flaky under concurrent test execution"]
8382
async fn store_spans_record_what_they_touched_as_attributes() {
8483
use crate::otel_tracing::test_exporter;
8584

@@ -97,6 +96,13 @@ async fn store_spans_record_what_they_touched_as_attributes() {
9796
.unwrap();
9897
store.list("sandbox", "default", 10, 0).await.unwrap();
9998

99+
traced
100+
.wait_for_spans(|spans| {
101+
["store.get", "store.get_by_name", "store.list"]
102+
.iter()
103+
.all(|name| spans.iter().any(|span| &span.name == name))
104+
})
105+
.await;
100106
let by_name = traced.span_with("store.get_by_name", "object.name", "my-sandbox");
101107
assert_eq!(
102108
test_exporter::attribute(&by_name, "object_type").as_deref(),
@@ -2807,7 +2813,6 @@ async fn membership_selector_escapes_adversarial_label_key() {
28072813
/// so a trace decomposes an RPC into the storage work it did rather than
28082814
/// bottoming out at the request boundary.
28092815
#[tokio::test]
2810-
#[ignore = "flaky under concurrent test execution"]
28112816
async fn store_operations_export_spans_with_parents() {
28122817
use tracing::Instrument as _;
28132818

@@ -2827,7 +2832,7 @@ async fn store_operations_export_spans_with_parents() {
28272832
.await;
28282833
drop(request_span);
28292834

2830-
let root = traced.span_named("request");
2835+
let root = traced.wait_for_span("request").await;
28312836
let spans = traced.finished_spans();
28322837
let child = spans
28332838
.iter()

‎crates/openshell-server/src/provider_refresh.rs‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3580,7 +3580,6 @@ mod tests {
35803580
}
35813581

35823582
#[tokio::test]
3583-
#[ignore = "flaky under concurrent test execution"]
35843583
async fn refresh_worker_records_a_root_span_only_when_a_state_has_work() {
35853584
use crate::otel_tracing::test_exporter;
35863585

@@ -3621,7 +3620,7 @@ mod tests {
36213620
Box::pin(run_refresh_worker_tick(&store, None, None))
36223621
.await
36233622
.unwrap();
3624-
test_exporter::assert_is_root(&traced.span_named("refresh.provider_credentials"));
3623+
test_exporter::assert_is_root(&traced.wait_for_span("refresh.provider_credentials").await);
36253624
}
36263625

36273626
#[test]

0 commit comments

Comments
 (0)