Skip to content

Commit 0de39c4

Browse files
committed
fix: Change thread local context to allow overlapped scopes
1 parent 4282d7a commit 0de39c4

File tree

2 files changed

+183
-37
lines changed

2 files changed

+183
-37
lines changed

opentelemetry/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
- Bug Fix: `InstrumentationScope` implementation for `PartialEq` and `Hash` fixed to include Attributes also.
1010
- *Breaking* Changed value type of `Baggage` from `Value` to `StringValue`
1111
- Updated `Baggage` constants to reflect latest standard (`MAX_KEY_VALUE_PAIRS` - 180 -> 64, `MAX_BYTES_FOR_ONE_PAIR` - removed) and increased insert performance see #[2284](https://github.com/open-telemetry/opentelemetry-rust/pull/2284).
12+
- Changed `Context` to use a stack to properly handle out of order dropping of `ContextGuard`. This imposes a limit of `65535` nested contexts on a single thread. See #[2378](https://github.com/open-telemetry/opentelemetry-rust/pull/2284) and #[1887](https://github.com/open-telemetry/opentelemetry-rust/issues/1887).
1213

1314
## 0.28.0
1415

opentelemetry/src/context.rs

Lines changed: 182 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::otel_warn;
12
#[cfg(feature = "trace")]
23
use crate::trace::context::SynchronizedSpan;
34
use std::any::{Any, TypeId};
@@ -9,7 +10,7 @@ use std::marker::PhantomData;
910
use std::sync::Arc;
1011

1112
thread_local! {
12-
static CURRENT_CONTEXT: RefCell<Context> = RefCell::new(Context::default());
13+
static CURRENT_CONTEXT: RefCell<ContextStack> = RefCell::new(ContextStack::default());
1314
}
1415

