use super::{
MetricsRecorder, PlanExecutionBoundaryKind, PlanExecutionId, QueryMetricsCheckpoint, RuntimeEventContext,
RuntimeObservationHandle, RuntimeObservationOwner, RuntimeObservationPayload, RuntimeObservationSession,
RuntimeSpanId, RuntimeTaskId, RuntimeTaskLifecycleObservation, RuntimeTerminalOutcome,
SealedRuntimeObservationSnapshot, SourceReadFact, TaskAttemptId, TaskLifecycleState,
};
use crate::{attempt::AttemptHandle, metrics::SourceReadRuntimeMetrics};
#[derive(Debug)]
pub struct RuntimeQueryLifecycle {
owner: Option<RuntimeObservationOwner>,
plan_execution_id: PlanExecutionId,
plan_span_id: RuntimeSpanId,
}
#[derive(Debug)]
pub struct RuntimePlanExecutionLifecycle {
observation: RuntimeObservationHandle,
owner: Option<RuntimeObservationOwner>,
plan_execution_id: PlanExecutionId,
plan_span_id: RuntimeSpanId,
finished: bool,
}
#[derive(Debug, Clone)]
pub struct RuntimeExecutionObservationScope {
observation: RuntimeObservationHandle,
plan_execution_id: PlanExecutionId,
plan_span_id: RuntimeSpanId,
metrics: MetricsRecorder,
}
impl RuntimeExecutionObservationScope {
pub const fn observation(&self) -> &RuntimeObservationHandle {
&self.observation
}
pub const fn plan_execution_id(&self) -> &PlanExecutionId {
&self.plan_execution_id
}
pub const fn plan_span_id(&self) -> RuntimeSpanId {
self.plan_span_id
}
pub fn for_metrics(observation: RuntimeObservationHandle, metrics: MetricsRecorder) -> Self {
Self {
observation,
plan_execution_id: metrics.plan_execution_id().clone(),
plan_span_id: RuntimeSpanId::new(1),
metrics,
}
}
pub const fn metrics(&self) -> &MetricsRecorder {
&self.metrics
}
#[must_use]
pub fn with_metrics(mut self, metrics: MetricsRecorder) -> Self {
self.metrics = metrics;
self
}
pub fn record_source_read(&self, fact: SourceReadFact) {
self.metrics.record_source_read(fact);
}
pub fn record_source_read_row(&self, row: SourceReadRuntimeMetrics) {
self.metrics.record_source_read_runtime(row);
}
pub fn metrics_checkpoint(&self) -> Option<QueryMetricsCheckpoint> {
self.metrics.checkpoint()
}
fn new(
observation: RuntimeObservationHandle,
plan_execution_id: PlanExecutionId,
plan_span_id: RuntimeSpanId,
) -> Self {
let metrics = MetricsRecorder::new(observation.clone(), plan_execution_id.clone());
Self { observation, plan_execution_id, plan_span_id, metrics }
}
}
impl RuntimeQueryLifecycle {
pub fn new(session: RuntimeObservationSession, plan_execution_id: PlanExecutionId) -> Self {
Self::from_owner(RuntimeObservationOwner::from_session(session), plan_execution_id)
}
pub fn from_owner(owner: RuntimeObservationOwner, plan_execution_id: PlanExecutionId) -> Self {
let lifecycle = Self { owner: Some(owner), plan_execution_id, plan_span_id: RuntimeSpanId::new(1) };
lifecycle.record_plan(PlanExecutionBoundaryKind::Started);
lifecycle
}
pub fn plan_execution_id(&self) -> &PlanExecutionId {
&self.plan_execution_id
}
pub const fn plan_span_id(&self) -> RuntimeSpanId {
self.plan_span_id
}
pub fn handle(&self) -> RuntimeObservationHandle {
self.owner.as_ref().map_or_else(RuntimeObservationHandle::disabled, RuntimeObservationOwner::handle)
}
pub fn execution_scope(&self) -> RuntimeExecutionObservationScope {
RuntimeExecutionObservationScope::new(self.handle(), self.plan_execution_id.clone(), self.plan_span_id)
}
pub fn finish(mut self, outcome: RuntimeTerminalOutcome) -> Option<SealedRuntimeObservationSnapshot> {
self.terminate(outcome)
}
fn terminate(&mut self, outcome: RuntimeTerminalOutcome) -> Option<SealedRuntimeObservationSnapshot> {
let plan_boundary = match &outcome {
RuntimeTerminalOutcome::Succeeded => PlanExecutionBoundaryKind::Finished,
RuntimeTerminalOutcome::Cancelled => PlanExecutionBoundaryKind::Cancelled,
RuntimeTerminalOutcome::Failed { .. }
| RuntimeTerminalOutcome::Dropped
| RuntimeTerminalOutcome::Panicked => PlanExecutionBoundaryKind::Failed,
};
self.record_plan(plan_boundary);
self.owner.take().and_then(|owner| owner.finish_query(Some(outcome)))
}
fn record_plan(&self, boundary: PlanExecutionBoundaryKind) {
if let Some(owner) = &self.owner {
owner.handle().record_with_context(
RuntimeEventContext::for_plan(self.plan_execution_id.clone()).with_span(self.plan_span_id, None),
|| RuntimeObservationPayload::PlanExecutionBoundary { boundary },
);
}
}
}
impl RuntimePlanExecutionLifecycle {
pub fn from_owner(owner: RuntimeObservationOwner, plan_execution_id: PlanExecutionId) -> Self {
Self::open(owner.handle(), Some(owner), plan_execution_id)
}
pub(crate) fn from_attempt(attempt: &AttemptHandle, plan_execution_id: PlanExecutionId) -> Self {
Self::open(attempt.observation(), None, plan_execution_id)
}
fn open(
observation: RuntimeObservationHandle,
owner: Option<RuntimeObservationOwner>,
plan_execution_id: PlanExecutionId,
) -> Self {
let lifecycle =
Self { observation, owner, plan_execution_id, plan_span_id: RuntimeSpanId::new(1), finished: false };
lifecycle.record_plan(PlanExecutionBoundaryKind::Started);
lifecycle
}
pub fn from_handle(observation: RuntimeObservationHandle, plan_execution_id: PlanExecutionId) -> Self {
Self::open(observation, None, plan_execution_id)
}
pub fn handle(&self) -> RuntimeObservationHandle {
self.observation.clone()
}
pub fn execution_scope(&self) -> RuntimeExecutionObservationScope {
RuntimeExecutionObservationScope::new(self.handle(), self.plan_execution_id.clone(), self.plan_span_id)
}
pub fn finish(mut self, outcome: RuntimeTerminalOutcome) {
self.terminate(outcome);
}
fn terminate(&mut self, outcome: RuntimeTerminalOutcome) {
let boundary = match &outcome {
RuntimeTerminalOutcome::Succeeded => PlanExecutionBoundaryKind::Finished,
RuntimeTerminalOutcome::Cancelled => PlanExecutionBoundaryKind::Cancelled,
RuntimeTerminalOutcome::Failed { .. }
| RuntimeTerminalOutcome::Dropped
| RuntimeTerminalOutcome::Panicked => PlanExecutionBoundaryKind::Failed,
};
self.record_plan(boundary);
self.finished = true;
if let Some(owner) = self.owner.take() {
owner.record_terminal_outcome(outcome);
}
}
fn record_plan(&self, boundary: PlanExecutionBoundaryKind) {
self.observation.record_with_context(
RuntimeEventContext::for_plan(self.plan_execution_id.clone()).with_span(self.plan_span_id, None),
|| RuntimeObservationPayload::PlanExecutionBoundary { boundary },
);
}
}
impl Drop for RuntimePlanExecutionLifecycle {
fn drop(&mut self) {
if self.finished {
return;
}
let outcome =
if std::thread::panicking() { RuntimeTerminalOutcome::Panicked } else { RuntimeTerminalOutcome::Dropped };
self.terminate(outcome);
}
}
impl Drop for RuntimeQueryLifecycle {
fn drop(&mut self) {
if self.owner.is_none() {
return;
}
let outcome =
if std::thread::panicking() { RuntimeTerminalOutcome::Panicked } else { RuntimeTerminalOutcome::Dropped };
let _ = self.terminate(outcome);
}
}
#[derive(Debug)]
pub struct RuntimeTaskLifecycle {
observation: RuntimeObservationHandle,
context: RuntimeEventContext,
attempt_id: TaskAttemptId,
task_kind: String,
terminal_recorded: bool,
}
impl RuntimeTaskLifecycle {
pub fn new(
observation: RuntimeObservationHandle,
plan_execution_id: PlanExecutionId,
attempt_id: TaskAttemptId,
task_kind: impl Into<String>,
span_id: RuntimeSpanId,
parent_span_id: RuntimeSpanId,
) -> Self {
let lifecycle = Self {
observation,
context: RuntimeEventContext::for_plan(plan_execution_id).with_span(span_id, Some(parent_span_id)),
attempt_id,
task_kind: task_kind.into(),
terminal_recorded: false,
};
lifecycle.record(TaskLifecycleState::Ready);
lifecycle.record(TaskLifecycleState::Started);
lifecycle
}
pub fn finish(mut self) {
self.record_terminal(TaskLifecycleState::Finished);
}
pub fn fail(mut self, error_code: impl Into<String>, cancelled: bool) {
let lifecycle = if cancelled {
TaskLifecycleState::Cancelled
} else {
TaskLifecycleState::Failed { error_code: error_code.into() }
};
self.record_terminal(lifecycle);
}
pub fn panicked(mut self) {
self.record_terminal(TaskLifecycleState::Panicked);
}
fn record_terminal(&mut self, lifecycle: TaskLifecycleState) {
if self.terminal_recorded {
return;
}
self.record(lifecycle);
self.terminal_recorded = true;
}
fn record(&self, lifecycle: TaskLifecycleState) {
self.observation.record_with_context(self.context.clone(), || {
RuntimeObservationPayload::TaskLifecycle(Box::new(RuntimeTaskLifecycleObservation {
attempt_id: self.attempt_id.clone(),
task_kind: self.task_kind.clone(),
lifecycle,
}))
});
}
}
impl Drop for RuntimeTaskLifecycle {
fn drop(&mut self) {
if self.terminal_recorded {
return;
}
let lifecycle =
if std::thread::panicking() { TaskLifecycleState::Panicked } else { TaskLifecycleState::Dropped };
self.record_terminal(lifecycle);
}
}
pub fn task_attempt_id(task_id: impl Into<RuntimeTaskId>, attempt: u32) -> TaskAttemptId {
TaskAttemptId::new(task_id.into(), super::RuntimeAttemptId::new(attempt))
}
#[cfg(test)]
mod tests {
use query_planning::correlation::QueryId;
use super::*;
use crate::observability::RuntimeObservationConfig;
#[test]
fn coordinator_handle_plan_lifecycle_records_without_sealing_query_root() {
let owner = RuntimeObservationOwner::new(
QueryId::new("coordinator-owned"),
None,
RuntimeObservationConfig::bounded(16, 4096),
);
let lifecycle =
RuntimePlanExecutionLifecycle::from_handle(owner.handle(), PlanExecutionId::new("coordinator-plan"));
lifecycle.finish(RuntimeTerminalOutcome::Succeeded);
assert!(owner.sealed_snapshot().is_none(), "plan completion must not seal a coordinator-owned query root");
let snapshot = owner.finish_query(Some(RuntimeTerminalOutcome::Succeeded)).unwrap();
assert!(snapshot.events().iter().any(|event| matches!(
event.payload,
RuntimeObservationPayload::PlanExecutionBoundary { boundary: PlanExecutionBoundaryKind::Finished }
)));
}
}