Skip to main content

obs_core/scope/
guard.rs

1//! `ScopeGuard` — RAII guard returned by `obs::scope!` /
2//! `obs::context!`. Pops the frame on drop and, for `Scope` kind,
3//! flushes the tail buffer when `seen_error == true`.
4//!
5//! Spec 13 §§ 2 + 6.
6
7use std::sync::Arc;
8
9use obs_proto::obs::v1::{ObsEnvelope, SamplingReason as PSamplingReason};
10
11use super::{ScopeField, ScopeFrame, ScopeKind, pop_frame, push_frame};
12use crate::observer::{Observer, enter_emit_envelope};
13
14/// RAII guard returned by `obs::scope!` and `obs::context!`. Dropping
15/// pops the frame; for `Scope` kind frames where any `>= ERROR`
16/// envelope was observed, the tail buffer is flushed back through the
17/// active observer with `sampling_reason = TAIL_ERROR`.
18#[must_use = "the scope guard is popped on Drop; bind to a name like `_scope`"]
19pub struct ScopeGuard {
20    /// `None` after `into_inner` so the destructor knows not to pop.
21    armed: bool,
22}
23
24impl std::fmt::Debug for ScopeGuard {
25    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        f.debug_struct("ScopeGuard")
27            .field("armed", &self.armed)
28            .finish()
29    }
30}
31
32impl ScopeGuard {
33    /// Push a `Scope` frame.
34    pub fn enter(fields: Vec<ScopeField>, tail_capacity: u16) -> Self {
35        let _ = push_frame(ScopeFrame::new(fields, ScopeKind::Scope, tail_capacity));
36        Self { armed: true }
37    }
38
39    /// Push a `Context` frame (no tail buffer).
40    pub fn enter_context(fields: Vec<ScopeField>) -> Self {
41        let _ = push_frame(ScopeFrame::new(fields, ScopeKind::Context, 0));
42        Self { armed: true }
43    }
44
45    /// Push a frame with explicit identity, used by the bridge to
46    /// stamp `(name, target)` for `obs::SpanTrace`.
47    pub fn enter_with_identity(
48        fields: Vec<ScopeField>,
49        kind: ScopeKind,
50        tail_capacity: u16,
51        name: &'static str,
52        target: &'static str,
53    ) -> Self {
54        let mut frame = ScopeFrame::new(fields, kind, tail_capacity);
55        frame.set_span_identity(name, target);
56        let _ = push_frame(frame);
57        Self { armed: true }
58    }
59
60    /// Push a fully-built [`ScopeFrame`] onto the active scope stack.
61    /// Used by [`super::ScopeFrameBuilder::push`] so external crates
62    /// can build a frame programmatically (instead of via the
63    /// `obs::scope!` macro) and own the resulting RAII guard.
64    /// Spec 94 D7-3.
65    pub fn enter_with_frame(frame: ScopeFrame) -> Self {
66        let _ = push_frame(frame);
67        Self { armed: true }
68    }
69
70    /// Detach the guard so the caller can layer it onto a
71    /// `Future::instrument(...)` (the future then re-applies the frame
72    /// on every poll). The frame is popped immediately so the caller
73    /// doesn't hold two copies.
74    #[must_use]
75    pub fn into_inner(mut self) -> ScopeFrame {
76        self.armed = false;
77        // Pop the live frame back out so the caller can transplant it
78        // onto an `Instrumented<F>`; if pop_frame returns None (because
79        // a child task already cleared its own stack), synthesise an
80        // empty frame so callers don't hit `Option::unwrap`.
81        pop_frame().unwrap_or_else(|| ScopeFrame::new(Vec::new(), ScopeKind::Scope, 64))
82    }
83}
84
85impl Drop for ScopeGuard {
86    fn drop(&mut self) {
87        if !self.armed {
88            return;
89        }
90        let Some(mut frame) = pop_frame() else {
91            return;
92        };
93        if frame.kind() != ScopeKind::Scope {
94            return;
95        }
96        if !frame.seen_error() {
97            return;
98        }
99        flush_tail_buffer(&mut frame);
100    }
101}
102
103fn flush_tail_buffer(frame: &mut ScopeFrame) {
104    let observer = crate::observer::observer();
105    flush_through(&observer, frame);
106}
107
108fn flush_through(observer: &Arc<dyn Observer>, frame: &mut ScopeFrame) {
109    for mut env in frame.drain_tail() {
110        env.sampling_reason =
111            ::buffa::EnumValue::Known(PSamplingReason::SAMPLING_REASON_TAIL_ERROR);
112        // Cannot recurse through enter_emit_envelope because the
113        // outer emit already cleared CAN_ENTER. Instead, dispatch
114        // directly: tail flush happens *after* the original error's
115        // emit completes so re-entry is safe.
116        flush_one(observer, env);
117    }
118}
119
120fn flush_one(observer: &Arc<dyn Observer>, env: ObsEnvelope) {
121    // Route through `enter_emit_envelope` so the CAN_ENTER re-entry
122    // guard (spec 11 § 3.1) is held during the dispatch — protects
123    // against a sink that synthesises events during tail flush.
124    enter_emit_envelope(observer, env);
125}