Skip to main content

icydb_core/db/executor/
trace.rs

1//! Executor query tracing boundary.
2//!
3//! Tracing is optional, injected by the caller, and must not affect execution semantics.
4
5use crate::{
6    db::query::plan::{AccessPath, AccessPlan, ExecutablePlan, PlanFingerprint},
7    error::{ErrorClass, ErrorOrigin, InternalError},
8    traits::EntityKind,
9};
10use sha2::{Digest, Sha256};
11
12///
13/// QueryTraceSink
14///
15
16pub trait QueryTraceSink: Send + Sync {
17    fn on_event(&self, event: QueryTraceEvent);
18}
19
20///
21/// TraceExecutorKind
22///
23
24#[derive(Clone, Copy, Debug, Eq, PartialEq)]
25pub enum TraceExecutorKind {
26    Load,
27    Save,
28    Delete,
29}
30
31///
32/// TraceAccess
33///
34
35#[derive(Clone, Copy, Debug, Eq, PartialEq)]
36pub enum TraceAccess {
37    ByKey,
38    ByKeys { count: u32 },
39    KeyRange,
40    IndexPrefix { name: &'static str, prefix_len: u32 },
41    FullScan,
42    Union { branches: u32 },
43    Intersection { branches: u32 },
44}
45
46///
47/// TracePhase
48///
49
50#[derive(Clone, Copy, Debug, Eq, PartialEq)]
51pub enum TracePhase {
52    Access,
53}
54
55///
56/// QueryTraceEvent
57///
58
59#[derive(Clone, Copy, Debug, Eq, PartialEq)]
60pub enum QueryTraceEvent {
61    Start {
62        fingerprint: PlanFingerprint,
63        executor: TraceExecutorKind,
64        access: Option<TraceAccess>,
65    },
66    Phase {
67        fingerprint: PlanFingerprint,
68        executor: TraceExecutorKind,
69        access: Option<TraceAccess>,
70        phase: TracePhase,
71        rows: u64,
72    },
73    Finish {
74        fingerprint: PlanFingerprint,
75        executor: TraceExecutorKind,
76        access: Option<TraceAccess>,
77        rows: u64,
78    },
79    Error {
80        fingerprint: PlanFingerprint,
81        executor: TraceExecutorKind,
82        access: Option<TraceAccess>,
83        class: ErrorClass,
84        origin: ErrorOrigin,
85    },
86}
87
88///
89/// TraceScope
90///
91
92pub struct TraceScope {
93    sink: &'static dyn QueryTraceSink,
94    fingerprint: PlanFingerprint,
95    executor: TraceExecutorKind,
96    access: Option<TraceAccess>,
97}
98
99impl TraceScope {
100    fn new(
101        sink: &'static dyn QueryTraceSink,
102        fingerprint: PlanFingerprint,
103        executor: TraceExecutorKind,
104        access: Option<TraceAccess>,
105    ) -> Self {
106        sink.on_event(QueryTraceEvent::Start {
107            fingerprint,
108            executor,
109            access,
110        });
111        Self {
112            sink,
113            fingerprint,
114            executor,
115            access,
116        }
117    }
118
119    pub(crate) fn finish(self, rows: u64) {
120        self.sink.on_event(QueryTraceEvent::Finish {
121            fingerprint: self.fingerprint,
122            executor: self.executor,
123            access: self.access,
124            rows,
125        });
126    }
127
128    pub(crate) fn phase(&self, phase: TracePhase, rows: u64) {
129        self.sink.on_event(QueryTraceEvent::Phase {
130            fingerprint: self.fingerprint,
131            executor: self.executor,
132            access: self.access,
133            phase,
134            rows,
135        });
136    }
137
138    pub(crate) fn error(self, err: &InternalError) {
139        self.sink.on_event(QueryTraceEvent::Error {
140            fingerprint: self.fingerprint,
141            executor: self.executor,
142            access: self.access,
143            class: err.class,
144            origin: err.origin,
145        });
146    }
147}
148
149pub fn start_plan_trace<E: EntityKind>(
150    sink: Option<&'static dyn QueryTraceSink>,
151    executor: TraceExecutorKind,
152    plan: &ExecutablePlan<E>,
153) -> Option<TraceScope> {
154    let sink = sink?;
155    let access = Some(trace_access_from_plan(plan.access()));
156    let fingerprint = plan.fingerprint();
157    Some(TraceScope::new(sink, fingerprint, executor, access))
158}
159
160pub fn start_exec_trace(
161    sink: Option<&'static dyn QueryTraceSink>,
162    executor: TraceExecutorKind,
163    entity_path: &'static str,
164    access: Option<TraceAccess>,
165    detail: Option<&'static str>,
166) -> Option<TraceScope> {
167    let sink = sink?;
168    let fingerprint = exec_fingerprint(executor, entity_path, detail);
169    Some(TraceScope::new(sink, fingerprint, executor, access))
170}
171
172fn trace_access_from_plan<K>(plan: &AccessPlan<K>) -> TraceAccess {
173    match plan {
174        AccessPlan::Path(path) => trace_access_from_path(path),
175        AccessPlan::Union(children) => {
176            // NOTE: Diagnostics are best-effort; overflow saturates to preserve determinism.
177            TraceAccess::Union {
178                branches: u32::try_from(children.len()).unwrap_or(u32::MAX),
179            }
180        }
181        AccessPlan::Intersection(children) => {
182            // NOTE: Diagnostics are best-effort; overflow saturates to preserve determinism.
183            TraceAccess::Intersection {
184                branches: u32::try_from(children.len()).unwrap_or(u32::MAX),
185            }
186        }
187    }
188}
189
190fn trace_access_from_path<K>(path: &AccessPath<K>) -> TraceAccess {
191    match path {
192        AccessPath::ByKey(_) => TraceAccess::ByKey,
193        AccessPath::ByKeys(keys) => {
194            // NOTE: Diagnostics are best-effort; overflow saturates to preserve determinism.
195            TraceAccess::ByKeys {
196                count: u32::try_from(keys.len()).unwrap_or(u32::MAX),
197            }
198        }
199        AccessPath::KeyRange { .. } => TraceAccess::KeyRange,
200        AccessPath::IndexPrefix { index, values } => {
201            // NOTE: Diagnostics are best-effort; overflow saturates to preserve determinism.
202            TraceAccess::IndexPrefix {
203                name: index.name,
204                prefix_len: u32::try_from(values.len()).unwrap_or(u32::MAX),
205            }
206        }
207        AccessPath::FullScan => TraceAccess::FullScan,
208    }
209}
210
211fn exec_fingerprint(
212    executor: TraceExecutorKind,
213    entity_path: &'static str,
214    detail: Option<&'static str>,
215) -> PlanFingerprint {
216    let mut hasher = Sha256::new();
217    hasher.update(b"execfp:v1");
218    hasher.update([executor_tag(executor)]);
219    write_str(&mut hasher, entity_path);
220    match detail {
221        Some(detail) => {
222            hasher.update([1u8]);
223            write_str(&mut hasher, detail);
224        }
225        None => {
226            hasher.update([0u8]);
227        }
228    }
229    let digest = hasher.finalize();
230    let mut out = [0u8; 32];
231    out.copy_from_slice(&digest);
232    PlanFingerprint::from_bytes(out)
233}
234
235const fn executor_tag(executor: TraceExecutorKind) -> u8 {
236    match executor {
237        TraceExecutorKind::Load => 0x01,
238        TraceExecutorKind::Save => 0x02,
239        TraceExecutorKind::Delete => 0x03,
240    }
241}
242
243fn write_str(hasher: &mut Sha256, value: &str) {
244    // NOTE: Diagnostics-only fingerprinting saturates on overflow to avoid panics.
245    let len = u32::try_from(value.len()).unwrap_or(u32::MAX);
246    hasher.update(len.to_be_bytes());
247    hasher.update(value.as_bytes());
248}