1use std::{
4 collections::BTreeMap,
5 fmt,
6 sync::{
7 Arc, Condvar, Mutex, MutexGuard, PoisonError,
8 atomic::{AtomicBool, Ordering},
9 },
10};
11
12#[cfg(test)]
13use super::RuntimeObservationDeliveryGate;
14use super::{
15 CleanupFailureTally, LiveRuntimeObservationSnapshot, PlanExecutionId, QueryAttemptReport, QueryId,
16 RetainedTopologyRecord, RuntimeEventContext, RuntimeObservationClassLoss, RuntimeObservationClock,
17 RuntimeObservationConfig, RuntimeObservationDeliveryGuard, RuntimeObservationFooter, RuntimeObservationHandle,
18 RuntimeObservationManifest, RuntimeObservationOwner, RuntimeObservationPayload, RuntimeObservationReasonLoss,
19 RuntimeObservationRecordClass, RuntimeObservationRecordDisposition, RuntimeObservationRecorderState,
20 RuntimeObservationSnapshotData, RuntimeTerminalOutcome, RuntimeTimelineEvent, RuntimeTopologyRecord,
21 SealedRuntimeObservationSnapshot, SourceLocationDigestKeyState, TraceId,
22};
23#[cfg(any(test, feature = "test-hooks"))]
24use super::{SystemRuntimeObservationClock, TestSourceLocationDigestKeyProvider};
25
26#[derive(Debug, Default)]
31pub struct RuntimeObservationSession {
32 pub(super) handle: RuntimeObservationHandle,
33}
34
35#[cfg(any(test, feature = "test-hooks"))]
40pub(crate) type AttemptReportProvider = Arc<dyn Fn() -> QueryAttemptReport + Send + Sync>;
41
42pub(crate) type AttemptReportSealProvider =
45 Arc<dyn Fn(&RuntimeObservationFooter, &CleanupFailureTally) -> Arc<QueryAttemptReport> + Send + Sync>;
46
47pub(super) struct RuntimeObservationSessionInner {
48 pub(super) manifest: RuntimeObservationManifest,
49 pub(super) source_location_digest_key: SourceLocationDigestKeyState,
50 pub(super) clock: Arc<dyn RuntimeObservationClock>,
51 pub(super) state: Mutex<RuntimeObservationRecorderState>,
52 pub(super) sealed: AtomicBool,
53 pub(super) seal_published: Condvar,
57}
58
59pub(super) struct RuntimeObservationSealObserver {
60 pub(super) notify: Box<dyn Fn(SealedRuntimeObservationSnapshot) + Send + Sync>,
61}
62
63impl fmt::Debug for RuntimeObservationSealObserver {
64 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
65 formatter.debug_struct("RuntimeObservationSealObserver").finish_non_exhaustive()
66 }
67}
68
69impl RuntimeObservationSealObserver {
70 pub(super) fn invoke(&self, snapshot: SealedRuntimeObservationSnapshot) {
71 let _delivery = RuntimeObservationDeliveryGuard::enter(None);
72 if std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| (self.notify)(snapshot))).is_err() {
73 eprintln!("[query-engine] warning: runtime observation seal observer panicked");
74 }
75 }
76}
77
78pub(crate) struct QueryCleanupSealDelivery {
82 pub(super) owner: Option<RuntimeObservationOwner>,
83 pub(super) deferred: Option<DeferredRuntimeObservationSeal>,
84 pub(super) already_sealed: Option<SealedRuntimeObservationSnapshot>,
85 #[cfg(test)]
86 pub(super) delivery_gate: Option<Arc<RuntimeObservationDeliveryGate>>,
87}
88
89impl QueryCleanupSealDelivery {
90 pub(super) fn already_sealed(snapshot: Option<SealedRuntimeObservationSnapshot>) -> Self {
91 Self {
92 owner: None,
93 deferred: None,
94 already_sealed: snapshot,
95 #[cfg(test)]
96 delivery_gate: None,
97 }
98 }
99
100 #[cfg(test)]
102 pub(crate) fn with_delivery_gate(mut self, gate: Option<Arc<RuntimeObservationDeliveryGate>>) -> Self {
103 self.delivery_gate = gate;
104 self
105 }
106
107 pub(crate) fn deliver(self) -> Option<SealedRuntimeObservationSnapshot> {
108 let Some(deferred) = self.deferred else { return self.already_sealed };
109 if let Some(owner) = self.owner {
110 return owner.complete_observer_delivery(deferred);
111 }
112 #[cfg(test)]
115 if let Some(gate) = self.delivery_gate {
116 gate.hold();
117 }
118 deferred.invoke_observers()
119 }
120}
121
122pub(super) struct DeferredRuntimeObservationSeal {
123 pub(super) snapshot: Option<SealedRuntimeObservationSnapshot>,
124 pub(super) observers: Vec<RuntimeObservationSealObserver>,
125}
126
127impl DeferredRuntimeObservationSeal {
128 pub(super) fn invoke_observers(self) -> Option<SealedRuntimeObservationSnapshot> {
129 if let Some(snapshot) = &self.snapshot {
130 for observer in self.observers {
131 observer.invoke(snapshot.clone());
132 }
133 }
134 self.snapshot
135 }
136}
137
138impl fmt::Debug for RuntimeObservationSessionInner {
139 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
140 formatter
141 .debug_struct("RuntimeObservationSessionInner")
142 .field("manifest", &self.manifest)
143 .field("sealed", &self.sealed.load(Ordering::Acquire))
144 .finish_non_exhaustive()
145 }
146}
147
148impl RuntimeObservationSession {
149 pub fn new(query_id: impl Into<QueryId>, trace_id: Option<TraceId>, config: RuntimeObservationConfig) -> Self {
151 Self { handle: RuntimeObservationHandle::new(query_id, trace_id, config) }
152 }
153
154 pub fn with_clock(
156 query_id: impl Into<QueryId>,
157 trace_id: Option<TraceId>,
158 config: RuntimeObservationConfig,
159 clock: Arc<dyn RuntimeObservationClock>,
160 ) -> Self {
161 Self { handle: RuntimeObservationHandle::with_clock(query_id, trace_id, config, clock) }
162 }
163
164 #[cfg(test)]
165 pub(crate) fn with_source_location_test_key(
166 query_id: impl Into<QueryId>,
167 trace_id: Option<TraceId>,
168 config: RuntimeObservationConfig,
169 key: [u8; blake3::KEY_LEN],
170 ) -> Self {
171 Self {
172 handle: RuntimeObservationHandle::with_clock_and_source_location_digest_key(
173 query_id,
174 trace_id,
175 config,
176 Arc::new(SystemRuntimeObservationClock::new()),
177 &TestSourceLocationDigestKeyProvider { key: Some(key) },
178 ),
179 }
180 }
181
182 #[cfg(any(test, feature = "test-hooks"))]
183 #[doc(hidden)]
184 pub fn with_unavailable_source_location_test_key(
185 query_id: impl Into<QueryId>,
186 trace_id: Option<TraceId>,
187 config: RuntimeObservationConfig,
188 ) -> Self {
189 Self {
190 handle: RuntimeObservationHandle::with_clock_and_source_location_digest_key(
191 query_id,
192 trace_id,
193 config,
194 Arc::new(SystemRuntimeObservationClock::new()),
195 &TestSourceLocationDigestKeyProvider { key: None },
196 ),
197 }
198 }
199
200 pub const fn disabled() -> Self {
202 Self { handle: RuntimeObservationHandle::disabled() }
203 }
204
205 pub fn handle(&self) -> RuntimeObservationHandle {
207 self.handle.clone()
208 }
209
210 pub const fn config(&self) -> RuntimeObservationConfig {
212 self.handle.config()
213 }
214
215 pub fn query_id(&self) -> Option<&QueryId> {
217 self.handle.query_id()
218 }
219
220 pub fn trace_id(&self) -> Option<&TraceId> {
222 self.handle.trace_id()
223 }
224
225 pub fn record<F>(&self, build_payload: F) -> RuntimeObservationRecordDisposition
227 where F: FnOnce() -> RuntimeObservationPayload {
228 self.handle.record(build_payload)
229 }
230
231 pub fn record_for_plan<F>(
233 &self,
234 plan_execution_id: PlanExecutionId,
235 build_payload: F,
236 ) -> RuntimeObservationRecordDisposition
237 where
238 F: FnOnce() -> RuntimeObservationPayload,
239 {
240 self.handle.record_for_plan(plan_execution_id, build_payload)
241 }
242
243 pub fn record_with_context<F>(
245 &self,
246 context: RuntimeEventContext,
247 build_payload: F,
248 ) -> RuntimeObservationRecordDisposition
249 where
250 F: FnOnce() -> RuntimeObservationPayload,
251 {
252 self.handle.record_with_context(context, build_payload)
253 }
254
255 pub fn record_topology<F>(
257 &self,
258 class: RuntimeObservationRecordClass,
259 build_record: F,
260 ) -> RuntimeObservationRecordDisposition
261 where
262 F: FnOnce() -> RuntimeTopologyRecord,
263 {
264 self.handle.record_topology(class, build_record)
265 }
266
267 pub fn live_snapshot(&self) -> Option<LiveRuntimeObservationSnapshot> {
269 self.handle.live_snapshot()
270 }
271
272 pub fn seal(&self, outcome: RuntimeTerminalOutcome) -> Option<SealedRuntimeObservationSnapshot> {
275 self.handle.seal_owned(outcome)
276 }
277
278 pub(super) fn seal_deferred(
279 &self,
280 outcome: Option<RuntimeTerminalOutcome>,
281 attempt_report_provider: Option<&AttemptReportSealProvider>,
282 ) -> DeferredRuntimeObservationSeal {
283 self.handle.seal_owned_deferred(outcome, attempt_report_provider)
284 }
285
286 pub(crate) fn seal_with_report(
293 &self,
294 outcome: RuntimeTerminalOutcome,
295 report: &AttemptReportSealProvider,
296 ) -> QueryCleanupSealDelivery {
297 let deferred =
298 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| self.seal_deferred(Some(outcome), Some(report))));
299 match deferred {
300 Ok(deferred) => QueryCleanupSealDelivery {
301 owner: None,
302 deferred: Some(deferred),
303 already_sealed: None,
304 #[cfg(test)]
305 delivery_gate: None,
306 },
307 Err(panic) => {
308 self.handle.mark_degraded();
309 let _ = self.seal_deferred(Some(RuntimeTerminalOutcome::Panicked), None).invoke_observers();
310 std::panic::resume_unwind(panic);
311 },
312 }
313 }
314
315 pub(crate) fn register_seal_observer<F>(&self, observer: F)
319 where F: Fn(SealedRuntimeObservationSnapshot) + Send + Sync + 'static {
320 let observer = RuntimeObservationSealObserver { notify: Box::new(observer) };
321 if let Some((observer, snapshot)) = self.handle.register_seal_observer(observer) {
322 observer.invoke(snapshot);
323 }
324 }
325
326 pub fn sealed_snapshot(&self) -> Option<SealedRuntimeObservationSnapshot> {
328 self.handle.sealed_snapshot()
329 }
330}
331
332impl RuntimeObservationSessionInner {
333 pub(super) fn lock_state(&self) -> MutexGuard<'_, RuntimeObservationRecorderState> {
334 match self.state.lock() {
335 Ok(state) => state,
336 Err(poisoned) => {
337 let mut state = poisoned.into_inner();
338 state.degraded = true;
339 state.poison_recoveries = state.poison_recoveries.checked_add(1).unwrap_or_else(|| {
340 state.counter_overflow = true;
341 u64::MAX
342 });
343 self.state.clear_poison();
344 state
345 },
346 }
347 }
348
349 pub(super) fn record_event_lazy<F>(
350 &self,
351 context: RuntimeEventContext,
352 build_payload: F,
353 ) -> RuntimeObservationRecordDisposition
354 where
355 F: FnOnce() -> RuntimeObservationPayload,
356 {
357 self.record_event_timestamped(context, |_monotonic_ns| build_payload()).0
358 }
359
360 #[cfg(test)]
361 pub(super) fn record_event_sized<F>(
362 &self,
363 context: RuntimeEventContext,
364 logical_bytes: u64,
365 build_payload: F,
366 ) -> RuntimeObservationRecordDisposition
367 where
368 F: FnOnce() -> RuntimeObservationPayload,
369 {
370 let mut state = self.lock_state();
371 if state.sealed_snapshot.is_some() {
372 return RuntimeObservationRecordDisposition::Sealed;
373 }
374 let sequence = state.take_sequence();
375 if let Some(reason) = state.rejection_reason(self.manifest.limits, logical_bytes) {
376 state.record_drop(RuntimeObservationRecordClass::TimelineEvent, sequence, reason);
377 return RuntimeObservationRecordDisposition::Dropped { sequence, reason };
378 }
379 let monotonic_ns = self.clock.monotonic_ns().max(state.last_monotonic_ns);
380 state.last_monotonic_ns = monotonic_ns;
381 let event = RuntimeTimelineEvent {
382 sequence,
383 monotonic_ns,
384 plan_execution_id: context.plan_execution_id,
385 span_id: context.span_id,
386 parent_span_id: context.parent_span_id,
387 payload: build_payload(),
388 };
389 let actual_bytes = event.logical_retained_bytes();
390 if actual_bytes != logical_bytes {
391 state.degraded = true;
392 }
393 match state.admit(self.manifest.limits, RuntimeObservationRecordClass::TimelineEvent, sequence, actual_bytes) {
394 Ok(()) => {
395 state.events.push(event);
396 RuntimeObservationRecordDisposition::Retained { sequence, logical_bytes: actual_bytes }
397 },
398 Err(reason) => RuntimeObservationRecordDisposition::Dropped { sequence, reason },
399 }
400 }
401
402 pub(super) fn record_event_timestamped<F>(
403 &self,
404 context: RuntimeEventContext,
405 build_payload: F,
406 ) -> (RuntimeObservationRecordDisposition, Option<u64>)
407 where
408 F: FnOnce(u64) -> RuntimeObservationPayload,
409 {
410 let mut state = self.lock_state();
411 if state.sealed_snapshot.is_some() {
412 return (RuntimeObservationRecordDisposition::Sealed, None);
413 }
414 let sequence = state.take_sequence();
415 let monotonic_ns = self.clock.monotonic_ns().max(state.last_monotonic_ns);
416 state.last_monotonic_ns = monotonic_ns;
417 const MIN_TIMELINE_EVENT_BYTES: u64 = 42;
418 if let Some(reason) = state.rejection_reason(self.manifest.limits, MIN_TIMELINE_EVENT_BYTES) {
419 state.record_drop(RuntimeObservationRecordClass::TimelineEvent, sequence, reason);
420 return (RuntimeObservationRecordDisposition::Dropped { sequence, reason }, Some(monotonic_ns));
421 }
422 let event = RuntimeTimelineEvent {
423 sequence,
424 monotonic_ns,
425 plan_execution_id: context.plan_execution_id,
426 span_id: context.span_id,
427 parent_span_id: context.parent_span_id,
428 payload: build_payload(monotonic_ns),
429 };
430 let logical_bytes = event.logical_retained_bytes();
431 let disposition = match state.admit(
432 self.manifest.limits,
433 RuntimeObservationRecordClass::TimelineEvent,
434 sequence,
435 logical_bytes,
436 ) {
437 Ok(()) => {
438 state.events.push(event);
439 RuntimeObservationRecordDisposition::Retained { sequence, logical_bytes }
440 },
441 Err(reason) => RuntimeObservationRecordDisposition::Dropped { sequence, reason },
442 };
443 (disposition, Some(monotonic_ns))
444 }
445
446 pub(super) fn record_topology_lazy<F>(
447 &self,
448 class: RuntimeObservationRecordClass,
449 build_record: F,
450 ) -> RuntimeObservationRecordDisposition
451 where
452 F: FnOnce() -> RuntimeTopologyRecord,
453 {
454 let mut state = self.lock_state();
455 if state.sealed_snapshot.is_some() {
456 return RuntimeObservationRecordDisposition::Sealed;
457 }
458 let sequence = state.take_sequence();
459 const MIN_TOPOLOGY_RECORD_BYTES: u64 = 24;
460 if let Some(reason) = state.rejection_reason(self.manifest.limits, MIN_TOPOLOGY_RECORD_BYTES) {
461 state.record_drop(class, sequence, reason);
462 return RuntimeObservationRecordDisposition::Dropped { sequence, reason };
463 }
464 let record = build_record();
465 if record.class() != class {
466 state.degraded = true;
467 }
468 let logical_bytes = record.logical_retained_bytes();
469 match state.admit(self.manifest.limits, record.class(), sequence, logical_bytes) {
470 Ok(()) => {
471 state.topology.push(RetainedTopologyRecord { sequence, record });
472 RuntimeObservationRecordDisposition::Retained { sequence, logical_bytes }
473 },
474 Err(reason) => RuntimeObservationRecordDisposition::Dropped { sequence, reason },
475 }
476 }
477
478 pub(super) fn snapshot_data(
479 &self,
480 state: &RuntimeObservationRecorderState,
481 complete: bool,
482 terminal: Option<RuntimeTerminalOutcome>,
483 ) -> RuntimeObservationSnapshotData {
484 let mut topology = state.topology.clone();
485 topology.sort_by(|left, right| left.record.cmp(&right.record).then(left.sequence.cmp(&right.sequence)));
486 let mut events = state.events.clone();
487 events.sort_by_key(|event| event.sequence);
488 let dropped_by_class = state
489 .dropped_by_class
490 .iter()
491 .map(|(class, count)| RuntimeObservationClassLoss { class: *class, count: *count })
492 .collect();
493 let dropped_by_reason = state
494 .dropped_by_reason
495 .iter()
496 .map(|(reason, count)| RuntimeObservationReasonLoss { reason: *reason, count: *count })
497 .collect();
498 RuntimeObservationSnapshotData {
499 manifest: self.manifest.clone(),
500 topology,
501 events,
502 footer: RuntimeObservationFooter {
503 complete,
504 terminal,
505 retained_records: state.retained_records,
506 retained_bytes: state.retained_bytes,
507 first_dropped_sequence: state.first_dropped_sequence,
508 last_attempted_sequence: state.last_attempted_sequence,
509 dropped_by_class,
510 dropped_by_reason,
511 truncated: state.first_dropped_sequence.is_some(),
512 degraded: state.degraded,
513 poison_recoveries: state.poison_recoveries,
514 attempt_report: None,
515 counter_overflow: state.counter_overflow,
516 },
517 plans: BTreeMap::new(),
518 }
519 }
520
521 pub(super) fn wait_for_seal<'a>(
523 &self,
524 state: MutexGuard<'a, RuntimeObservationRecorderState>,
525 ) -> MutexGuard<'a, RuntimeObservationRecorderState> {
526 self.seal_published.wait(state).unwrap_or_else(PoisonError::into_inner)
527 }
528
529 pub(super) fn take_sealed_snapshot_data(
532 &self,
533 state: &mut RuntimeObservationRecorderState,
534 outcome: Option<RuntimeTerminalOutcome>,
535 ) -> RuntimeObservationSnapshotData {
536 let mut topology = std::mem::take(&mut state.topology);
537 topology.sort_by(|left, right| left.record.cmp(&right.record).then(left.sequence.cmp(&right.sequence)));
538 let mut events = std::mem::take(&mut state.events);
539 events.sort_by_key(|event| event.sequence);
540 let dropped_by_class = state
541 .dropped_by_class
542 .iter()
543 .map(|(class, count)| RuntimeObservationClassLoss { class: *class, count: *count })
544 .collect();
545 let dropped_by_reason = state
546 .dropped_by_reason
547 .iter()
548 .map(|(reason, count)| RuntimeObservationReasonLoss { reason: *reason, count: *count })
549 .collect();
550 let footer = RuntimeObservationFooter {
551 complete: true,
552 terminal: outcome,
553 retained_records: state.retained_records,
554 retained_bytes: state.retained_bytes,
555 first_dropped_sequence: state.first_dropped_sequence,
556 last_attempted_sequence: state.last_attempted_sequence,
557 dropped_by_class,
558 dropped_by_reason,
559 truncated: state.first_dropped_sequence.is_some(),
560 degraded: state.degraded,
561 poison_recoveries: state.poison_recoveries,
562 attempt_report: None,
563 counter_overflow: state.counter_overflow,
564 };
565 RuntimeObservationSnapshotData {
566 manifest: self.manifest.clone(),
567 topology,
568 events,
569 footer,
570 plans: BTreeMap::new(),
571 }
572 }
573}