Skip to content

Commit 36c48db

Browse files
chore: context stabilization tests & logging pieces (#2833)
Co-authored-by: Cijo Thomas <[email protected]>
1 parent b03296c commit 36c48db

File tree

3 files changed

+311
-14
lines changed

3 files changed

+311
-14
lines changed

opentelemetry/Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ internal-logs = ["tracing"]
4343
opentelemetry_sdk = { path = "../opentelemetry-sdk", features = ["spec_unstable_logs_enabled"]} # for documentation tests
4444
criterion = { workspace = true }
4545
rand = { workspace = true, features = ["os_rng", "thread_rng"] }
46+
tokio = { version = "1.0", features = ["full"] }
47+
futures = "0.3"
4648

4749
[[bench]]
4850
name = "metrics"

opentelemetry/src/context.rs

+234-6
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,15 @@ impl ContextStack {
460460
// The empty context is always at the bottom of the [`ContextStack`]
461461
// and cannot be popped, and the overflow position is invalid, so do
462462
// nothing.
463+
otel_warn!(
464+
name: "Context.OutOfOrderDrop",
465+
position = pos,
466+
message = if pos == ContextStack::BASE_POS {
467+
"Attempted to pop the base context which is not allowed"
468+
} else {
469+
"Attempted to pop the overflow position which is not allowed"
470+
}
471+
);
463472
return;
464473
}
465474
let len: u16 = self.stack.len() as u16;
@@ -479,6 +488,12 @@ impl ContextStack {
479488
// This is an out of order pop.
480489
if pos >= len {
481490
// This is an invalid id, ignore it.
491+
otel_warn!(
492+
name: "Context.PopOutOfBounds",
493+
position = pos,
494+
stack_length = len,
495+
message = "Attempted to pop beyond the end of the context stack"
496+
);
482497
return;
483498
}
484499
// Clear out the entry at the given id.
@@ -505,6 +520,8 @@ impl Default for ContextStack {
505520
#[cfg(test)]
506521
mod tests {
507522
use super::*;
523+
use std::time::Duration;
524+
use tokio::time::sleep;
508525

509526
#[derive(Debug, PartialEq)]
510527
struct ValueA(u64);
@@ -653,20 +670,231 @@ mod tests {
653670
for _ in 0..16 {
654671
let cx_guard = Context::current().with_value(ValueA(1)).attach();
655672
assert_eq!(cx_guard.cx_pos, ContextStack::MAX_POS);
656-
assert_eq!(Context::current().get(), Some(&ValueA(2)));
673+
assert_eq!(Context::current().get::<ValueA>(), Some(&ValueA(2)));
657674
assert_eq!(Context::current().get(), Some(&ValueB(stack_max_pos - 2)));
658675
guards.push(cx_guard);
659676
}
660677
}
661678

679+
/// Tests that a new ContextStack is created with the correct initial capacity.
680+
#[test]
681+
fn test_initial_capacity() {
682+
let stack = ContextStack::default();
683+
assert_eq!(stack.stack.capacity(), ContextStack::INITIAL_CAPACITY);
684+
}
685+
686+
/// Tests that map_current_cx correctly accesses the current context.
687+
#[test]
688+
fn test_map_current_cx() {
689+
let mut stack = ContextStack::default();
690+
let test_value = ValueA(42);
691+
stack.current_cx = Context::new().with_value(test_value);
692+
693+
let result = stack.map_current_cx(|cx| {
694+
assert_eq!(cx.get::<ValueA>(), Some(&ValueA(42)));
695+
true
696+
});
697+
assert!(result);
698+
}
699+
700+
/// Tests popping contexts in non-sequential order.
701+
#[test]
702+
fn test_pop_id_out_of_order() {
703+
let mut stack = ContextStack::default();
704+
705+
// Push three contexts
706+
let cx1 = Context::new().with_value(ValueA(1));
707+
let cx2 = Context::new().with_value(ValueA(2));
708+
let cx3 = Context::new().with_value(ValueA(3));
709+
710+
let id1 = stack.push(cx1);
711+
let id2 = stack.push(cx2);
712+
let id3 = stack.push(cx3);
713+
714+
// Pop middle context first - should not affect current context
715+
stack.pop_id(id2);
716+
assert_eq!(stack.current_cx.get::<ValueA>(), Some(&ValueA(3)));
717+
assert_eq!(stack.stack.len(), 3); // Length unchanged for middle pops
718+
719+
// Pop last context - should restore previous valid context
720+
stack.pop_id(id3);
721+
assert_eq!(stack.current_cx.get::<ValueA>(), Some(&ValueA(1)));
722+
assert_eq!(stack.stack.len(), 1);
723+
724+
// Pop first context - should restore to empty state
725+
stack.pop_id(id1);
726+
assert_eq!(stack.current_cx.get::<ValueA>(), None);
727+
assert_eq!(stack.stack.len(), 0);
728+
}
729+
730+
/// Tests edge cases in context stack operations. IRL these should log
731+
/// warnings, and definitely not panic.
662732
#[test]
663-
fn context_stack_pop_id() {
664-
// This is to get full line coverage of the `pop_id` function.
665-
// In real life the `Drop`` implementation of `ContextGuard` ensures that
666-
// the ids are valid and inside the bounds.
733+
fn test_pop_id_edge_cases() {
667734
let mut stack = ContextStack::default();
735+
736+
// Test popping BASE_POS - should be no-op
668737
stack.pop_id(ContextStack::BASE_POS);
738+
assert_eq!(stack.stack.len(), 0);
739+
740+
// Test popping MAX_POS - should be no-op
669741
stack.pop_id(ContextStack::MAX_POS);
670-
stack.pop_id(4711);
742+
assert_eq!(stack.stack.len(), 0);
743+
744+
// Test popping invalid position - should be no-op
745+
stack.pop_id(1000);
746+
assert_eq!(stack.stack.len(), 0);
747+
748+
// Test popping from empty stack - should be safe
749+
stack.pop_id(1);
750+
assert_eq!(stack.stack.len(), 0);
751+
}
752+
753+
/// Tests stack behavior when reaching maximum capacity.
754+
/// Once we push beyond this point, we should end up with a context
755+
/// that points _somewhere_, but mutating it should not affect the current
756+
/// active context.
757+
#[test]
758+
fn test_push_overflow() {
759+
let mut stack = ContextStack::default();
760+
let max_pos = ContextStack::MAX_POS as usize;
761+
762+
// Fill stack up to max position
763+
for i in 0..max_pos {
764+
let cx = Context::new().with_value(ValueA(i as u64));
765+
let id = stack.push(cx);
766+
assert_eq!(id, (i + 1) as u16);
767+
}
768+
769+
// Try to push beyond capacity
770+
let cx = Context::new().with_value(ValueA(max_pos as u64));
771+
let id = stack.push(cx);
772+
assert_eq!(id, ContextStack::MAX_POS);
773+
774+
// Verify current context remains unchanged after overflow
775+
assert_eq!(
776+
stack.current_cx.get::<ValueA>(),
777+
Some(&ValueA((max_pos - 2) as u64))
778+
);
779+
}
780+
781+
/// Tests that:
782+
/// 1. Parent context values are properly propagated to async operations
783+
/// 2. Values added during async operations do not affect parent context
784+
#[tokio::test]
785+
async fn test_async_context_propagation() {
786+
// A nested async operation we'll use to test propagation
787+
async fn nested_operation() {
788+
// Verify we can see the parent context's value
789+
assert_eq!(
790+
Context::current().get::<ValueA>(),
791+
Some(&ValueA(42)),
792+
"Parent context value should be available in async operation"
793+
);
794+
795+
// Create new context
796+
let cx_with_both = Context::current()
797+
.with_value(ValueA(43)) // override ValueA
798+
.with_value(ValueB(24)); // Add new ValueB
799+
800+
// Run nested async operation with both values
801+
async {
802+
// Verify both values are available
803+
assert_eq!(
804+
Context::current().get::<ValueA>(),
805+
Some(&ValueA(43)),
806+
"Parent value should still be available after adding new value"
807+
);
808+
assert_eq!(
809+
Context::current().get::<ValueB>(),
810+
Some(&ValueB(24)),
811+
"New value should be available in async operation"
812+
);
813+
814+
// Do some async work to simulate real-world scenario
815+
sleep(Duration::from_millis(10)).await;
816+
817+
// Values should still be available after async work
818+
assert_eq!(
819+
Context::current().get::<ValueA>(),
820+
Some(&ValueA(43)),
821+
"Parent value should persist across await points"
822+
);
823+
assert_eq!(
824+
Context::current().get::<ValueB>(),
825+
Some(&ValueB(24)),
826+
"New value should persist across await points"
827+
);
828+
}
829+
.with_context(cx_with_both)
830+
.await;
831+
}
832+
833+
// Set up initial context with ValueA
834+
let parent_cx = Context::new().with_value(ValueA(42));
835+
836+
// Create and run async operation with the parent context explicitly propagated
837+
nested_operation().with_context(parent_cx.clone()).await;
838+
839+
// After async operation completes:
840+
// 1. Parent context should be unchanged
841+
assert_eq!(
842+
parent_cx.get::<ValueA>(),
843+
Some(&ValueA(42)),
844+
"Parent context should be unchanged"
845+
);
846+
assert_eq!(
847+
parent_cx.get::<ValueB>(),
848+
None,
849+
"Parent context should not see values added in async operation"
850+
);
851+
852+
// 2. Current context should be back to default
853+
assert_eq!(
854+
Context::current().get::<ValueA>(),
855+
None,
856+
"Current context should be back to default"
857+
);
858+
assert_eq!(
859+
Context::current().get::<ValueB>(),
860+
None,
861+
"Current context should not have async operation's values"
862+
);
863+
}
864+
865+
///
866+
/// Tests that unnatural parent->child relationships in nested async
867+
/// operations behave properly.
868+
///
869+
#[tokio::test]
870+
async fn test_out_of_order_context_detachment_futures() {
871+
// This function returns a future, but doesn't await it
872+
// It will complete before the future that it creates.
873+
async fn create_a_future() -> impl std::future::Future<Output = ()> {
874+
// Create a future that will do some work, referencing our current
875+
// context, but don't await it.
876+
async {
877+
assert_eq!(Context::current().get::<ValueA>(), Some(&ValueA(42)));
878+
879+
// Longer work
880+
sleep(Duration::from_millis(50)).await;
881+
}
882+
.with_context(Context::current())
883+
}
884+
885+
// Create our base context
886+
let parent_cx = Context::new().with_value(ValueA(42));
887+
888+
// await our nested function, which will create and detach a context
889+
let future = create_a_future().with_context(parent_cx).await;
890+
891+
// Execute the future. The future that created it is long gone, but this shouldn't
892+
// cause issues.
893+
let _a = future.await;
894+
895+
// Nothing terrible (e.g., panics!) should happen, and we should definitely not have any
896+
// values attached to our current context that were set in the nested operations.
897+
assert_eq!(Context::current().get::<ValueA>(), None);
898+
assert_eq!(Context::current().get::<ValueB>(), None);
671899
}
672900
}

0 commit comments

Comments
 (0)