Skip to main content

query_flow/
tracer.rs

1//! Tracer trait for observing query-flow execution.
2//!
3//! This module defines the [`Tracer`] trait and related types for observing
4//! query execution. The default [`NoopTracer`] provides zero-cost when tracing
5//! is not needed.
6//!
7//! # Example
8//!
9//! ```
10//! use query_flow::{
11//!     query, Db, QueryCacheKey, QueryError, QueryRuntime, SpanContext, SpanId, TraceId, Tracer,
12//! };
13//!
14//! // Custom tracer implementation
15//! struct MyTracer;
16//!
17//! impl Tracer for MyTracer {
18//!     fn new_span_id(&self) -> SpanId {
19//!         SpanId(1)
20//!     }
21//!
22//!     fn new_trace_id(&self) -> TraceId {
23//!         TraceId(1)
24//!     }
25//!
26//!     fn on_query_start(&self, ctx: &SpanContext, query: &QueryCacheKey) {
27//!         println!("Query started: {:?} (trace={:?})", query, ctx.trace_id);
28//!     }
29//! }
30//!
31//! #[query]
32//! fn double(db: &impl Db, x: i32) -> Result<i32, QueryError> {
33//!     let _ = db;
34//!     Ok(x * 2)
35//! }
36//!
37//! let runtime = QueryRuntime::with_tracer(MyTracer);
38//! assert_eq!(*runtime.query(Double::new(21)).unwrap(), 42);
39//! ```
40
41use serde::{Deserialize, Serialize};
42
43use crate::key::{AssetCacheKey, FullCacheKey, QueryCacheKey};
44
45/// Unique identifier for a query execution span.
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
47pub struct SpanId(pub u64);
48
49impl SpanId {
50    /// Zero value, used by NoopTracer.
51    pub const ZERO: Self = Self(0);
52}
53
54/// Unique identifier for a trace (a complete query execution tree).
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
56pub struct TraceId(pub u64);
57
58impl TraceId {
59    /// Zero value, used by NoopTracer.
60    pub const ZERO: Self = Self(0);
61}
62
63/// Context for a span within a trace, providing parent-child relationships.
64///
65/// This enables reconstructing the full dependency tree of query executions.
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
67pub struct SpanContext {
68    /// Unique identifier for this span.
69    pub span_id: SpanId,
70    /// Identifier for the trace this span belongs to.
71    pub trace_id: TraceId,
72    /// The parent span's ID, if this is a nested query.
73    pub parent_span_id: Option<SpanId>,
74}
75
76/// Query execution result classification.
77#[derive(Debug, Clone, PartialEq, Eq)]
78pub enum ExecutionResult {
79    /// Query computed a new value (output changed).
80    Changed,
81    /// Query completed but output unchanged (early cutoff applied).
82    Unchanged,
83    /// Query returned cached value without execution.
84    CacheHit,
85    /// Query suspended waiting for async loading.
86    Suspended,
87    /// Query detected a dependency cycle.
88    CycleDetected,
89    /// Query failed with an error.
90    Error { message: String },
91}
92
93/// Asset loading state for tracing.
94#[derive(Debug, Clone, PartialEq, Eq)]
95pub enum TracerAssetState {
96    /// Asset is currently loading.
97    Loading,
98    /// Asset is ready with a value.
99    Ready,
100    /// Asset was not found.
101    NotFound,
102}
103
104/// Reason for cache invalidation.
105#[derive(Debug, Clone, PartialEq, Eq)]
106pub enum InvalidationReason {
107    /// A dependency query changed its output.
108    DependencyChanged { dep: FullCacheKey },
109    /// An asset dependency was updated.
110    AssetChanged { asset: FullCacheKey },
111    /// Manual invalidation was triggered.
112    ManualInvalidation,
113    /// An asset was removed.
114    AssetRemoved { asset: FullCacheKey },
115}
116
117/// Tracer trait for observing query-flow execution.
118///
119/// Implementations can collect events for testing, forward to the `tracing` crate,
120/// or provide custom observability.
121///
122/// All methods have default empty implementations, so you only need to override
123/// the events you're interested in. The [`NoopTracer`] uses all defaults for
124/// zero-cost when tracing is disabled.
125///
126/// # Thread Safety
127///
128/// Implementations must be `Send + Sync` as the tracer may be called from
129/// multiple threads concurrently.
130pub trait Tracer: Send + Sync + 'static {
131    /// Generate a new unique span ID.
132    ///
133    /// Called at the start of each query execution.
134    fn new_span_id(&self) -> SpanId;
135
136    /// Generate a new unique trace ID.
137    ///
138    /// Called when starting a root query (one with no parent in the span stack).
139    fn new_trace_id(&self) -> TraceId;
140
141    /// Called when a query execution starts.
142    ///
143    /// Use `query.type_name()` to get the query type and `query.debug_repr()` for the key.
144    #[inline]
145    fn on_query_start(&self, _ctx: &SpanContext, _query: &QueryCacheKey) {}
146
147    /// Called when cache validity is checked.
148    #[inline]
149    fn on_cache_check(&self, _ctx: &SpanContext, _query: &QueryCacheKey, _valid: bool) {}
150
151    /// Called when a query execution ends.
152    #[inline]
153    fn on_query_end(&self, _ctx: &SpanContext, _query: &QueryCacheKey, _result: ExecutionResult) {}
154
155    /// Called when a query dependency is registered during execution.
156    #[inline]
157    fn on_dependency_registered(
158        &self,
159        _ctx: &SpanContext,
160        _parent: &FullCacheKey,
161        _dependency: &FullCacheKey,
162    ) {
163    }
164
165    /// Called when an asset dependency is registered during execution.
166    #[inline]
167    fn on_asset_dependency_registered(
168        &self,
169        _ctx: &SpanContext,
170        _parent: &FullCacheKey,
171        _asset: &FullCacheKey,
172    ) {
173    }
174
175    /// Called when early cutoff comparison is performed.
176    #[inline]
177    fn on_early_cutoff_check(
178        &self,
179        _ctx: &SpanContext,
180        _query: &QueryCacheKey,
181        _output_changed: bool,
182    ) {
183    }
184
185    /// Called when an asset is requested (START event).
186    ///
187    /// This is called BEFORE the locator executes. Child queries called by
188    /// the locator will appear as children of this asset in the trace tree.
189    #[inline]
190    fn on_asset_requested(&self, _ctx: &SpanContext, _asset: &AssetCacheKey) {}
191
192    /// Called when an asset locator finishes execution.
193    ///
194    /// This is the "end" event for assets, corresponding to `on_query_end` for queries.
195    /// Called after the locator executes with the final state.
196    #[inline]
197    fn on_asset_located(
198        &self,
199        _ctx: &SpanContext,
200        _asset: &AssetCacheKey,
201        _state: TracerAssetState,
202    ) {
203    }
204
205    /// Called when an asset is resolved with a value.
206    #[inline]
207    fn on_asset_resolved(&self, _asset: &AssetCacheKey, _changed: bool) {}
208
209    /// Called when an asset is invalidated.
210    #[inline]
211    fn on_asset_invalidated(&self, _asset: &AssetCacheKey) {}
212
213    /// Called when a query is invalidated.
214    #[inline]
215    fn on_query_invalidated(&self, _query: &QueryCacheKey, _reason: InvalidationReason) {}
216
217    /// Called when a dependency cycle is detected.
218    ///
219    /// The path can contain both queries and assets since cycles may involve asset locators.
220    #[inline]
221    fn on_cycle_detected(&self, _path: &[FullCacheKey]) {}
222
223    /// Called when a query is accessed, providing the [`FullCacheKey`] for GC tracking.
224    ///
225    /// This is called at the start of each query execution, before `on_query_start`.
226    /// Use this to track access times or reference counts for garbage collection.
227    ///
228    /// # Example
229    ///
230    /// ```
231    /// use query_flow::{query, Db, FullCacheKey, QueryError, QueryRuntime, SpanId, TraceId, Tracer};
232    /// use std::collections::HashMap;
233    /// use std::sync::Mutex;
234    /// use std::time::Instant;
235    ///
236    /// struct GcTracer {
237    ///     access_times: Mutex<HashMap<FullCacheKey, Instant>>,
238    /// }
239    ///
240    /// impl Tracer for GcTracer {
241    ///     fn new_span_id(&self) -> SpanId { SpanId(0) }
242    ///
243    ///     fn new_trace_id(&self) -> TraceId { TraceId(0) }
244    ///
245    ///     fn on_query_key(&self, full_key: &FullCacheKey) {
246    ///         self.access_times.lock().unwrap()
247    ///             .insert(full_key.clone(), Instant::now());
248    ///     }
249    /// }
250    ///
251    /// #[query]
252    /// fn double(db: &impl Db, x: i32) -> Result<i32, QueryError> {
253    ///     let _ = db;
254    ///     Ok(x * 2)
255    /// }
256    ///
257    /// let runtime = QueryRuntime::with_tracer(GcTracer {
258    ///     access_times: Mutex::new(HashMap::new()),
259    /// });
260    /// runtime.query(Double::new(21)).unwrap();
261    ///
262    /// assert_eq!(runtime.tracer().access_times.lock().unwrap().len(), 1);
263    /// ```
264    #[inline]
265    fn on_query_key(&self, _full_key: &FullCacheKey) {}
266}
267
268/// Zero-cost tracer that discards all events.
269///
270/// This is the default tracer for [`QueryRuntime`](crate::QueryRuntime).
271pub struct NoopTracer;
272
273impl Tracer for NoopTracer {
274    #[inline(always)]
275    fn new_span_id(&self) -> SpanId {
276        // ZERO is valid because all callbacks are no-ops, so no one observes these IDs.
277        // This avoids atomic counter overhead.
278        SpanId::ZERO
279    }
280
281    #[inline(always)]
282    fn new_trace_id(&self) -> TraceId {
283        TraceId::ZERO
284    }
285    // All other methods use the default empty implementations
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291    use crate::key::QueryCacheKey;
292    use std::sync::atomic::AtomicUsize;
293    use std::sync::atomic::Ordering;
294    use std::sync::Arc;
295
296    struct CountingTracer {
297        start_count: AtomicUsize,
298        end_count: AtomicUsize,
299    }
300
301    impl CountingTracer {
302        fn new() -> Self {
303            Self {
304                start_count: AtomicUsize::new(0),
305                end_count: AtomicUsize::new(0),
306            }
307        }
308    }
309
310    impl Tracer for CountingTracer {
311        fn new_span_id(&self) -> SpanId {
312            SpanId(1)
313        }
314
315        fn new_trace_id(&self) -> TraceId {
316            TraceId(1)
317        }
318
319        fn on_query_start(&self, _ctx: &SpanContext, _query: &QueryCacheKey) {
320            self.start_count.fetch_add(1, Ordering::Relaxed);
321        }
322
323        fn on_query_end(
324            &self,
325            _ctx: &SpanContext,
326            _query: &QueryCacheKey,
327            _result: ExecutionResult,
328        ) {
329            self.end_count.fetch_add(1, Ordering::Relaxed);
330        }
331    }
332
333    #[test]
334    fn test_noop_tracer_returns_zero() {
335        let tracer = NoopTracer;
336        assert_eq!(tracer.new_span_id(), SpanId::ZERO);
337        assert_eq!(tracer.new_trace_id(), TraceId::ZERO);
338    }
339
340    #[test]
341    fn test_counting_tracer() {
342        let tracer = CountingTracer::new();
343        let key = QueryCacheKey::new(("TestQuery",));
344
345        let ctx1 = SpanContext {
346            span_id: SpanId(1),
347            trace_id: TraceId(1),
348            parent_span_id: None,
349        };
350        let ctx2 = SpanContext {
351            span_id: SpanId(2),
352            trace_id: TraceId(1),
353            parent_span_id: Some(SpanId(1)),
354        };
355
356        tracer.on_query_start(&ctx1, &key);
357        tracer.on_query_start(&ctx2, &key);
358        tracer.on_query_end(&ctx1, &key, ExecutionResult::Changed);
359
360        assert_eq!(tracer.start_count.load(Ordering::Relaxed), 2);
361        assert_eq!(tracer.end_count.load(Ordering::Relaxed), 1);
362    }
363
364    #[test]
365    fn test_tracer_is_send_sync() {
366        fn assert_send_sync<T: Send + Sync>() {}
367        assert_send_sync::<NoopTracer>();
368        assert_send_sync::<Arc<CountingTracer>>();
369    }
370}