query_runtime/observability/recorder/
state.rs1use std::collections::BTreeMap;
4
5use super::{
6 CleanupFailureSummary, ObservationSequence, PlanExecutionId, QueryMetrics, QueryMetricsCheckpoint,
7 QueryMetricsSummary, RetainedTopologyRecord, RuntimeObservationLimits, RuntimeObservationLossReason,
8 RuntimeObservationRecordClass, RuntimeObservationSealObserver, RuntimeTimelineEvent,
9 SealedRuntimeObservationSnapshot, SourceReadRuntimeMetrics,
10};
11use crate::observability::LakehouseCacheFillEventSnapshot;
12
13#[derive(Debug, Clone)]
17pub(crate) struct CleanupFailureTally {
18 count: u64,
19 complete: bool,
20 first: Option<CleanupFailureSummary>,
21}
22
23impl Default for CleanupFailureTally {
24 fn default() -> Self {
25 Self { count: 0, complete: true, first: None }
26 }
27}
28
29impl CleanupFailureTally {
30 pub(super) fn record(&mut self, summary: CleanupFailureSummary) {
34 if let Some(first) = &mut self.first {
35 first.mark_incomplete();
39 } else {
40 self.first = Some(summary);
41 }
42 match self.count.checked_add(1) {
43 Some(next) => self.count = next,
44 None => self.complete = false,
45 }
46 }
47
48 pub(crate) fn count(&self) -> u64 {
49 self.count
50 }
51
52 pub(crate) fn complete(&self) -> bool {
53 self.complete
54 }
55
56 pub(crate) fn first(&self) -> Option<&CleanupFailureSummary> {
57 self.first.as_ref()
58 }
59
60 #[cfg(test)]
61 pub(super) fn with_count_for_test(count: u64) -> Self {
62 Self { count, ..Self::default() }
63 }
64}
65
66#[derive(Debug, Default)]
67pub(super) struct RuntimeObservationRecorderState {
68 pub(super) next_sequence: u64,
69 pub(super) last_monotonic_ns: u64,
70 pub(super) retained_records: u64,
71 pub(super) retained_bytes: u64,
72 pub(super) topology: Vec<RetainedTopologyRecord>,
73 pub(super) events: Vec<RuntimeTimelineEvent>,
74 pub(super) first_dropped_sequence: Option<ObservationSequence>,
75 pub(super) last_attempted_sequence: Option<ObservationSequence>,
76 pub(super) dropped_by_class: BTreeMap<RuntimeObservationRecordClass, u64>,
77 pub(super) dropped_by_reason: BTreeMap<RuntimeObservationLossReason, u64>,
78 pub(super) degraded: bool,
79 pub(super) poison_recoveries: u64,
80 pub(super) counter_overflow: bool,
81 pub(super) sealed_snapshot: Option<SealedRuntimeObservationSnapshot>,
82 pub(super) seal_observers: Vec<RuntimeObservationSealObserver>,
83 pub(super) cleanup_failures: CleanupFailureTally,
84 pub(super) metrics: BTreeMap<PlanExecutionId, PlanExecutionMetrics>,
90 pub(super) metrics_revision: u64,
91 pub(super) budget_detail_records: u64,
93 pub(super) budget_detail_bytes: u64,
94 pub(super) cache_fill_events: Vec<LakehouseCacheFillEventSnapshot>,
100 pub(super) cache_fill_events_overflow: u64,
101}
102
103#[derive(Debug, Default)]
107pub(super) struct PlanExecutionMetrics {
108 pub(super) summary: QueryMetricsSummary,
109 pub(super) totals_saturated: bool,
110 pub(super) source_read_rows: Vec<SourceReadRuntimeMetrics>,
111 pub(super) source_read_rows_dropped: u64,
112 pub(super) reduction: QueryMetrics,
115 pub(super) detail_rows_dropped: u64,
116 pub(super) budget_windows: BTreeMap<u64, BudgetWindowPeaks>,
119 pub(super) next_budget_window: u64,
120 pub(super) frozen: Option<QueryMetricsCheckpoint>,
123}
124
125impl PlanExecutionMetrics {
126 pub(super) const fn is_frozen(&self) -> bool {
127 self.frozen.is_some()
128 }
129}
130
131#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
134pub struct BudgetWindowPeaks {
135 pub observed_peak_bytes: Option<u64>,
137 pub retained_bytes: Option<u64>,
139}
140
141#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147#[must_use = "an open budget window must be closed onto its task"]
148pub struct BudgetWindow(pub(super) u64);
149
150impl RuntimeObservationRecorderState {
151 pub(super) fn take_sequence(&mut self) -> ObservationSequence {
152 let value = self.next_sequence.max(1);
153 self.next_sequence = value.checked_add(1).unwrap_or_else(|| {
154 self.counter_overflow = true;
155 u64::MAX
156 });
157 let sequence = ObservationSequence::new(value);
158 self.last_attempted_sequence = Some(sequence);
159 sequence
160 }
161
162 pub(super) fn rejection_reason(
163 &self,
164 limits: RuntimeObservationLimits,
165 logical_bytes: u64,
166 ) -> Option<RuntimeObservationLossReason> {
167 if self.retained_records >= limits.max_retained_records {
168 Some(RuntimeObservationLossReason::RecordLimit)
169 } else if logical_bytes > limits.max_retained_bytes {
170 Some(RuntimeObservationLossReason::Oversized)
171 } else if self.retained_bytes.checked_add(logical_bytes).is_none_or(|sum| sum > limits.max_retained_bytes) {
172 Some(RuntimeObservationLossReason::ByteLimit)
173 } else {
174 None
175 }
176 }
177
178 pub(super) fn record_drop(
179 &mut self,
180 class: RuntimeObservationRecordClass,
181 sequence: ObservationSequence,
182 reason: RuntimeObservationLossReason,
183 ) {
184 self.first_dropped_sequence.get_or_insert(sequence);
185 let class_count = self.dropped_by_class.get(&class).copied().unwrap_or(0);
186 let next_class_count = class_count.checked_add(1).unwrap_or_else(|| {
187 self.counter_overflow = true;
188 u64::MAX
189 });
190 self.dropped_by_class.insert(class, next_class_count);
191 let reason_count = self.dropped_by_reason.get(&reason).copied().unwrap_or(0);
192 let next_reason_count = reason_count.checked_add(1).unwrap_or_else(|| {
193 self.counter_overflow = true;
194 u64::MAX
195 });
196 self.dropped_by_reason.insert(reason, next_reason_count);
197 }
198
199 pub(super) fn admit_detail(
206 &mut self,
207 limits: RuntimeObservationLimits,
208 class: RuntimeObservationRecordClass,
209 sequence: ObservationSequence,
210 logical_bytes: u64,
211 ) -> Result<(), RuntimeObservationLossReason> {
212 if class != RuntimeObservationRecordClass::BudgetDetail {
213 return self.admit(limits, class, sequence, logical_bytes);
214 }
215 let share = RuntimeObservationLimits {
216 max_retained_records: limits.max_retained_records / 2,
217 max_retained_bytes: limits.max_retained_bytes / 2,
218 };
219 let share_rejection = if self.budget_detail_records >= share.max_retained_records {
220 Some(RuntimeObservationLossReason::RecordLimit)
221 } else if self.budget_detail_bytes.saturating_add(logical_bytes) > share.max_retained_bytes {
222 Some(RuntimeObservationLossReason::ByteLimit)
223 } else {
224 None
225 };
226 if let Some(reason) = share_rejection {
227 self.record_drop(class, sequence, reason);
228 return Err(reason);
229 }
230 self.admit(limits, class, sequence, logical_bytes)?;
231 self.budget_detail_records = self.budget_detail_records.saturating_add(1);
232 self.budget_detail_bytes = self.budget_detail_bytes.saturating_add(logical_bytes);
233 Ok(())
234 }
235
236 pub(super) fn admit(
237 &mut self,
238 limits: RuntimeObservationLimits,
239 class: RuntimeObservationRecordClass,
240 sequence: ObservationSequence,
241 logical_bytes: u64,
242 ) -> Result<(), RuntimeObservationLossReason> {
243 if let Some(reason) = self.rejection_reason(limits, logical_bytes) {
244 self.record_drop(class, sequence, reason);
245 return Err(reason);
246 }
247 self.retained_records = self.retained_records.checked_add(1).unwrap_or_else(|| {
248 self.counter_overflow = true;
249 u64::MAX
250 });
251 self.retained_bytes = self.retained_bytes.checked_add(logical_bytes).unwrap_or_else(|| {
252 self.counter_overflow = true;
253 u64::MAX
254 });
255 Ok(())
256 }
257}