Skip to main content

query_runtime/observability/recorder/
owner.rs

1//! Shared terminal ownership, lifecycle fence, and observer delivery.
2
3#[cfg(test)]
4use std::sync::atomic::{AtomicBool, Ordering};
5use std::{
6    cell::RefCell,
7    fmt,
8    sync::{Arc, Condvar, Mutex, MutexGuard},
9};
10
11#[cfg(any(test, feature = "test-hooks"))]
12use super::{AttemptReportProvider, RuntimeObservationFooter};
13use super::{
14    AttemptReportSealProvider, DeferredRuntimeObservationSeal, LiveRuntimeObservationSnapshot, QueryBoundaryKind,
15    QueryCleanupSealDelivery, QueryId, RuntimeObservationClock, RuntimeObservationConfig, RuntimeObservationHandle,
16    RuntimeObservationPayload, RuntimeObservationSealObserver, RuntimeObservationSession, RuntimeTerminalOutcome,
17    SealedRuntimeObservationSnapshot, TraceId,
18};
19
20/// Cloneable shared ownership of one query observation session.
21///
22/// Clones carry the same terminal authority and the recorder's first-terminal
23/// rule makes sealing idempotent. Execution contexts still receive only
24/// [`RuntimeObservationHandle`].
25#[derive(Clone)]
26pub struct RuntimeObservationOwner {
27    session: Arc<RuntimeObservationSession>,
28    lifecycle: Arc<RuntimeObservationOwnerLifecycle>,
29    // A fixed report for tests that seal an owner with no attempt. In
30    // production an attempt passes its own report provider to each seal, the
31    // only report source.
32    #[cfg(any(test, feature = "test-hooks"))]
33    attempt_report_provider: Option<AttemptReportProvider>,
34}
35
36#[derive(Debug)]
37pub(super) struct RuntimeObservationOwnerLifecycle {
38    state: Mutex<RuntimeObservationOwnerLifecycleState>,
39    finished: Condvar,
40}
41
42/// Test-only barrier at the start of ordinary deferred observer delivery.
43///
44/// The terminal seal commit deliberately runs before this barrier. It lets
45/// lifecycle tests prove that report-ready does not depend on observer
46/// delivery or capacity reuse.
47#[cfg(test)]
48#[derive(Debug)]
49pub(crate) struct RuntimeObservationDeliveryGate {
50    entered: AtomicBool,
51    gate: std::sync::Barrier,
52}
53
54#[cfg(test)]
55impl RuntimeObservationDeliveryGate {
56    pub(crate) fn new() -> Self {
57        Self { entered: AtomicBool::new(false), gate: std::sync::Barrier::new(2) }
58    }
59
60    pub(crate) fn entered(&self) -> bool {
61        self.entered.load(Ordering::Acquire)
62    }
63
64    pub(crate) fn open(&self) {
65        self.gate.wait();
66    }
67
68    /// Marks delivery as held and waits until the test opens the gate.
69    pub(super) fn hold(&self) {
70        self.entered.store(true, Ordering::Release);
71        self.gate.wait();
72    }
73}
74
75#[derive(Debug)]
76pub(super) enum RuntimeObservationOwnerLifecycleState {
77    Open {
78        cleanup_started: bool,
79        pending_outcome: Option<RuntimeTerminalOutcome>,
80    },
81    Finishing {
82        late_observers: Vec<RuntimeObservationSealObserver>,
83    },
84    Delivering {
85        snapshot: Option<SealedRuntimeObservationSnapshot>,
86        late_observers: Vec<RuntimeObservationSealObserver>,
87    },
88    Sealed(Option<SealedRuntimeObservationSnapshot>),
89}
90
91thread_local! {
92    static RUNTIME_OBSERVATION_DELIVERY_STACK: RefCell<Vec<Option<*const RuntimeObservationOwnerLifecycle>>> =
93        const { RefCell::new(Vec::new()) };
94}
95
96/// Returns whether the current thread is synchronously delivering a runtime-observation seal observer.
97///
98/// Backend close-and-wait uses this to reject synchronous reentry, which would
99/// otherwise wait for the observer currently executing on this thread.
100pub(crate) fn in_runtime_observation_delivery() -> bool {
101    RUNTIME_OBSERVATION_DELIVERY_STACK.with(|stack| !stack.borrow().is_empty())
102}
103
104fn in_owner_observation_delivery(lifecycle: &Arc<RuntimeObservationOwnerLifecycle>) -> bool {
105    let lifecycle = Arc::as_ptr(lifecycle);
106    RUNTIME_OBSERVATION_DELIVERY_STACK
107        .with(|stack| stack.borrow().iter().any(|active| active.is_some_and(|active| active == lifecycle)))
108}
109
110pub(super) struct RuntimeObservationDeliveryGuard {
111    lifecycle: Option<*const RuntimeObservationOwnerLifecycle>,
112}
113
114impl RuntimeObservationDeliveryGuard {
115    pub(super) fn enter(lifecycle: Option<&Arc<RuntimeObservationOwnerLifecycle>>) -> Self {
116        let lifecycle = lifecycle.map(Arc::as_ptr);
117        RUNTIME_OBSERVATION_DELIVERY_STACK.with(|stack| stack.borrow_mut().push(lifecycle));
118        Self { lifecycle }
119    }
120}
121
122impl Drop for RuntimeObservationDeliveryGuard {
123    fn drop(&mut self) {
124        RUNTIME_OBSERVATION_DELIVERY_STACK.with(|stack| {
125            let active = stack.borrow_mut().pop();
126            debug_assert_eq!(active, Some(self.lifecycle));
127        });
128    }
129}
130
131impl Default for RuntimeObservationOwnerLifecycle {
132    fn default() -> Self {
133        Self {
134            state: Mutex::new(RuntimeObservationOwnerLifecycleState::Open {
135                cleanup_started: false,
136                pending_outcome: None,
137            }),
138            finished: Condvar::new(),
139        }
140    }
141}
142
143impl fmt::Debug for RuntimeObservationOwner {
144    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
145        formatter
146            .debug_struct("RuntimeObservationOwner")
147            .field("query_id", &self.query_id().map(QueryId::as_str))
148            .field("enabled", &self.handle().is_enabled())
149            .finish_non_exhaustive()
150    }
151}
152
153impl RuntimeObservationOwner {
154    /// Creates shared owner authority for a new query observation session.
155    pub fn new(query_id: impl Into<QueryId>, trace_id: Option<TraceId>, config: RuntimeObservationConfig) -> Self {
156        Self::from_session(RuntimeObservationSession::new(query_id, trace_id, config))
157    }
158
159    /// Creates shared owner authority with an injected monotonic clock.
160    pub fn with_clock(
161        query_id: impl Into<QueryId>,
162        trace_id: Option<TraceId>,
163        config: RuntimeObservationConfig,
164        clock: Arc<dyn RuntimeObservationClock>,
165    ) -> Self {
166        Self::from_session(RuntimeObservationSession::with_clock(query_id, trace_id, config, clock))
167    }
168
169    /// Wraps an existing unique session in shared one-shot ownership.
170    pub fn from_session(session: RuntimeObservationSession) -> Self {
171        let owner = Self {
172            session: Arc::new(session),
173            lifecycle: Arc::new(RuntimeObservationOwnerLifecycle::default()),
174            #[cfg(any(test, feature = "test-hooks"))]
175            attempt_report_provider: None,
176        };
177        owner.handle().record(|| RuntimeObservationPayload::QueryBoundary { boundary: QueryBoundaryKind::Started });
178        owner
179    }
180
181    /// Installs a construction-time fixed attempt-summary provider.
182    #[cfg(any(test, feature = "test-hooks"))]
183    #[doc(hidden)]
184    pub fn with_attempt_report_provider(mut self, provider: AttemptReportProvider) -> Self {
185        self.attempt_report_provider = Some(provider);
186        self
187    }
188
189    #[cfg(any(test, feature = "test-hooks"))]
190    fn fixed_report_provider(&self) -> Option<AttemptReportSealProvider> {
191        self.attempt_report_provider.as_ref().map(|provider| {
192            let provider = Arc::clone(provider);
193            Arc::new(move |footer: &RuntimeObservationFooter, _: &_| {
194                let mut report = provider();
195                report.set_observation_completeness(footer);
196                Arc::new(report)
197            }) as AttemptReportSealProvider
198        })
199    }
200
201    /// Creates a disabled shared owner.
202    pub fn disabled() -> Self {
203        Self::from_session(RuntimeObservationSession::disabled())
204    }
205
206    /// Returns cloneable recording access without terminal authority.
207    pub fn handle(&self) -> RuntimeObservationHandle {
208        self.session.handle()
209    }
210
211    /// Returns the immutable query identity.
212    pub fn query_id(&self) -> Option<&QueryId> {
213        self.session.query_id()
214    }
215
216    /// Returns the immutable optional trace identity.
217    pub fn trace_id(&self) -> Option<&TraceId> {
218        self.session.trace_id()
219    }
220
221    /// Returns the immutable recorder configuration used to create this owner.
222    pub fn config(&self) -> RuntimeObservationConfig {
223        self.session.config()
224    }
225
226    /// Fixes the first terminal and returns the shared sealed snapshot.
227    pub fn seal(&self, outcome: RuntimeTerminalOutcome) -> Option<SealedRuntimeObservationSnapshot> {
228        let outcome = loop {
229            let mut lifecycle = self.lock_lifecycle();
230            match &mut *lifecycle {
231                RuntimeObservationOwnerLifecycleState::Open { .. } => {
232                    *lifecycle = RuntimeObservationOwnerLifecycleState::Finishing { late_observers: Vec::new() };
233                    break outcome;
234                },
235                RuntimeObservationOwnerLifecycleState::Finishing { .. } => {
236                    lifecycle = match self.lifecycle.finished.wait(lifecycle) {
237                        Ok(lifecycle) => lifecycle,
238                        Err(poisoned) => poisoned.into_inner(),
239                    };
240                    if let RuntimeObservationOwnerLifecycleState::Sealed(snapshot) = &*lifecycle {
241                        return snapshot.clone();
242                    }
243                },
244                RuntimeObservationOwnerLifecycleState::Delivering { snapshot, .. } => {
245                    if in_owner_observation_delivery(&self.lifecycle) {
246                        return snapshot.clone();
247                    }
248                    lifecycle = match self.lifecycle.finished.wait(lifecycle) {
249                        Ok(lifecycle) => lifecycle,
250                        Err(poisoned) => poisoned.into_inner(),
251                    };
252                    if let RuntimeObservationOwnerLifecycleState::Sealed(snapshot) = &*lifecycle {
253                        return snapshot.clone();
254                    }
255                },
256                RuntimeObservationOwnerLifecycleState::Sealed(snapshot) => return snapshot.clone(),
257            }
258        };
259        self.finish_owner_terminal(Some(outcome), None, |_| {})
260    }
261
262    /// Retains one plan or admission outcome until the query owner completes cleanup.
263    ///
264    /// The first non-success outcome remains authoritative, so later cleanup or
265    /// cancellation cannot reclassify an already structured native failure.
266    pub(crate) fn record_terminal_outcome(&self, outcome: RuntimeTerminalOutcome) {
267        let mut lifecycle = self.lock_lifecycle();
268        if let RuntimeObservationOwnerLifecycleState::Open { pending_outcome, .. } = &mut *lifecycle {
269            *pending_outcome = Some(match pending_outcome.take() {
270                None | Some(RuntimeTerminalOutcome::Succeeded) => outcome,
271                Some(existing) => existing,
272            });
273        }
274    }
275
276    /// Records that query-owned cleanup has begun without sealing the recorder.
277    pub(crate) fn begin_query_cleanup(&self) {
278        let should_record = {
279            let mut lifecycle = self.lock_lifecycle();
280            match &mut *lifecycle {
281                RuntimeObservationOwnerLifecycleState::Open { cleanup_started, .. } if !*cleanup_started => {
282                    *cleanup_started = true;
283                    true
284                },
285                RuntimeObservationOwnerLifecycleState::Open { .. }
286                | RuntimeObservationOwnerLifecycleState::Finishing { .. }
287                | RuntimeObservationOwnerLifecycleState::Delivering { .. }
288                | RuntimeObservationOwnerLifecycleState::Sealed(_) => false,
289            }
290        };
291        if should_record {
292            self.handle()
293                .record(|| RuntimeObservationPayload::QueryBoundary { boundary: QueryBoundaryKind::CleanupStarted });
294        }
295    }
296
297    /// Seals after query-owned cleanup, using `fallback` only when execution
298    /// never produced a more specific outcome.
299    pub(crate) fn finish_query_cleanup(
300        &self,
301        fallback: RuntimeTerminalOutcome,
302    ) -> Option<SealedRuntimeObservationSnapshot> {
303        self.seal_query_cleanup(fallback).deliver()
304    }
305
306    /// Records cleanup completion and seals the recorder state so the report
307    /// is published, returning the deferred observer delivery. Capacity release
308    /// may proceed once this returns. [`QueryCleanupSealDelivery::deliver`] runs
309    /// afterwards and never gates reuse.
310    pub(crate) fn seal_query_cleanup(&self, fallback: RuntimeTerminalOutcome) -> QueryCleanupSealDelivery {
311        self.seal_query_cleanup_with(CleanupSeal::Recorded { fallback: Some(fallback) })
312    }
313
314    fn seal_query_cleanup_with(&self, seal: CleanupSeal) -> QueryCleanupSealDelivery {
315        self.begin_query_cleanup();
316        let outcome = loop {
317            let mut lifecycle = self.lock_lifecycle();
318            match &mut *lifecycle {
319                RuntimeObservationOwnerLifecycleState::Open { pending_outcome, .. } => {
320                    let pending_outcome = pending_outcome.take();
321                    *lifecycle = RuntimeObservationOwnerLifecycleState::Finishing { late_observers: Vec::new() };
322                    break match seal {
323                        CleanupSeal::Recorded { fallback } => pending_outcome.or(fallback),
324                    };
325                },
326                RuntimeObservationOwnerLifecycleState::Finishing { .. } => {
327                    lifecycle = match self.lifecycle.finished.wait(lifecycle) {
328                        Ok(lifecycle) => lifecycle,
329                        Err(poisoned) => poisoned.into_inner(),
330                    };
331                    if let RuntimeObservationOwnerLifecycleState::Sealed(snapshot) = &*lifecycle {
332                        return QueryCleanupSealDelivery::already_sealed(snapshot.clone());
333                    }
334                },
335                RuntimeObservationOwnerLifecycleState::Delivering { snapshot, .. } => {
336                    if in_owner_observation_delivery(&self.lifecycle) {
337                        return QueryCleanupSealDelivery::already_sealed(snapshot.clone());
338                    }
339                    lifecycle = match self.lifecycle.finished.wait(lifecycle) {
340                        Ok(lifecycle) => lifecycle,
341                        Err(poisoned) => poisoned.into_inner(),
342                    };
343                    if let RuntimeObservationOwnerLifecycleState::Sealed(snapshot) = &*lifecycle {
344                        return QueryCleanupSealDelivery::already_sealed(snapshot.clone());
345                    }
346                },
347                RuntimeObservationOwnerLifecycleState::Sealed(snapshot) => {
348                    return QueryCleanupSealDelivery::already_sealed(snapshot.clone());
349                },
350            }
351        };
352        let deferred = self.seal_owner_terminal(outcome, None, |outcome| {
353            self.handle()
354                .record(|| RuntimeObservationPayload::QueryBoundary { boundary: QueryBoundaryKind::CleanupFinished });
355            if let Some(outcome) = outcome {
356                let boundary = query_terminal_boundary(outcome);
357                self.handle().record(|| RuntimeObservationPayload::QueryBoundary { boundary });
358            }
359        });
360        QueryCleanupSealDelivery {
361            owner: Some(self.clone()),
362            deferred: Some(deferred),
363            already_sealed: None,
364            #[cfg(test)]
365            delivery_gate: None,
366        }
367    }
368
369    /// Completes a query that has no admitted handle responsible for cleanup.
370    ///
371    /// `None` is reserved for coordinator-owned records that have no attempt
372    /// lifecycle. Their observation still seals, and its terminal field stays
373    /// absent. The seal does not synthesize an engine attempt outcome from
374    /// delivery data.
375    pub fn finish_query(&self, outcome: Option<RuntimeTerminalOutcome>) -> Option<SealedRuntimeObservationSnapshot> {
376        match outcome {
377            Some(outcome) => {
378                self.record_terminal_outcome(outcome.clone());
379                self.finish_query_cleanup(outcome)
380            },
381            None => self.seal_query_cleanup_with(CleanupSeal::Recorded { fallback: None }).deliver(),
382        }
383    }
384
385    /// Returns a live incomplete snapshot before terminal sealing.
386    pub fn live_snapshot(&self) -> Option<LiveRuntimeObservationSnapshot> {
387        self.session.live_snapshot()
388    }
389
390    /// Returns the immutable sealed snapshot once the first terminal outcome is fixed.
391    pub fn sealed_snapshot(&self) -> Option<SealedRuntimeObservationSnapshot> {
392        self.session.sealed_snapshot()
393    }
394
395    /// Registers an infallible notification for this session's immutable seal.
396    ///
397    /// The observer runs at most once for this registration and outside
398    /// recorder locks. Registrations racing terminal publication join the
399    /// owner's delivery fence; registrations after that fence has completed
400    /// deliver the existing snapshot immediately. A panic is isolated from
401    /// query execution and cleanup.
402    pub fn register_seal_observer<F>(&self, observer: F)
403    where F: Fn(SealedRuntimeObservationSnapshot) + Send + Sync + 'static {
404        let mut observer = Some(RuntimeObservationSealObserver { notify: Box::new(observer) });
405        let immediate = {
406            let mut lifecycle = self.lock_lifecycle();
407            match &mut *lifecycle {
408                RuntimeObservationOwnerLifecycleState::Open { .. } => {
409                    self.handle().register_seal_observer(observer.take().expect("observer exists"))
410                },
411                RuntimeObservationOwnerLifecycleState::Finishing { late_observers }
412                | RuntimeObservationOwnerLifecycleState::Delivering { late_observers, .. } => {
413                    late_observers.push(observer.take().expect("observer exists"));
414                    None
415                },
416                RuntimeObservationOwnerLifecycleState::Sealed(snapshot) => {
417                    snapshot.clone().map(|snapshot| (observer.take().expect("observer exists"), snapshot))
418                },
419            }
420        };
421        if let Some((observer, snapshot)) = immediate {
422            observer.invoke(snapshot);
423        }
424    }
425
426    fn finish_owner_terminal<F>(
427        &self,
428        outcome: Option<RuntimeTerminalOutcome>,
429        attempt_report: Option<AttemptReportSealProvider>,
430        before_seal: F,
431    ) -> Option<SealedRuntimeObservationSnapshot>
432    where
433        F: FnOnce(Option<&RuntimeTerminalOutcome>),
434    {
435        let deferred = self.seal_owner_terminal(outcome, attempt_report, before_seal);
436        self.complete_observer_delivery(deferred)
437    }
438
439    fn seal_owner_terminal<F>(
440        &self,
441        outcome: Option<RuntimeTerminalOutcome>,
442        attempt_report: Option<AttemptReportSealProvider>,
443        before_seal: F,
444    ) -> DeferredRuntimeObservationSeal
445    where
446        F: FnOnce(Option<&RuntimeTerminalOutcome>),
447    {
448        let finishing = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
449            before_seal(outcome.as_ref());
450            #[cfg(any(test, feature = "test-hooks"))]
451            let attempt_report = attempt_report.or_else(|| self.fixed_report_provider());
452            let attempt_report_provider = outcome.as_ref().and(attempt_report);
453            self.session.seal_deferred(outcome, attempt_report_provider.as_ref())
454        }));
455        match finishing {
456            Ok(deferred) => deferred,
457            Err(panic) => {
458                self.handle().mark_degraded();
459                let deferred = self.session.seal_deferred(Some(RuntimeTerminalOutcome::Panicked), None);
460                let _ = self.complete_observer_delivery(deferred);
461                std::panic::resume_unwind(panic);
462            },
463        }
464    }
465
466    pub(super) fn complete_observer_delivery(
467        &self,
468        deferred: DeferredRuntimeObservationSeal,
469    ) -> Option<SealedRuntimeObservationSnapshot> {
470        let snapshot = deferred.snapshot.clone();
471        let mut observers = deferred.observers;
472        {
473            let mut lifecycle = self.lock_lifecycle();
474            let late_observers = match &mut *lifecycle {
475                RuntimeObservationOwnerLifecycleState::Finishing { late_observers } => std::mem::take(late_observers),
476                _ => unreachable!("terminal owner must publish before observer delivery"),
477            };
478            if snapshot.is_some() {
479                observers.extend(late_observers);
480            }
481            *lifecycle = RuntimeObservationOwnerLifecycleState::Delivering {
482                snapshot: snapshot.clone(),
483                late_observers: Vec::new(),
484            };
485        }
486        let _delivery = RuntimeObservationDeliveryGuard::enter(Some(&self.lifecycle));
487        loop {
488            if let Some(snapshot) = &snapshot {
489                for observer in observers.drain(..) {
490                    observer.invoke(snapshot.clone());
491                }
492            }
493            let mut lifecycle = self.lock_lifecycle();
494            let late_observers = match &mut *lifecycle {
495                RuntimeObservationOwnerLifecycleState::Delivering { late_observers, .. } => {
496                    std::mem::take(late_observers)
497                },
498                _ => unreachable!("observer delivery must retain terminal owner state"),
499            };
500            if late_observers.is_empty() || snapshot.is_none() {
501                *lifecycle = RuntimeObservationOwnerLifecycleState::Sealed(snapshot.clone());
502                self.lifecycle.finished.notify_all();
503                return snapshot;
504            }
505            observers = late_observers;
506        }
507    }
508
509    pub(super) fn lock_lifecycle(&self) -> MutexGuard<'_, RuntimeObservationOwnerLifecycleState> {
510        match self.lifecycle.state.lock() {
511            Ok(lifecycle) => lifecycle,
512            Err(poisoned) => poisoned.into_inner(),
513        }
514    }
515}
516
517impl Default for RuntimeObservationOwner {
518    fn default() -> Self {
519        Self::disabled()
520    }
521}
522
523/// How a cleanup seal picks its terminal outcome.
524enum CleanupSeal {
525    /// A recorded pending outcome wins over `fallback`; the report, if any,
526    /// comes from the owner's construction-time provider.
527    Recorded { fallback: Option<RuntimeTerminalOutcome> },
528}
529
530fn query_terminal_boundary(outcome: &RuntimeTerminalOutcome) -> QueryBoundaryKind {
531    match outcome {
532        RuntimeTerminalOutcome::Succeeded => QueryBoundaryKind::Succeeded,
533        RuntimeTerminalOutcome::Failed { .. } => QueryBoundaryKind::Failed,
534        RuntimeTerminalOutcome::Cancelled => QueryBoundaryKind::Cancelled,
535        RuntimeTerminalOutcome::Dropped => QueryBoundaryKind::Dropped,
536        RuntimeTerminalOutcome::Panicked => QueryBoundaryKind::Panicked,
537    }
538}