1516
/// An execution-scoped collection of values.
@@ -122,7 +123,7 @@ impl Context {
122123
/// Note: This function will panic if you attempt to attach another context
123124
/// while the current one is still borrowed.
124125
pub fn map_current<T>(f: impl FnOnce(&Context) -> T) -> T {
125-
CURRENT_CONTEXT.with(|cx| f(&cx.borrow()))
126+
CURRENT_CONTEXT.with(|cx| cx.borrow().map_current_cx(f))
126127
}
127128

128129
/// Returns a clone of the current thread's context with the given value.
@@ -298,12 +299,10 @@ impl Context {
298299
/// assert_eq!(Context::current().get::<ValueA>(), None);
299300
/// ```
300301
pub fn attach(self) -> ContextGuard {
301-
let previous_cx = CURRENT_CONTEXT
302-
.try_with(|current| current.replace(self))
303-
.ok();
302+
let cx_id = CURRENT_CONTEXT.with(|cx| cx.borrow_mut().push(self));
304303

305304
ContextGuard {
306-
previous_cx,
305+
cx_pos: cx_id,
307306
_marker: PhantomData,
308307
}
309308
}
@@ -344,17 +343,19 @@ impl fmt::Debug for Context {
344343
}
345344

346345
/// A guard that resets the current context to the prior context when dropped.
347-
#[allow(missing_debug_implementations)]
346+
#[derive(Debug)]
348347
pub struct ContextGuard {
349-
previous_cx: Option<Context>,
350-
// ensure this type is !Send as it relies on thread locals
348+
// The position of the context in the stack. This is used to pop the context.
349+
cx_pos: u16,
350+
// Ensure this type is !Send as it relies on thread locals
351351
_marker: PhantomData<*const ()>,
352352
}
353353

354354
impl Drop for ContextGuard {
355355
fn drop(&mut self) {
356-
if let Some(previous_cx) = self.previous_cx.take() {
357-
let _ = CURRENT_CONTEXT.try_with(|current| current.replace(previous_cx));
356+
let id = self.cx_pos;
357+
if id > ContextStack::BASE_POS && id < ContextStack::MAX_POS {
358+
CURRENT_CONTEXT.with(|context_stack| context_stack.borrow_mut().pop_id(id));
358359
}
359360
}
360361
}
@@ -381,17 +382,115 @@ impl Hasher for IdHasher {
381382
}
382383
}
383384

385+
/// A stack for keeping track of the [`Context`] instances that have been attached
386+
/// to a thread.
387+
///
388+
/// The stack allows for popping of contexts by position, which is used to do out
389+
/// of order dropping of [`ContextGuard`] instances. Only when the top of the
390+
/// stack is popped, the topmost [`Context`] is actually restored.
391+
///
392+
/// The stack relies on the fact that it is thread local and that the
393+
/// [`ContextGuard`] instances that are constructed using it can't be shared with
394+
/// other threads.
395+
struct ContextStack {
396+
/// This is the current [`Context`] that is active on this thread, and the top
397+
/// of the [`ContextStack`]. It is always present, and if the `stack` is empty
398+
/// it's an empty [`Context`].
399+
///
400+
/// Having this here allows for fast access to the current [`Context`].
401+
current_cx: Context,
402+
/// A `stack` of the other contexts that have been attached to the thread.
403+
stack: Vec<Option<Context>>,
404+
/// Ensure this type is !Send as it relies on thread locals
405+
_marker: PhantomData<*const ()>,
406+
}
407+
408+
impl ContextStack {
409+
const BASE_POS: u16 = 0;
410+
const MAX_POS: u16 = u16::MAX;
411+
const INITIAL_CAPACITY: usize = 8;
412+
413+
#[inline(always)]
414+
fn push(&mut self, cx: Context) -> u16 {
415+
// The next id is the length of the `stack`, plus one since we have the
416+
// top of the [`ContextStack`] as the `current_cx`.
417+
let next_id = self.stack.len() + 1;
418+
if next_id < ContextStack::MAX_POS.into() {
419+
let current_cx = std::mem::replace(&mut self.current_cx, cx);
420+
self.stack.push(Some(current_cx));
421+
next_id as u16
422+
} else {
423+
// This is an overflow, log it and ignore it.
424+
otel_warn!(
425+
name: "Context.AttachFailed",
426+
message = format!("Too many contexts. Max limit is {}. \
427+
Context::current() remains unchanged as this attach failed. \
428+
Dropping the returned ContextGuard will have no impact on Context::current()",
429+
ContextStack::MAX_POS)
430+
);
431+
ContextStack::MAX_POS
432+
}
433+
}
434+
435+
#[inline(always)]
436+
fn pop_id(&mut self, pos: u16) {
437+
if pos == ContextStack::BASE_POS || pos == ContextStack::MAX_POS {
438+
// The empty context is always at the bottom of the [`ContextStack`]
439+
// and cannot be popped, and the overflow position is invalid, so do
440+
// nothing.
441+
return;
442+
}
443+
let len: u16 = self.stack.len() as u16;
444+
// Are we at the top of the [`ContextStack`]?
445+
if pos == len {
446+
// Shrink the stack if possible to clear out any out of order pops.
447+
while let Some(None) = self.stack.last() {
448+
_ = self.stack.pop();
449+
}
450+
// Restore the previous context. This will always happen since the
451+
// empty context is always at the bottom of the stack if the
452+
// [`ContextStack`] is not empty.
453+
if let Some(Some(next_cx)) = self.stack.pop() {
454+
self.current_cx = next_cx;
455+
}
456+
} else {
457+
// This is an out of order pop.
458+
if pos >= len {
459+
// This is an invalid id, ignore it.
460+
return;
461+
}
462+
// Clear out the entry at the given id.
463+
_ = self.stack[pos as usize].take();
464+
}
465+
}
466+
467+
#[inline(always)]
468+
fn map_current_cx<T>(&self, f: impl FnOnce(&Context) -> T) -> T {
469+
f(&self.current_cx)
470+
}
471+
}
472+
473+
impl Default for ContextStack {
474+
fn default() -> Self {
475+
ContextStack {
476+
current_cx: Context::default(),
477+
stack: Vec::with_capacity(ContextStack::INITIAL_CAPACITY),
478+
_marker: PhantomData,
479+
}
480+
}
481+
}
482+
384483
#[cfg(test)]
385484
mod tests {
386485
use super::*;
387486

487+
#[derive(Debug, PartialEq)]
488+
struct ValueA(u64);
489+
#[derive(Debug, PartialEq)]
490+
struct ValueB(u64);
491+
388492
#[test]
389493
fn context_immutable() {
390-
#[derive(Debug, PartialEq)]
391-
struct ValueA(u64);
392-
#[derive(Debug, PartialEq)]
393-
struct ValueB(u64);
394-
395494
// start with Current, which should be an empty context
396495
let cx = Context::current();
397496
assert_eq!(cx.get::<ValueA>(), None);
@@ -424,66 +523,56 @@ mod tests {
424523

425524
#[test]
426525
fn nested_contexts() {
427-
#[derive(Debug, PartialEq)]
428-
struct ValueA(&'static str);
429-
#[derive(Debug, PartialEq)]
430-
struct ValueB(u64);
431-
let _outer_guard = Context::new().with_value(ValueA("a")).attach();
526+
let _outer_guard = Context::new().with_value(ValueA(1)).attach();
432527

433528
// Only value `a` is set
434529
let current = Context::current();
435-
assert_eq!(current.get(), Some(&ValueA("a")));
530+
assert_eq!(current.get(), Some(&ValueA(1)));
436531
assert_eq!(current.get::<ValueB>(), None);
437532

438533
{
439534
let _inner_guard = Context::current_with_value(ValueB(42)).attach();
440535
// Both values are set in inner context
441536
let current = Context::current();
442-
assert_eq!(current.get(), Some(&ValueA("a")));
537+
assert_eq!(current.get(), Some(&ValueA(1)));
443538
assert_eq!(current.get(), Some(&ValueB(42)));
444539

445540
assert!(Context::map_current(|cx| {
446-
assert_eq!(cx.get(), Some(&ValueA("a")));
541+
assert_eq!(cx.get(), Some(&ValueA(1)));
447542
assert_eq!(cx.get(), Some(&ValueB(42)));
448543
true
449544
}));
450545
}
451546

452547
// Resets to only value `a` when inner guard is dropped
453548
let current = Context::current();
454-
assert_eq!(current.get(), Some(&ValueA("a")));
549+
assert_eq!(current.get(), Some(&ValueA(1)));
455550
assert_eq!(current.get::<ValueB>(), None);
456551

457552
assert!(Context::map_current(|cx| {
458-
assert_eq!(cx.get(), Some(&ValueA("a")));
553+
assert_eq!(cx.get(), Some(&ValueA(1)));
459554
assert_eq!(cx.get::<ValueB>(), None);
460555
true
461556
}));
462557
}
463558

464559
#[test]
465-
#[ignore = "overlapping contexts are not supported yet"]
466560
fn overlapping_contexts() {
467-
#[derive(Debug, PartialEq)]
468-
struct ValueA(&'static str);
469-
#[derive(Debug, PartialEq)]
470-
struct ValueB(u64);
471-
472-
let outer_guard = Context::new().with_value(ValueA("a")).attach();
561+
let outer_guard = Context::new().with_value(ValueA(1)).attach();
473562

474563
// Only value `a` is set
475564
let current = Context::current();
476-
assert_eq!(current.get(), Some(&ValueA("a")));
565+
assert_eq!(current.get(), Some(&ValueA(1)));
477566
assert_eq!(current.get::<ValueB>(), None);
478567

479568
let inner_guard = Context::current_with_value(ValueB(42)).attach();
480569
// Both values are set in inner context
481570
let current = Context::current();
482-
assert_eq!(current.get(), Some(&ValueA("a")));
571+
assert_eq!(current.get(), Some(&ValueA(1)));
483572
assert_eq!(current.get(), Some(&ValueB(42)));
484573

485574
assert!(Context::map_current(|cx| {
486-
assert_eq!(cx.get(), Some(&ValueA("a")));
575+
assert_eq!(cx.get(), Some(&ValueA(1)));
487576
assert_eq!(cx.get(), Some(&ValueB(42)));
488577
true
489578
}));
@@ -492,7 +581,7 @@ mod tests {
492581

493582
// `inner_guard` is still alive so both `ValueA` and `ValueB` should still be accessible
494583
let current = Context::current();
495-
assert_eq!(current.get(), Some(&ValueA("a")));
584+
assert_eq!(current.get(), Some(&ValueA(1)));
496585
assert_eq!(current.get(), Some(&ValueB(42)));
497586

498587
drop(inner_guard);
@@ -502,4 +591,60 @@ mod tests {
502591
assert_eq!(current.get::<ValueA>(), None);
503592
assert_eq!(current.get::<ValueB>(), None);
504593
}
594+
595+
#[test]
596+
fn too_many_contexts() {
597+
let mut guards: Vec<ContextGuard> = Vec::with_capacity(ContextStack::MAX_POS as usize);
598+
let stack_max_pos = ContextStack::MAX_POS as u64;
599+
// Fill the stack up until the last position
600+
for i in 1..stack_max_pos {
601+
let cx_guard = Context::current().with_value(ValueB(i)).attach();
602+
assert_eq!(Context::current().get(), Some(&ValueB(i)));
603+
assert_eq!(cx_guard.cx_pos, i as u16);
604+
guards.push(cx_guard);
605+
}
606+
// Let's overflow the stack a couple of times
607+
for _ in 0..16 {
608+
let cx_guard = Context::current().with_value(ValueA(1)).attach();
609+
assert_eq!(cx_guard.cx_pos, ContextStack::MAX_POS);
610+
assert_eq!(Context::current().get::<ValueA>(), None);
611+
assert_eq!(Context::current().get(), Some(&ValueB(stack_max_pos - 1)));
612+
guards.push(cx_guard);
613+
}
614+
// Drop the overflow contexts
615+
for _ in 0..16 {
616+
guards.pop();
617+
assert_eq!(Context::current().get::<ValueA>(), None);
618+
assert_eq!(Context::current().get(), Some(&ValueB(stack_max_pos - 1)));
619+
}
620+
// Drop one more so we can add a new one
621+
guards.pop();
622+
assert_eq!(Context::current().get::<ValueA>(), None);
623+
assert_eq!(Context::current().get(), Some(&ValueB(stack_max_pos - 2)));
624+
// Push a new context and see that it works
625+
let cx_guard = Context::current().with_value(ValueA(2)).attach();
626+
assert_eq!(cx_guard.cx_pos, ContextStack::MAX_POS - 1);
627+
assert_eq!(Context::current().get(), Some(&ValueA(2)));
628+
assert_eq!(Context::current().get(), Some(&ValueB(stack_max_pos - 2)));
629+
guards.push(cx_guard);
630+
// Let's overflow the stack a couple of times again
631+
for _ in 0..16 {
632+
let cx_guard = Context::current().with_value(ValueA(1)).attach();
633+
assert_eq!(cx_guard.cx_pos, ContextStack::MAX_POS);
634+
assert_eq!(Context::current().get(), Some(&ValueA(2)));
635+
assert_eq!(Context::current().get(), Some(&ValueB(stack_max_pos - 2)));
636+
guards.push(cx_guard);
637+
}
638+
}
639+
640+
#[test]
641+
fn context_stack_pop_id() {
642+
// This is to get full line coverage of the `pop_id` function.
643+
// In real life the `Drop`` implementation of `ContextGuard` ensures that
644+
// the ids are valid and inside the bounds.
645+
let mut stack = ContextStack::default();
646+
stack.pop_id(ContextStack::BASE_POS);
647+
stack.pop_id(ContextStack::MAX_POS);
648+
stack.pop_id(4711);
649+
}
505650
}

0 commit comments

Comments
 (0)