sqlitegraph/backend/native/v2/edge_cluster/
cluster_trace.rs1use crate::backend::native::FileOffset;
8use std::cell::{Cell, RefCell};
9use std::fmt::Write;
10
11#[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
77pub fn strict_mode_enabled() -> bool {
79 STRICT_MODE.with(|cell| cell.get())
80}
81
82pub 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
91pub fn current_trace_context() -> Option<TraceContext> {
93 TRACE_CONTEXT.with(|slot| *slot.borrow())
94}
95
96pub 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 let initial_state = strict_mode_enabled();
166 {
167 let _guard = StrictModeGuard::new(true);
168 assert!(strict_mode_enabled());
169 }
170 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 let current_ctx = current_trace_context();
188 assert!(current_ctx.is_some());
189 assert_eq!(current_ctx.unwrap().node_id, 123);
190 }
191
192 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 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}