use std::collections::BTreeMap;
use super::{
CleanupFailureSummary, ObservationSequence, PlanExecutionId, QueryMetrics, QueryMetricsCheckpoint,
QueryMetricsSummary, RetainedTopologyRecord, RuntimeObservationLimits, RuntimeObservationLossReason,
RuntimeObservationRecordClass, RuntimeObservationSealObserver, RuntimeTimelineEvent,
SealedRuntimeObservationSnapshot, SourceReadRuntimeMetrics,
};
use crate::observability::LakehouseCacheFillEventSnapshot;
#[derive(Debug, Clone)]
pub(crate) struct CleanupFailureTally {
count: u64,
complete: bool,
first: Option<CleanupFailureSummary>,
}
impl Default for CleanupFailureTally {
fn default() -> Self {
Self { count: 0, complete: true, first: None }
}
}
impl CleanupFailureTally {
pub(super) fn record(&mut self, summary: CleanupFailureSummary) {
if let Some(first) = &mut self.first {
first.mark_incomplete();
} else {
self.first = Some(summary);
}
match self.count.checked_add(1) {
Some(next) => self.count = next,
None => self.complete = false,
}
}
pub(crate) fn count(&self) -> u64 {
self.count
}
pub(crate) fn complete(&self) -> bool {
self.complete
}
pub(crate) fn first(&self) -> Option<&CleanupFailureSummary> {
self.first.as_ref()
}
#[cfg(test)]
pub(super) fn with_count_for_test(count: u64) -> Self {
Self { count, ..Self::default() }
}
}
#[derive(Debug, Default)]
pub(super) struct RuntimeObservationRecorderState {
pub(super) next_sequence: u64,
pub(super) last_monotonic_ns: u64,
pub(super) retained_records: u64,
pub(super) retained_bytes: u64,
pub(super) topology: Vec<RetainedTopologyRecord>,
pub(super) events: Vec<RuntimeTimelineEvent>,
pub(super) first_dropped_sequence: Option<ObservationSequence>,
pub(super) last_attempted_sequence: Option<ObservationSequence>,
pub(super) dropped_by_class: BTreeMap<RuntimeObservationRecordClass, u64>,
pub(super) dropped_by_reason: BTreeMap<RuntimeObservationLossReason, u64>,
pub(super) degraded: bool,
pub(super) poison_recoveries: u64,
pub(super) counter_overflow: bool,
pub(super) sealed_snapshot: Option<SealedRuntimeObservationSnapshot>,
pub(super) seal_observers: Vec<RuntimeObservationSealObserver>,
pub(super) cleanup_failures: CleanupFailureTally,
pub(super) metrics: BTreeMap<PlanExecutionId, PlanExecutionMetrics>,
pub(super) metrics_revision: u64,
pub(super) budget_detail_records: u64,
pub(super) budget_detail_bytes: u64,
pub(super) cache_fill_events: Vec<LakehouseCacheFillEventSnapshot>,
pub(super) cache_fill_events_overflow: u64,
}
#[derive(Debug, Default)]
pub(super) struct PlanExecutionMetrics {
pub(super) summary: QueryMetricsSummary,
pub(super) totals_saturated: bool,
pub(super) source_read_rows: Vec<SourceReadRuntimeMetrics>,
pub(super) source_read_rows_dropped: u64,
pub(super) reduction: QueryMetrics,
pub(super) detail_rows_dropped: u64,
pub(super) budget_windows: BTreeMap<u64, BudgetWindowPeaks>,
pub(super) next_budget_window: u64,
pub(super) frozen: Option<QueryMetricsCheckpoint>,
}
impl PlanExecutionMetrics {
pub(super) const fn is_frozen(&self) -> bool {
self.frozen.is_some()
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct BudgetWindowPeaks {
pub observed_peak_bytes: Option<u64>,
pub retained_bytes: Option<u64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[must_use = "an open budget window must be closed onto its task"]
pub struct BudgetWindow(pub(super) u64);
impl RuntimeObservationRecorderState {
pub(super) fn take_sequence(&mut self) -> ObservationSequence {
let value = self.next_sequence.max(1);
self.next_sequence = value.checked_add(1).unwrap_or_else(|| {
self.counter_overflow = true;
u64::MAX
});
let sequence = ObservationSequence::new(value);
self.last_attempted_sequence = Some(sequence);
sequence
}
pub(super) fn rejection_reason(
&self,
limits: RuntimeObservationLimits,
logical_bytes: u64,
) -> Option<RuntimeObservationLossReason> {
if self.retained_records >= limits.max_retained_records {
Some(RuntimeObservationLossReason::RecordLimit)
} else if logical_bytes > limits.max_retained_bytes {
Some(RuntimeObservationLossReason::Oversized)
} else if self.retained_bytes.checked_add(logical_bytes).is_none_or(|sum| sum > limits.max_retained_bytes) {
Some(RuntimeObservationLossReason::ByteLimit)
} else {
None
}
}
pub(super) fn record_drop(
&mut self,
class: RuntimeObservationRecordClass,
sequence: ObservationSequence,
reason: RuntimeObservationLossReason,
) {
self.first_dropped_sequence.get_or_insert(sequence);
let class_count = self.dropped_by_class.get(&class).copied().unwrap_or(0);
let next_class_count = class_count.checked_add(1).unwrap_or_else(|| {
self.counter_overflow = true;
u64::MAX
});
self.dropped_by_class.insert(class, next_class_count);
let reason_count = self.dropped_by_reason.get(&reason).copied().unwrap_or(0);
let next_reason_count = reason_count.checked_add(1).unwrap_or_else(|| {
self.counter_overflow = true;
u64::MAX
});
self.dropped_by_reason.insert(reason, next_reason_count);
}
pub(super) fn admit_detail(
&mut self,
limits: RuntimeObservationLimits,
class: RuntimeObservationRecordClass,
sequence: ObservationSequence,
logical_bytes: u64,
) -> Result<(), RuntimeObservationLossReason> {
if class != RuntimeObservationRecordClass::BudgetDetail {
return self.admit(limits, class, sequence, logical_bytes);
}
let share = RuntimeObservationLimits {
max_retained_records: limits.max_retained_records / 2,
max_retained_bytes: limits.max_retained_bytes / 2,
};
let share_rejection = if self.budget_detail_records >= share.max_retained_records {
Some(RuntimeObservationLossReason::RecordLimit)
} else if self.budget_detail_bytes.saturating_add(logical_bytes) > share.max_retained_bytes {
Some(RuntimeObservationLossReason::ByteLimit)
} else {
None
};
if let Some(reason) = share_rejection {
self.record_drop(class, sequence, reason);
return Err(reason);
}
self.admit(limits, class, sequence, logical_bytes)?;
self.budget_detail_records = self.budget_detail_records.saturating_add(1);
self.budget_detail_bytes = self.budget_detail_bytes.saturating_add(logical_bytes);
Ok(())
}
pub(super) fn admit(
&mut self,
limits: RuntimeObservationLimits,
class: RuntimeObservationRecordClass,
sequence: ObservationSequence,
logical_bytes: u64,
) -> Result<(), RuntimeObservationLossReason> {
if let Some(reason) = self.rejection_reason(limits, logical_bytes) {
self.record_drop(class, sequence, reason);
return Err(reason);
}
self.retained_records = self.retained_records.checked_add(1).unwrap_or_else(|| {
self.counter_overflow = true;
u64::MAX
});
self.retained_bytes = self.retained_bytes.checked_add(logical_bytes).unwrap_or_else(|| {
self.counter_overflow = true;
u64::MAX
});
Ok(())
}
}