use std::time::Instant;
use serde::Serialize;
use tracing::Span;
use crate::broker::RequestContext;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum Phase {
SecurityExtract,
CatalogResolve,
Authorize,
BuildLogical,
CompilePlan,
AcquireLease,
Execute,
EmitAudit,
}
impl Phase {
pub fn as_str(self) -> &'static str {
match self {
Self::SecurityExtract => "security_extract",
Self::CatalogResolve => "catalog_resolve",
Self::Authorize => "authorize",
Self::BuildLogical => "build_logical",
Self::CompilePlan => "compile_plan",
Self::AcquireLease => "acquire_lease",
Self::Execute => "execute",
Self::EmitAudit => "emit_audit",
}
}
pub fn all() -> &'static [Phase] {
&[
Phase::SecurityExtract,
Phase::CatalogResolve,
Phase::Authorize,
Phase::BuildLogical,
Phase::CompilePlan,
Phase::AcquireLease,
Phase::Execute,
Phase::EmitAudit,
]
}
}
pub struct RequestPipeline<'a> {
rpc: &'static str,
context: &'a RequestContext,
started: Instant,
}
impl<'a> RequestPipeline<'a> {
pub fn new(rpc: &'static str, context: &'a RequestContext) -> Self {
Self {
rpc,
context,
started: Instant::now(),
}
}
pub fn enter(&self, phase: Phase) -> Span {
tracing::info_span!(
target: "udb.request",
"udb.request",
rpc = self.rpc,
phase = phase.as_str(),
tenant_id = %self.context.tenant_id,
project_id = %self.context.project_id,
correlation_id = %self.context.correlation_id,
target_backend = %self.context.target_backend,
target_instance = %self.context.target_instance,
)
}
pub fn elapsed_ms(&self) -> u64 {
self.started.elapsed().as_millis() as u64
}
pub fn is_dry_run(_context: &RequestContext) -> bool {
false
}
}
#[cfg(test)]
mod tests {
use super::*;
fn fake_context() -> RequestContext {
RequestContext {
tenant_id: "acme".into(),
project_id: "billing".into(),
correlation_id: "corr-1".into(),
purpose: "test".into(),
..Default::default()
}
}
#[test]
fn phase_tokens_are_pinned() {
assert_eq!(Phase::SecurityExtract.as_str(), "security_extract");
assert_eq!(Phase::CatalogResolve.as_str(), "catalog_resolve");
assert_eq!(Phase::Authorize.as_str(), "authorize");
assert_eq!(Phase::BuildLogical.as_str(), "build_logical");
assert_eq!(Phase::CompilePlan.as_str(), "compile_plan");
assert_eq!(Phase::AcquireLease.as_str(), "acquire_lease");
assert_eq!(Phase::Execute.as_str(), "execute");
assert_eq!(Phase::EmitAudit.as_str(), "emit_audit");
}
#[test]
fn all_phases_returned_in_order() {
let names: Vec<&str> = Phase::all().iter().map(|p| p.as_str()).collect();
assert_eq!(
names,
vec![
"security_extract",
"catalog_resolve",
"authorize",
"build_logical",
"compile_plan",
"acquire_lease",
"execute",
"emit_audit",
]
);
}
#[test]
fn pipeline_enter_does_not_panic_and_records_elapsed() {
let ctx = fake_context();
let pipeline = RequestPipeline::new("Select", &ctx);
let _guard = pipeline.enter(Phase::Execute).entered();
let elapsed = pipeline.elapsed_ms();
assert!(
elapsed < 1_000,
"test pipeline elapsed unreasonably: {elapsed}"
);
}
}