Skip to main content

sqlitegraph/backend/native/v2/edge_cluster/
cluster_trace.rs

1//! Trace and debugging infrastructure for edge cluster operations.
2//!
3//! This module provides thread-local trace context management and strict mode
4//! validation for cluster operations. It isolates debugging concerns from
5//! the core cluster logic.
6
7use crate::backend::native::FileOffset;
8use std::cell::{Cell, RefCell};
9use std::fmt::Write;
10
11/// Adjacency direction for cluster construction.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13pub enum Direction {
14    Outgoing,
15    Incoming,
16}
17
18#[derive(Clone, Copy, Debug)]
19pub struct TraceContext {
20    pub node_id: i64,
21    pub direction: Direction,
22    pub cluster_offset: FileOffset,
23    pub payload_size: u32,
24    pub strict: bool,
25}
26
27pub struct TraceGuard {
28    strict_guard: StrictModeGuard,
29}
30
31pub struct StrictModeGuard {
32    previous: bool,
33}
34
35thread_local! {
36    static TRACE_CONTEXT: RefCell<Option<TraceContext>> = RefCell::new(None);
37    static STRICT_MODE: Cell<bool> = Cell::new(false);
38}
39
40impl TraceGuard {
41    pub fn new(context: TraceContext) -> Self {
42        TRACE_CONTEXT.with(|slot| {
43            *slot.borrow_mut() = Some(context);
44        });
45        let strict_guard = StrictModeGuard::new(context.strict);
46        TraceGuard { strict_guard }
47    }
48}
49
50impl Drop for TraceGuard {
51    fn drop(&mut self) {
52        TRACE_CONTEXT.with(|slot| {
53            slot.borrow_mut().take();
54        });
55    }
56}
57
58impl StrictModeGuard {
59    pub fn new(strict: bool) -> Self {
60        let previous = STRICT_MODE.with(|cell| {
61            let prev = cell.get();
62            cell.set(strict);
63            prev
64        });
65        StrictModeGuard { previous }
66    }
67}
68
69impl Drop for StrictModeGuard {
70    fn drop(&mut self) {
71        STRICT_MODE.with(|cell| {
72            cell.set(self.previous);
73        });
74    }
75}
76
77/// Check if strict mode is currently enabled for trace validation.
78pub fn strict_mode_enabled() -> bool {
79    STRICT_MODE.with(|cell| cell.get())
80}
81
82/// Execute a function with the current trace context if available.
83pub fn with_trace_context<F: FnOnce(&TraceContext)>(f: F) {
84    TRACE_CONTEXT.with(|slot| {
85        if let Some(ctx) = *slot.borrow() {
86            f(&ctx);
87        }
88    });
89}
90
91/// Get the current trace context if available.
92pub fn current_trace_context() -> Option<TraceContext> {
93    TRACE_CONTEXT.with(|slot| *slot.borrow())
94}
95
96/// Format a detailed reason string for strict mode violations.
97pub fn format_strict_reason(
98    ctx: Option<TraceContext>,
99    detail: &str,
100    edge_index: usize,
101    cursor: usize,
102    payload_size: usize,
103    remaining: usize,
104    preview: &[u8],
105) -> String {
106    let mut preview_hex = String::new();
107    for (i, byte) in preview.iter().enumerate() {
108        if i > 0 {
109            preview_hex.push(' ');
110        }
111        let _ = write!(&mut preview_hex, "{:02X}", byte);
112    }
113    let preview_ascii = String::from_utf8_lossy(preview);
114
115    if let Some(ctx) = ctx {
116        format!(
117            "{} [node_id={}, direction={:?}, cluster_offset={}, payload_size={}, edge_index={}, cursor={}, remaining={}, preview_hex={}, preview_ascii={:?}]",
118            detail,
119            ctx.node_id,
120            ctx.direction,
121            ctx.cluster_offset,
122            payload_size,
123            edge_index,
124            cursor,
125            remaining,
126            preview_hex,
127            preview_ascii
128        )
129    } else {
130        format!(
131            "{} [payload_size={}, edge_index={}, cursor={}, remaining={}, preview_hex={}, preview_ascii={:?}]",
132            detail, payload_size, edge_index, cursor, remaining, preview_hex, preview_ascii
133        )
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn test_direction_equality() {
143        assert_eq!(Direction::Outgoing, Direction::Outgoing);
144        assert_eq!(Direction::Incoming, Direction::Incoming);
145        assert_ne!(Direction::Outgoing, Direction::Incoming);
146    }
147
148    #[test]
149    fn test_trace_context_creation() {
150        let ctx = TraceContext {
151            node_id: 42,
152            direction: Direction::Outgoing,
153            cluster_offset: 1000,
154            payload_size: 500,
155            strict: true,
156        };
157        assert_eq!(ctx.node_id, 42);
158        assert_eq!(ctx.direction, Direction::Outgoing);
159        assert!(ctx.strict);
160    }
161
162    #[test]
163    fn test_strict_mode_guard() {
164        // Test that strict mode guard preserves previous state
165        let initial_state = strict_mode_enabled();
166        {
167            let _guard = StrictModeGuard::new(true);
168            assert!(strict_mode_enabled());
169        }
170        // Should return to original state after guard drops
171        assert_eq!(strict_mode_enabled(), initial_state);
172    }
173
174    #[test]
175    fn test_trace_guard() {
176        let ctx = TraceContext {
177            node_id: 123,
178            direction: Direction::Incoming,
179            cluster_offset: 2000,
180            payload_size: 300,
181            strict: false,
182        };
183
184        {
185            let _guard = TraceGuard::new(ctx);
186            // Test that trace context is available
187            let current_ctx = current_trace_context();
188            assert!(current_ctx.is_some());
189            assert_eq!(current_ctx.unwrap().node_id, 123);
190        }
191
192        // Trace context should be cleared after guard drops
193        assert!(current_trace_context().is_none());
194    }
195
196    #[test]
197    fn test_with_trace_context() {
198        let ctx = TraceContext {
199            node_id: 999,
200            direction: Direction::Outgoing,
201            cluster_offset: 5000,
202            payload_size: 100,
203            strict: true,
204        };
205
206        {
207            let _guard = TraceGuard::new(ctx);
208            let mut called = false;
209            with_trace_context(|trace_ctx| {
210                called = true;
211                assert_eq!(trace_ctx.node_id, 999);
212                assert_eq!(trace_ctx.direction, Direction::Outgoing);
213            });
214            assert!(called);
215        }
216
217        // Should not call function when no trace context
218        let mut called = false;
219        with_trace_context(|_trace_ctx| {
220            called = true;
221        });
222        assert!(!called);
223    }
224
225    #[test]
226    fn test_format_strict_reason_with_context() {
227        let ctx = TraceContext {
228            node_id: 42,
229            direction: Direction::Outgoing,
230            cluster_offset: 1000,
231            payload_size: 200,
232            strict: true,
233        };
234
235        let reason =
236            format_strict_reason(Some(ctx), "Test error", 5, 100, 200, 50, b"\x01\x02\x03");
237
238        assert!(reason.contains("Test error"));
239        assert!(reason.contains("node_id=42"));
240        assert!(reason.contains("direction=Outgoing"));
241        assert!(reason.contains("cluster_offset=1000"));
242        assert!(reason.contains("edge_index=5"));
243        assert!(reason.contains("01 02 03"));
244    }
245
246    #[test]
247    fn test_format_strict_reason_without_context() {
248        let reason = format_strict_reason(None, "Test error", 3, 50, 150, 75, b"\xFF\xEE");
249
250        assert!(reason.contains("Test error"));
251        assert!(reason.contains("payload_size=150"));
252        assert!(reason.contains("edge_index=3"));
253        assert!(reason.contains("FF EE"));
254        assert!(!reason.contains("node_id="));
255    }
256}