Skip to main content

linkrs_query/executor/streaming/
query_registry.rs

1//! Query registry — process-level query identity and cancellation.
2//!
3//! Every query execution must first obtain a unique, non-zero `QueryId` from
4//! the [`QueryRegistry`].  The registry also holds the unified cancellation
5//! source, deadline, and weak handles so that `KILL QUERY` works regardless
6//! of which layer initiated cancellation.
7
8use std::sync::atomic::{AtomicU64, Ordering};
9use std::sync::Arc;
10use std::time::Instant;
11
12use dashmap::DashMap;
13
14use super::transaction_scope::CancelReason;
15
16// ── QueryId ─────────────────────────────────────────────────────────────────
17
18/// Typed, non-zero query identifier.
19///
20/// Allocated by [`QueryRegistry::register`].  Zero is never a valid id.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22pub struct QueryId(pub u64);
23
24impl std::fmt::Display for QueryId {
25    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        write!(f, "Q{}", self.0)
27    }
28}
29
30impl QueryId {
31    pub const fn new(value: u64) -> Self {
32        Self(value)
33    }
34
35    pub fn is_zero(&self) -> bool {
36        self.0 == 0
37    }
38
39    pub fn as_u64(&self) -> u64 {
40        self.0
41    }
42
43    pub fn as_i64(&self) -> i64 {
44        self.0 as i64
45    }
46}
47
48// ── QueryMetadata ───────────────────────────────────────────────────────────
49
50/// Metadata stored in the registry for each active query.
51#[derive(Debug, Clone)]
52pub struct QueryMetadata {
53    pub query_id: QueryId,
54    pub session_id: Option<i64>,
55    pub user_name: Option<String>,
56    pub space_name: Option<String>,
57    pub query_text: Option<String>,
58    pub start_time: Instant,
59}
60
61// ── QueryRegistry ───────────────────────────────────────────────────────────
62
63/// Process-level query registry.
64///
65/// - Allocates unique, non-zero `QueryId` values.
66/// - Tracks active queries with metadata and weak runtime handles.
67/// - Provides unified cancellation through a per-query `CancelReason`.
68/// - Responsible for clean teardown: once teardown completes, the query
69///   is removed from the registry.
70#[derive(Debug)]
71pub struct QueryRegistry {
72    next_id: AtomicU64,
73    active: DashMap<QueryId, QueryEntry>,
74}
75
76#[derive(Debug)]
77struct QueryEntry {
78    _metadata: QueryMetadata,
79    cancel_reason: Arc<parking_lot::Mutex<Option<CancelReason>>>,
80}
81
82impl QueryRegistry {
83    pub fn new() -> Self {
84        Self {
85            next_id: AtomicU64::new(1),
86            active: DashMap::new(),
87        }
88    }
89
90    /// Register a new query and obtain its unique `QueryId`.
91    ///
92    /// The caller should later call [`Self::unregister`] once the query
93    /// lifecycle ends (after teardown).
94    pub fn register(&self, metadata: QueryMetadata) -> (QueryId, CancelToken) {
95        self.register_with_token(QueryId(0), metadata, None)
96    }
97
98    /// Register a query under a caller-provided ID.
99    ///
100    /// Used to honor a request-scoped `query_id` (e.g. one supplied by the
101    /// server layer).  If the requested ID is already active or zero, the
102    /// registry falls back to allocating a fresh unique ID so that identity
103    /// uniqueness is never violated.
104    pub fn register_with_id(
105        &self,
106        requested: QueryId,
107        metadata: QueryMetadata,
108    ) -> (QueryId, CancelToken) {
109        self.register_with_token(requested, metadata, None)
110    }
111
112    /// Register a query with an externally-owned cancellation token.
113    ///
114    /// When `external_token` is provided, the registry entry shares the same
115    /// underlying cancellation state, so a `CancelToken::cancel` performed by
116    /// the owner (e.g. [`QueryContext::mark_killed`]) is immediately visible
117    /// through the registry and vice versa.  This is the single-token
118    /// convergence point for KILL QUERY / cancellation.
119    pub fn register_with_token(
120        &self,
121        requested: QueryId,
122        metadata: QueryMetadata,
123        external_token: Option<CancelToken>,
124    ) -> (QueryId, CancelToken) {
125        let id = if requested.is_zero() || self.active.contains_key(&requested) {
126            self.allocate_id()
127        } else {
128            requested
129        };
130        let token = external_token.unwrap_or_default();
131        let reason = token.0.clone();
132
133        self.active.insert(
134            id,
135            QueryEntry {
136                _metadata: QueryMetadata {
137                    query_id: id,
138                    ..metadata
139                },
140                cancel_reason: reason,
141            },
142        );
143
144        (id, token)
145    }
146
147    /// Unregister a completed query.
148    ///
149    /// Returns `true` if the query was actually registered, `false` if it
150    /// was already unknown (harmless — may happen after forced removal).
151    pub fn unregister(&self, query_id: QueryId) -> bool {
152        self.active.remove(&query_id).is_some()
153    }
154
155    /// Cancel a query with a reason.
156    ///
157    /// Returns the first reason that was set (idempotent — subsequent calls
158    /// for the same query return `None`).
159    pub fn cancel(&self, query_id: QueryId, reason: CancelReason) -> Option<CancelReason> {
160        if let Some(entry) = self.active.get(&query_id) {
161            let mut slot = entry.cancel_reason.lock();
162            if slot.is_some() {
163                None // first reason wins
164            } else {
165                *slot = Some(reason);
166                slot.clone()
167            }
168        } else {
169            None
170        }
171    }
172
173    /// Cancel all active queries with a given reason (used on shutdown).
174    pub fn cancel_all(&self, reason: CancelReason) -> Vec<QueryId> {
175        let ids: Vec<QueryId> = self.active.iter().map(|e| *e.key()).collect();
176        for id in &ids {
177            self.cancel(*id, reason.clone());
178        }
179        ids
180    }
181
182    /// Check whether a query has been cancelled and get the reason.
183    pub fn cancellation_reason(&self, query_id: QueryId) -> Option<CancelReason> {
184        self.active
185            .get(&query_id)
186            .and_then(|e| e.cancel_reason.lock().clone())
187    }
188
189    /// Number of currently active queries.
190    pub fn active_count(&self) -> usize {
191        self.active.len()
192    }
193
194    /// Iterate over all active query IDs.
195    pub fn active_queries(&self) -> Vec<QueryId> {
196        self.active.iter().map(|e| *e.key()).collect()
197    }
198
199    fn allocate_id(&self) -> QueryId {
200        loop {
201            let raw = self.next_id.fetch_add(1, Ordering::Relaxed);
202            if raw != 0 {
203                return QueryId(raw);
204            }
205        }
206    }
207}
208
209impl Default for QueryRegistry {
210    fn default() -> Self {
211        Self::new()
212    }
213}
214
215// ── CancelToken ─────────────────────────────────────────────────────────────
216
217/// Thread-safe token that tracks whether a query was cancelled and why.
218///
219/// Shared with [`ExecutionRuntime`](super::runtime::ExecutionRuntime) and
220/// checked by operators during execution.
221#[derive(Debug, Clone)]
222pub struct CancelToken(Arc<parking_lot::Mutex<Option<CancelReason>>>);
223
224impl CancelToken {
225    pub fn new() -> Self {
226        Self(Arc::new(parking_lot::Mutex::new(None)))
227    }
228
229    /// Mark as cancelled.  Returns the _first_ reason that was set, or
230    /// `None` if already cancelled.
231    pub fn cancel(&self, reason: CancelReason) -> Option<CancelReason> {
232        let mut slot = self.0.lock();
233        if slot.is_some() {
234            None
235        } else {
236            *slot = Some(reason);
237            slot.clone()
238        }
239    }
240
241    /// Check whether cancelled.
242    pub fn is_cancelled(&self) -> bool {
243        self.0.lock().is_some()
244    }
245
246    /// Get the cancellation reason, if any.
247    pub fn reason(&self) -> Option<CancelReason> {
248        self.0.lock().clone()
249    }
250
251    /// Clear the cancellation state (for reuse).
252    pub fn clear(&self) {
253        *self.0.lock() = None;
254    }
255}
256
257impl Default for CancelToken {
258    fn default() -> Self {
259        Self::new()
260    }
261}
262
263// ── QueryGuard ──────────────────────────────────────────────────────────────
264
265/// RAII guard that unregisters a query from the registry on drop.
266#[derive(Debug)]
267pub struct QueryGuard {
268    registry: Option<Arc<QueryRegistry>>,
269    query_id: QueryId,
270    finished: bool,
271}
272
273impl QueryGuard {
274    pub fn new(registry: Arc<QueryRegistry>, query_id: QueryId) -> Self {
275        Self {
276            registry: Some(registry),
277            query_id,
278            finished: false,
279        }
280    }
281
282    /// Unregister immediately without waiting for drop.
283    pub fn finish(&mut self) {
284        if !self.finished {
285            self.finished = true;
286            if let Some(ref reg) = self.registry {
287                reg.unregister(self.query_id);
288            }
289        }
290    }
291}
292
293impl Drop for QueryGuard {
294    fn drop(&mut self) {
295        if !self.finished {
296            if let Some(ref reg) = self.registry {
297                reg.unregister(self.query_id);
298            }
299        }
300    }
301}
302
303#[cfg(test)]
304mod tests {
305    use super::*;
306    #[test]
307    fn test_register_and_unregister() {
308        let reg = Arc::new(QueryRegistry::new());
309        let meta = QueryMetadata {
310            query_id: QueryId(0),
311            session_id: Some(1),
312            user_name: Some("test".to_string()),
313            space_name: None,
314            query_text: Some("MATCH (n) RETURN n".to_string()),
315            start_time: Instant::now(),
316        };
317        let (id, _) = reg.register(meta);
318        assert!(!id.is_zero());
319        assert_eq!(reg.active_count(), 1);
320
321        assert!(reg.unregister(id));
322        assert_eq!(reg.active_count(), 0);
323    }
324
325    #[test]
326    fn test_cancel_token() {
327        let token = CancelToken::new();
328        assert!(!token.is_cancelled());
329
330        let first = token.cancel(CancelReason::UserKill);
331        assert_eq!(first, Some(CancelReason::UserKill));
332        assert!(token.is_cancelled());
333
334        let second = token.cancel(CancelReason::Deadline);
335        assert!(second.is_none());
336        assert_eq!(token.reason(), Some(CancelReason::UserKill));
337    }
338
339    #[test]
340    fn test_cancel_all() {
341        let reg = Arc::new(QueryRegistry::new());
342        let meta1 = QueryMetadata {
343            query_id: QueryId(0),
344            session_id: None,
345            user_name: None,
346            space_name: None,
347            query_text: None,
348            start_time: Instant::now(),
349        };
350        let meta2 = meta1.clone();
351        reg.register(meta1);
352        reg.register(meta2);
353
354        let cancelled = reg.cancel_all(CancelReason::Shutdown);
355        assert_eq!(cancelled.len(), 2);
356        assert_eq!(reg.active_count(), 2);
357
358        for id in &cancelled {
359            assert_eq!(reg.cancellation_reason(*id), Some(CancelReason::Shutdown));
360        }
361    }
362
363    #[test]
364    fn test_query_guard_drop() {
365        let reg = Arc::new(QueryRegistry::new());
366        let meta = QueryMetadata {
367            query_id: QueryId(0),
368            session_id: None,
369            user_name: None,
370            space_name: None,
371            query_text: None,
372            start_time: Instant::now(),
373        };
374        let (id, _) = reg.register(meta);
375        {
376            let _guard = QueryGuard::new(reg.clone(), id);
377            assert_eq!(reg.active_count(), 1);
378        }
379        assert_eq!(reg.active_count(), 0);
380    }
381
382    #[test]
383    fn test_query_guard_finish() {
384        let reg = Arc::new(QueryRegistry::new());
385        let meta = QueryMetadata {
386            query_id: QueryId(0),
387            session_id: None,
388            user_name: None,
389            space_name: None,
390            query_text: None,
391            start_time: Instant::now(),
392        };
393        let (id, _) = reg.register(meta);
394        let mut guard = QueryGuard::new(reg.clone(), id);
395        guard.finish();
396        assert_eq!(reg.active_count(), 0);
397        // Drop after finish is safe (no double-unregister)
398    }
399
400    #[test]
401    fn test_ids_are_unique_and_non_zero() {
402        let reg = QueryRegistry::new();
403        let mut ids = std::collections::HashSet::new();
404        for _ in 0..100 {
405            let meta = QueryMetadata {
406                query_id: QueryId(0),
407                session_id: None,
408                user_name: None,
409                space_name: None,
410                query_text: None,
411                start_time: Instant::now(),
412            };
413            let (id, _) = reg.register(meta);
414            assert!(!id.is_zero());
415            assert!(ids.insert(id));
416        }
417    }
418
419    #[test]
420    fn test_register_with_requested_id_honored() {
421        let reg = Arc::new(QueryRegistry::new());
422        let meta = QueryMetadata {
423            query_id: QueryId(0),
424            session_id: Some(7),
425            user_name: None,
426            space_name: None,
427            query_text: None,
428            start_time: Instant::now(),
429        };
430        let (id, _) = reg.register_with_id(QueryId(42), meta);
431        assert_eq!(id, QueryId(42), "request-scoped id must be honored");
432        assert_eq!(reg.active_count(), 1);
433
434        let (id2, _) = reg.register_with_id(
435            QueryId(42),
436            QueryMetadata {
437                query_id: QueryId(0),
438                session_id: None,
439                user_name: None,
440                space_name: None,
441                query_text: None,
442                start_time: Instant::now(),
443            },
444        );
445        assert_ne!(
446            id2,
447            QueryId(42),
448            "a duplicate requested id must fall back to a fresh allocation"
449        );
450        assert!(!id2.is_zero());
451        assert_eq!(reg.active_count(), 2);
452    }
453
454    #[test]
455    fn test_register_with_zero_id_allocates() {
456        let reg = Arc::new(QueryRegistry::new());
457        let meta = QueryMetadata {
458            query_id: QueryId(0),
459            session_id: None,
460            user_name: None,
461            space_name: None,
462            query_text: None,
463            start_time: Instant::now(),
464        };
465        let (id, _) = reg.register_with_id(QueryId(0), meta);
466        assert!(!id.is_zero(), "zero is never a valid id");
467    }
468
469    #[test]
470    fn test_register_with_external_token_shares_state() {
471        let reg = Arc::new(QueryRegistry::new());
472        let external = CancelToken::new();
473        let meta = QueryMetadata {
474            query_id: QueryId(0),
475            session_id: None,
476            user_name: None,
477            space_name: None,
478            query_text: None,
479            start_time: Instant::now(),
480        };
481        let (id, token) = reg.register_with_token(QueryId(0), meta.clone(), Some(external.clone()));
482        assert!(!id.is_zero());
483        assert!(!token.is_cancelled());
484        assert_eq!(reg.cancellation_reason(id), None);
485
486        // Cancelling the external token must be visible through the registry.
487        external.cancel(CancelReason::UserKill);
488        assert!(token.is_cancelled());
489        assert_eq!(reg.cancellation_reason(id), Some(CancelReason::UserKill));
490
491        // Cancelling via the registry must be visible through the token.
492        let external2 = CancelToken::new();
493        let (id2, token2) = reg.register_with_token(QueryId(0), meta, Some(external2.clone()));
494        reg.cancel(id2, CancelReason::Deadline);
495        assert!(token2.is_cancelled());
496        assert_eq!(external2.reason(), Some(CancelReason::Deadline));
497    }
498
499    #[test]
500    fn test_register_without_token_allocates_fresh() {
501        let reg = Arc::new(QueryRegistry::new());
502        let meta = QueryMetadata {
503            query_id: QueryId(0),
504            session_id: None,
505            user_name: None,
506            space_name: None,
507            query_text: None,
508            start_time: Instant::now(),
509        };
510        let (id, token) = reg.register_with_token(QueryId(0), meta, None);
511        assert!(!token.is_cancelled());
512        reg.cancel(id, CancelReason::Shutdown);
513        assert!(token.is_cancelled());
514    }
515}