1use crate::{MongrelQueryError, Result, SqlTestHook};
2use mongreldb_core::{CancellationReason, ExecutionControl};
3use parking_lot::Mutex;
4use std::collections::{HashMap, VecDeque};
5use std::fmt;
6use std::str::FromStr;
7use std::sync::atomic::{AtomicU8, AtomicUsize, Ordering};
8use std::sync::{Arc, Weak};
9use std::time::{Duration, Instant};
10
11const DEFAULT_MAX_ACTIVE: usize = 1_024;
12const DEFAULT_MAX_FINISHED: usize = 2_048;
13const DEFAULT_MAX_FINISHED_BYTES: usize = 8 * 1024 * 1024;
14const DEFAULT_MAX_COMPACT: usize = 100_000;
15const DEFAULT_MAX_COMPACT_BYTES: usize = 32 * 1024 * 1024;
16const DEFAULT_FINISHED_TTL: Duration = Duration::from_secs(60);
17const MAX_METADATA_BYTES: usize = 256;
18
19#[derive(Clone, Copy, PartialEq, Eq, Hash)]
20pub struct QueryId([u8; 16]);
21
22impl QueryId {
23 pub fn random() -> Result<Self> {
24 let mut bytes = [0; 16];
25 getrandom::getrandom(&mut bytes).map_err(|error| {
26 MongrelQueryError::Core(mongreldb_core::MongrelError::Other(format!(
27 "query id randomness failed: {error}"
28 )))
29 })?;
30 Ok(Self(bytes))
31 }
32
33 pub fn as_bytes(&self) -> &[u8; 16] {
34 &self.0
35 }
36}
37
38impl fmt::Display for QueryId {
39 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
40 for byte in self.0 {
41 write!(formatter, "{byte:02x}")?;
42 }
43 Ok(())
44 }
45}
46
47impl fmt::Debug for QueryId {
48 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
49 fmt::Display::fmt(self, formatter)
50 }
51}
52
53impl FromStr for QueryId {
54 type Err = MongrelQueryError;
55
56 fn from_str(value: &str) -> Result<Self> {
57 if value.len() != 32 || !value.bytes().all(|byte| byte.is_ascii_hexdigit()) {
58 return Err(MongrelQueryError::Core(
59 mongreldb_core::MongrelError::InvalidArgument(
60 "query id must be exactly 32 hexadecimal characters".into(),
61 ),
62 ));
63 }
64 let mut bytes = [0; 16];
65 for (index, chunk) in value.as_bytes().chunks_exact(2).enumerate() {
66 let text = std::str::from_utf8(chunk).map_err(|_| {
67 MongrelQueryError::Core(mongreldb_core::MongrelError::InvalidArgument(
68 "query id is not valid UTF-8".into(),
69 ))
70 })?;
71 bytes[index] = u8::from_str_radix(text, 16).map_err(|_| {
72 MongrelQueryError::Core(mongreldb_core::MongrelError::InvalidArgument(
73 "query id contains invalid hexadecimal".into(),
74 ))
75 })?;
76 }
77 Ok(Self(bytes))
78 }
79}
80
81impl serde::Serialize for QueryId {
82 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
83 where
84 S: serde::Serializer,
85 {
86 serializer.serialize_str(&self.to_string())
87 }
88}
89
90impl<'de> serde::Deserialize<'de> for QueryId {
91 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
92 where
93 D: serde::Deserializer<'de>,
94 {
95 let value = <String as serde::Deserialize>::deserialize(deserializer)?;
96 value.parse().map_err(serde::de::Error::custom)
97 }
98}
99
100#[derive(Debug, Clone, Default)]
101pub struct SqlQueryOptions {
102 pub query_id: Option<QueryId>,
103 pub timeout: Option<Duration>,
104 pub owner: Option<String>,
105 pub session_id: Option<String>,
106 pub parent_control: Option<ExecutionControl>,
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110#[repr(u8)]
111pub enum SqlQueryPhase {
112 Queued = 0,
113 Planning = 1,
114 Executing = 2,
115 Streaming = 3,
116 Serializing = 4,
117 CommitCritical = 5,
118 Cancelling = 6,
119 Completed = 7,
120 Failed = 8,
121 Cancelled = 9,
122}
123
124impl SqlQueryPhase {
125 fn from_u8(value: u8) -> Self {
126 match value {
127 0 => Self::Queued,
128 1 => Self::Planning,
129 2 => Self::Executing,
130 3 => Self::Streaming,
131 4 => Self::Serializing,
132 5 => Self::CommitCritical,
133 6 => Self::Cancelling,
134 7 => Self::Completed,
135 8 => Self::Failed,
136 9 => Self::Cancelled,
137 _ => Self::Failed,
138 }
139 }
140
141 fn is_terminal(self) -> bool {
142 matches!(self, Self::Completed | Self::Failed | Self::Cancelled)
143 }
144}
145
146#[derive(Debug, Clone, Copy, PartialEq, Eq)]
147pub enum CancelOutcome {
148 Accepted,
149 AlreadyCancelling,
150 TooLate,
151 AlreadyFinished,
152 NotFound,
153}
154
155#[derive(Debug, Clone, Default, PartialEq, Eq)]
156pub struct DurableOutcome {
157 pub committed: bool,
158 pub committed_statements: usize,
159 pub last_commit_epoch: Option<u64>,
160 pub first_commit_statement_index: Option<usize>,
161 pub last_commit_statement_index: Option<usize>,
162}
163
164impl DurableOutcome {
165 fn record_commit(&mut self, statement_index: usize, epoch: Option<u64>) {
166 self.committed = true;
167 if self.last_commit_statement_index != Some(statement_index) {
168 self.committed_statements = self.committed_statements.saturating_add(1);
169 }
170 self.first_commit_statement_index
171 .get_or_insert(statement_index);
172 self.last_commit_statement_index = Some(statement_index);
173 if epoch.is_some() {
174 self.last_commit_epoch = epoch;
175 }
176 }
177}
178
179#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
180pub enum SerializationOutcome {
181 #[default]
182 NotStarted,
183 InProgress,
184 Succeeded,
185 Failed,
186}
187
188#[derive(Debug, Clone, Copy, PartialEq, Eq)]
189pub enum QueryTerminalErrorCategory {
190 Cancellation,
191 Deadline,
192 ResultLimit,
193 Serialization,
194 Execution,
195}
196
197#[derive(Debug, Clone, PartialEq, Eq)]
198pub struct QueryTerminalError {
199 pub code: String,
200 pub category: QueryTerminalErrorCategory,
201}
202
203#[derive(Debug, Clone, Default, PartialEq, Eq)]
204struct QueryOutcomeState {
205 durable: DurableOutcome,
206 serialization: SerializationOutcome,
207 terminal_error: Option<QueryTerminalError>,
208 terminal_state_override: Option<QueryTerminalState>,
209 cancellation_reason_override: Option<CancellationReason>,
210 phase_override: Option<SqlQueryPhase>,
211}
212
213#[derive(Debug, Clone, Copy, PartialEq, Eq)]
214pub enum QueryTerminalState {
215 OutcomeUnknown,
216 Completed,
217 FailedBeforeCommit,
218 CancelledBeforeCommit,
219 DeadlineBeforeCommit,
220 Committed,
221 CommittedWithError,
222 PartiallyCommitted,
223 CancelledAfterCommit,
224 DeadlineAfterCommit,
225}
226
227#[derive(Debug, Clone)]
228pub struct QueryStatus {
229 pub query_id: QueryId,
230 pub owner: Option<String>,
231 pub session_id: Option<String>,
232 pub phase: SqlQueryPhase,
233 pub started_at: Instant,
234 pub deadline: Option<Instant>,
235 pub operation: String,
236 pub sql_fingerprint: [u8; 32],
237 pub cancellation_reason: CancellationReason,
238 pub committed: bool,
240 pub durable_outcome: DurableOutcome,
241 pub terminal_error: Option<QueryTerminalError>,
242 pub serialization_outcome: SerializationOutcome,
243 pub outcome_unknown: bool,
244 terminal_state_override: Option<QueryTerminalState>,
245 pub completed_statements: usize,
246 pub statement_index: usize,
247 pub cancel_requested_at: Option<Instant>,
248 pub queue_duration: Duration,
249 pub planning_duration: Duration,
250 pub execution_duration: Duration,
251 pub serialization_duration: Duration,
252 pub cancel_requested_phase: Option<SqlQueryPhase>,
253 pub cancel_observed_phase: Option<SqlQueryPhase>,
254 pub commit_fence_outcome: CommitFenceOutcome,
255}
256
257impl QueryStatus {
258 pub fn terminal_state(&self) -> Option<QueryTerminalState> {
259 if !self.phase.is_terminal() {
260 return None;
261 }
262 if let Some(terminal_state) = self.terminal_state_override {
263 return Some(terminal_state);
264 }
265 if !self.durable_outcome.committed {
266 return Some(match (self.phase, self.cancellation_reason) {
267 (SqlQueryPhase::Completed, _) => QueryTerminalState::Completed,
268 (SqlQueryPhase::Cancelled, CancellationReason::Deadline) => {
269 QueryTerminalState::DeadlineBeforeCommit
270 }
271 (SqlQueryPhase::Cancelled, _) => QueryTerminalState::CancelledBeforeCommit,
272 _ => QueryTerminalState::FailedBeforeCommit,
273 });
274 }
275 Some(match (self.phase, self.cancellation_reason) {
276 (SqlQueryPhase::Completed, _) => QueryTerminalState::Committed,
277 (SqlQueryPhase::Cancelled, CancellationReason::Deadline) => {
278 QueryTerminalState::DeadlineAfterCommit
279 }
280 (SqlQueryPhase::Cancelled, _) => QueryTerminalState::CancelledAfterCommit,
281 _ if self
282 .durable_outcome
283 .last_commit_statement_index
284 .is_some_and(|index| index < self.statement_index) =>
285 {
286 QueryTerminalState::PartiallyCommitted
287 }
288 _ => QueryTerminalState::CommittedWithError,
289 })
290 }
291}
292
293#[derive(Debug, Clone, Copy, PartialEq, Eq)]
294pub enum CommitFenceOutcome {
295 NotReached,
296 CancelWon,
297 CommitWon,
298}
299
300#[derive(Debug, Clone)]
301struct QueryTrace {
302 phase_started_at: Instant,
303 queue_duration: Duration,
304 planning_duration: Duration,
305 execution_duration: Duration,
306 serialization_duration: Duration,
307 cancel_requested_phase: Option<SqlQueryPhase>,
308 cancel_observed_phase: Option<SqlQueryPhase>,
309 commit_fence_outcome: CommitFenceOutcome,
310}
311
312impl QueryTrace {
313 fn new(started_at: Instant) -> Self {
314 Self {
315 phase_started_at: started_at,
316 queue_duration: Duration::ZERO,
317 planning_duration: Duration::ZERO,
318 execution_duration: Duration::ZERO,
319 serialization_duration: Duration::ZERO,
320 cancel_requested_phase: None,
321 cancel_observed_phase: None,
322 commit_fence_outcome: CommitFenceOutcome::NotReached,
323 }
324 }
325
326 fn transition(&mut self, phase: SqlQueryPhase) {
327 let now = Instant::now();
328 let elapsed = now.saturating_duration_since(self.phase_started_at);
329 match phase {
330 SqlQueryPhase::Queued => self.queue_duration += elapsed,
331 SqlQueryPhase::Planning => self.planning_duration += elapsed,
332 SqlQueryPhase::Executing | SqlQueryPhase::Streaming | SqlQueryPhase::CommitCritical => {
333 self.execution_duration += elapsed
334 }
335 SqlQueryPhase::Serializing => self.serialization_duration += elapsed,
336 SqlQueryPhase::Cancelling
337 | SqlQueryPhase::Completed
338 | SqlQueryPhase::Failed
339 | SqlQueryPhase::Cancelled => {}
340 }
341 self.phase_started_at = now;
342 }
343}
344
345#[derive(Debug)]
346struct RegisteredQuery {
347 id: QueryId,
348 owner: Option<String>,
349 session_id: Option<String>,
350 control: ExecutionControl,
351 phase: AtomicU8,
352 started_at: Instant,
353 deadline: Option<Instant>,
354 operation: Mutex<String>,
355 sql_fingerprint: Mutex<[u8; 32]>,
356 outcome: Mutex<QueryOutcomeState>,
357 completed_statements: AtomicUsize,
358 statement_index: AtomicUsize,
359 cancel_requested_at: Mutex<Option<Instant>>,
360 trace: Mutex<QueryTrace>,
361}
362
363impl RegisteredQuery {
364 fn phase(&self) -> SqlQueryPhase {
365 SqlQueryPhase::from_u8(self.phase.load(Ordering::Acquire))
366 }
367
368 fn status(&self) -> QueryStatus {
369 let outcome = self.outcome.lock().clone();
370 let phase = outcome.phase_override.unwrap_or_else(|| self.phase());
371 let mut trace = self.trace.lock().clone();
372 trace.transition(phase);
373 QueryStatus {
374 query_id: self.id,
375 owner: self.owner.clone(),
376 session_id: self.session_id.clone(),
377 phase,
378 started_at: self.started_at,
379 deadline: self.deadline,
380 operation: self.operation.lock().clone(),
381 sql_fingerprint: *self.sql_fingerprint.lock(),
382 cancellation_reason: outcome
383 .cancellation_reason_override
384 .unwrap_or_else(|| self.control.reason()),
385 committed: outcome.durable.committed,
386 durable_outcome: outcome.durable,
387 terminal_error: outcome.terminal_error,
388 serialization_outcome: outcome.serialization,
389 outcome_unknown: outcome.terminal_state_override
390 == Some(QueryTerminalState::OutcomeUnknown),
391 terminal_state_override: outcome.terminal_state_override,
392 completed_statements: self.completed_statements.load(Ordering::Acquire),
393 statement_index: self.statement_index.load(Ordering::Acquire),
394 cancel_requested_at: *self.cancel_requested_at.lock(),
395 queue_duration: trace.queue_duration,
396 planning_duration: trace.planning_duration,
397 execution_duration: trace.execution_duration,
398 serialization_duration: trace.serialization_duration,
399 cancel_requested_phase: trace.cancel_requested_phase,
400 cancel_observed_phase: trace.cancel_observed_phase,
401 commit_fence_outcome: trace.commit_fence_outcome,
402 }
403 }
404
405 fn record_transition(&self, phase: SqlQueryPhase) {
406 self.trace.lock().transition(phase);
407 }
408}
409
410#[derive(Debug, Clone)]
411struct FinishedQuery {
412 status: QueryStatus,
413 finished_at: Instant,
414 approximate_bytes: usize,
415}
416
417#[derive(Debug, Clone)]
418pub struct CompactFinishedQuery {
419 pub query_id: QueryId,
420 pub owner: Option<String>,
421 pub session_id: Option<String>,
422 pub phase: SqlQueryPhase,
423 pub terminal_state: QueryTerminalState,
424 pub cancellation_reason: CancellationReason,
425 pub durable_outcome: DurableOutcome,
426 pub serialization_outcome: SerializationOutcome,
427 pub terminal_error: Option<QueryTerminalError>,
428 pub completed_statements: usize,
429 pub statement_index: usize,
430 finished_at: Instant,
431 approximate_bytes: usize,
432}
433
434impl CompactFinishedQuery {
435 fn from_status(status: QueryStatus, finished_at: Instant) -> Self {
436 let terminal_state = status
437 .terminal_state()
438 .unwrap_or(QueryTerminalState::OutcomeUnknown);
439 let approximate_bytes = std::mem::size_of::<Self>()
440 + status.owner.as_ref().map_or(0, String::len)
441 + status.session_id.as_ref().map_or(0, String::len)
442 + status
443 .terminal_error
444 .as_ref()
445 .map_or(0, |error| error.code.len());
446 Self {
447 query_id: status.query_id,
448 owner: status.owner,
449 session_id: status.session_id,
450 phase: status.phase,
451 terminal_state,
452 cancellation_reason: status.cancellation_reason,
453 durable_outcome: status.durable_outcome,
454 serialization_outcome: status.serialization_outcome,
455 terminal_error: status.terminal_error,
456 completed_statements: status.completed_statements,
457 statement_index: status.statement_index,
458 finished_at,
459 approximate_bytes,
460 }
461 }
462}
463
464#[derive(Debug, Default)]
465struct RegistryState {
466 active: HashMap<QueryId, Arc<RegisteredQuery>>,
467 detailed: HashMap<QueryId, FinishedQuery>,
468 detailed_lru: VecDeque<QueryId>,
469 detailed_bytes: usize,
470 compact: HashMap<QueryId, CompactFinishedQuery>,
471 compact_lru: VecDeque<QueryId>,
472 compact_bytes: usize,
473 demotions: u64,
474 compact_evictions: u64,
475 active_rejections: u64,
476}
477
478#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
479pub struct QueryRegistryStats {
480 pub active: usize,
481 pub queued: usize,
482 pub detailed: usize,
483 pub compact: usize,
484 pub detailed_bytes: usize,
485 pub compact_bytes: usize,
486 pub demotions: u64,
487 pub compact_evictions: u64,
488 pub active_rejections: u64,
489 pub oldest_compact_age: Duration,
490}
491
492#[derive(Debug)]
493pub struct SqlQueryRegistry {
494 state: Mutex<RegistryState>,
495 max_active: usize,
496 max_detailed: usize,
497 max_detailed_bytes: usize,
498 max_compact: usize,
499 max_compact_bytes: usize,
500 finished_ttl: Duration,
501}
502
503impl Default for SqlQueryRegistry {
504 fn default() -> Self {
505 Self::new_with_limits(
506 DEFAULT_MAX_ACTIVE,
507 DEFAULT_MAX_FINISHED,
508 DEFAULT_MAX_FINISHED_BYTES,
509 DEFAULT_MAX_COMPACT,
510 DEFAULT_MAX_COMPACT_BYTES,
511 DEFAULT_FINISHED_TTL,
512 )
513 }
514}
515
516impl SqlQueryRegistry {
517 pub fn new(
518 max_active: usize,
519 max_finished: usize,
520 max_finished_bytes: usize,
521 finished_ttl: Duration,
522 ) -> Self {
523 Self::new_with_limits(
524 max_active,
525 max_finished,
526 max_finished_bytes,
527 DEFAULT_MAX_COMPACT,
528 DEFAULT_MAX_COMPACT_BYTES,
529 finished_ttl,
530 )
531 }
532
533 pub fn new_with_limits(
534 max_active: usize,
535 max_detailed: usize,
536 max_detailed_bytes: usize,
537 max_compact: usize,
538 max_compact_bytes: usize,
539 finished_ttl: Duration,
540 ) -> Self {
541 Self {
542 state: Mutex::new(RegistryState::default()),
543 max_active: max_active.max(1),
544 max_detailed,
545 max_detailed_bytes,
546 max_compact,
547 max_compact_bytes,
548 finished_ttl,
549 }
550 }
551
552 pub fn register(self: &Arc<Self>, options: SqlQueryOptions) -> Result<RegisteredSqlQuery> {
553 validate_metadata("owner", options.owner.as_deref())?;
554 validate_metadata("session id", options.session_id.as_deref())?;
555 let id = match options.query_id {
556 Some(id) => id,
557 None => QueryId::random()?,
558 };
559 let deadline_base = Instant::now();
560 let deadline = options
561 .timeout
562 .map(|timeout| {
563 deadline_base.checked_add(timeout).ok_or_else(|| {
564 MongrelQueryError::Core(mongreldb_core::MongrelError::InvalidArgument(
565 "query timeout exceeds the monotonic clock range".into(),
566 ))
567 })
568 })
569 .transpose()?;
570 let control = match options.parent_control {
571 Some(parent) => parent.child_with_deadline(deadline),
572 None => ExecutionControl::new(deadline),
573 };
574 let started_at = Instant::now();
575 let query = Arc::new(RegisteredQuery {
576 id,
577 owner: options.owner,
578 session_id: options.session_id,
579 deadline: control.deadline(),
580 control,
581 phase: AtomicU8::new(SqlQueryPhase::Queued as u8),
582 started_at,
583 operation: Mutex::new("UNKNOWN".into()),
584 sql_fingerprint: Mutex::new([0; 32]),
585 outcome: Mutex::new(QueryOutcomeState::default()),
586 completed_statements: AtomicUsize::new(0),
587 statement_index: AtomicUsize::new(0),
588 cancel_requested_at: Mutex::new(None),
589 trace: Mutex::new(QueryTrace::new(started_at)),
590 });
591 let mut state = self.state.lock();
592 self.prune_locked(&mut state);
593 if state.active.contains_key(&id)
594 || state.detailed.contains_key(&id)
595 || state.compact.contains_key(&id)
596 {
597 return Err(MongrelQueryError::QueryIdConflict { query_id: id });
598 }
599 if state.active.len() >= self.max_active {
600 state.active_rejections = state.active_rejections.saturating_add(1);
601 return Err(MongrelQueryError::QueryRegistryFull);
602 }
603 state.active.insert(id, Arc::clone(&query));
604 Ok(RegisteredSqlQuery {
605 registry: Arc::downgrade(self),
606 query,
607 })
608 }
609
610 pub fn cancel(&self, query_id: QueryId) -> CancelOutcome {
611 let query = {
612 let mut state = self.state.lock();
613 self.prune_locked(&mut state);
614 if let Some(query) = state.active.get(&query_id) {
615 Some(Arc::clone(query))
616 } else if state.detailed.contains_key(&query_id)
617 || state.compact.contains_key(&query_id)
618 {
619 return CancelOutcome::AlreadyFinished;
620 } else {
621 return CancelOutcome::NotFound;
622 }
623 };
624 match query {
625 Some(query) => query.request_cancel(CancellationReason::ClientRequest),
626 None => CancelOutcome::NotFound,
627 }
628 }
629
630 pub fn status(&self, query_id: QueryId) -> Option<QueryStatus> {
631 let mut state = self.state.lock();
632 self.prune_locked(&mut state);
633 state
634 .active
635 .get(&query_id)
636 .map(|query| query.status())
637 .or_else(|| {
638 state
639 .detailed
640 .get(&query_id)
641 .map(|finished| finished.status.clone())
642 })
643 }
644
645 pub fn compact_finished_status(&self, query_id: QueryId) -> Option<CompactFinishedQuery> {
646 let mut state = self.state.lock();
647 self.prune_locked(&mut state);
648 state.compact.get(&query_id).cloned()
649 }
650
651 pub fn cancel_session(&self, session_id: &str, reason: CancellationReason) -> usize {
652 let queries = {
653 let state = self.state.lock();
654 state
655 .active
656 .values()
657 .filter(|query| query.session_id.as_deref() == Some(session_id))
658 .cloned()
659 .collect::<Vec<_>>()
660 };
661 let mut accepted = 0;
662 for query in queries {
663 if query.request_cancel(reason) == CancelOutcome::Accepted {
664 accepted += 1;
665 }
666 }
667 accepted
668 }
669
670 pub fn cancel_all(&self, reason: CancellationReason) -> usize {
671 let queries = self
672 .state
673 .lock()
674 .active
675 .values()
676 .cloned()
677 .collect::<Vec<_>>();
678 let mut accepted = 0;
679 for query in queries {
680 if query.request_cancel(reason) == CancelOutcome::Accepted {
681 accepted += 1;
682 }
683 }
684 accepted
685 }
686
687 pub fn active_count(&self) -> usize {
688 self.state.lock().active.len()
689 }
690
691 pub fn active_statuses(&self) -> Vec<QueryStatus> {
692 self.state
693 .lock()
694 .active
695 .values()
696 .map(|query| query.status())
697 .collect()
698 }
699
700 pub fn active_for_session(&self, session_id: &str) -> usize {
701 self.state
702 .lock()
703 .active
704 .values()
705 .filter(|query| query.session_id.as_deref() == Some(session_id))
706 .count()
707 }
708
709 pub fn queued_count(&self) -> usize {
710 self.state
711 .lock()
712 .active
713 .values()
714 .filter(|query| query.phase() == SqlQueryPhase::Queued)
715 .count()
716 }
717
718 pub fn entry_count(&self) -> usize {
719 let mut state = self.state.lock();
720 self.prune_locked(&mut state);
721 state.active.len() + state.detailed.len() + state.compact.len()
722 }
723
724 pub fn approximate_bytes(&self) -> usize {
725 let mut state = self.state.lock();
726 self.prune_locked(&mut state);
727 state.detailed_bytes
728 + state.compact_bytes
729 + state
730 .active
731 .len()
732 .saturating_mul(std::mem::size_of::<RegisteredQuery>())
733 }
734
735 pub fn finished_count(&self) -> usize {
736 let mut state = self.state.lock();
737 self.prune_locked(&mut state);
738 state.detailed.len() + state.compact.len()
739 }
740
741 pub fn stats(&self) -> QueryRegistryStats {
742 let mut state = self.state.lock();
743 self.prune_locked(&mut state);
744 let now = Instant::now();
745 QueryRegistryStats {
746 active: state.active.len(),
747 queued: state
748 .active
749 .values()
750 .filter(|query| query.phase() == SqlQueryPhase::Queued)
751 .count(),
752 detailed: state.detailed.len(),
753 compact: state.compact.len(),
754 detailed_bytes: state.detailed_bytes,
755 compact_bytes: state.compact_bytes,
756 demotions: state.demotions,
757 compact_evictions: state.compact_evictions,
758 active_rejections: state.active_rejections,
759 oldest_compact_age: state
760 .compact_lru
761 .front()
762 .and_then(|id| state.compact.get(id))
763 .map_or(Duration::ZERO, |entry| {
764 now.saturating_duration_since(entry.finished_at)
765 }),
766 }
767 }
768
769 fn finish(&self, query: &Arc<RegisteredQuery>) {
770 debug_assert!(query.phase().is_terminal());
771 let status = query.status();
772 let approximate_bytes = std::mem::size_of::<FinishedQuery>()
773 + status.owner.as_ref().map_or(0, String::len)
774 + status.session_id.as_ref().map_or(0, String::len)
775 + status.operation.len()
776 + status
777 .terminal_error
778 .as_ref()
779 .map_or(0, |error| error.code.len());
780 let mut state = self.state.lock();
781 if state.active.remove(&query.id).is_none() {
782 return;
783 }
784 let finished_at = Instant::now();
785 if self.max_detailed > 0 && self.max_detailed_bytes > 0 {
786 state.detailed_bytes = state.detailed_bytes.saturating_add(approximate_bytes);
787 state.detailed_lru.push_back(query.id);
788 state.detailed.insert(
789 query.id,
790 FinishedQuery {
791 status,
792 finished_at,
793 approximate_bytes,
794 },
795 );
796 } else {
797 self.insert_compact_locked(
798 &mut state,
799 CompactFinishedQuery::from_status(status, finished_at),
800 );
801 }
802 self.prune_locked(&mut state);
803 }
804
805 fn prune_locked(&self, state: &mut RegistryState) {
806 let now = Instant::now();
807 while let Some(query_id) = state.detailed_lru.front().copied() {
808 let Some(entry) = state.detailed.get(&query_id) else {
809 state.detailed_lru.pop_front();
810 continue;
811 };
812 let expired = now.saturating_duration_since(entry.finished_at) >= self.finished_ttl;
813 let over_limit = state.detailed.len() > self.max_detailed
814 || state.detailed_bytes > self.max_detailed_bytes;
815 if !expired && !over_limit {
816 break;
817 }
818 state.detailed_lru.pop_front();
819 if let Some(entry) = state.detailed.remove(&query_id) {
820 state.detailed_bytes = state.detailed_bytes.saturating_sub(entry.approximate_bytes);
821 if !expired {
822 state.demotions = state.demotions.saturating_add(1);
823 self.insert_compact_locked(
824 state,
825 CompactFinishedQuery::from_status(entry.status, entry.finished_at),
826 );
827 }
828 }
829 }
830 while let Some(query_id) = state.compact_lru.front().copied() {
831 let Some(entry) = state.compact.get(&query_id) else {
832 state.compact_lru.pop_front();
833 continue;
834 };
835 let expired = now.saturating_duration_since(entry.finished_at) >= self.finished_ttl;
836 let over_limit = state.compact.len() > self.max_compact
837 || state.compact_bytes > self.max_compact_bytes;
838 if !expired && !over_limit {
839 break;
840 }
841 state.compact_lru.pop_front();
842 if let Some(entry) = state.compact.remove(&query_id) {
843 state.compact_bytes = state.compact_bytes.saturating_sub(entry.approximate_bytes);
844 if !expired {
845 state.compact_evictions = state.compact_evictions.saturating_add(1);
846 }
847 }
848 }
849 }
850
851 fn insert_compact_locked(&self, state: &mut RegistryState, entry: CompactFinishedQuery) {
852 if self.max_compact == 0 || self.max_compact_bytes == 0 {
853 state.compact_evictions = state.compact_evictions.saturating_add(1);
854 return;
855 }
856 state.compact_bytes = state.compact_bytes.saturating_add(entry.approximate_bytes);
857 state.compact_lru.push_back(entry.query_id);
858 state.compact.insert(entry.query_id, entry);
859 }
860}
861
862impl RegisteredQuery {
863 fn request_cancel(&self, reason: CancellationReason) -> CancelOutcome {
864 loop {
865 let phase = self.phase();
866 match phase {
867 SqlQueryPhase::Queued
868 | SqlQueryPhase::Planning
869 | SqlQueryPhase::Executing
870 | SqlQueryPhase::Streaming
871 | SqlQueryPhase::Serializing => {
872 if self
873 .phase
874 .compare_exchange(
875 phase as u8,
876 SqlQueryPhase::Cancelling as u8,
877 Ordering::AcqRel,
878 Ordering::Acquire,
879 )
880 .is_ok()
881 {
882 self.control.cancel(reason);
883 *self.cancel_requested_at.lock() = Some(Instant::now());
884 let mut trace = self.trace.lock();
885 trace.transition(phase);
886 trace.cancel_requested_phase = Some(phase);
887 if trace.commit_fence_outcome == CommitFenceOutcome::NotReached {
888 trace.commit_fence_outcome = CommitFenceOutcome::CancelWon;
889 }
890 return CancelOutcome::Accepted;
891 }
892 }
893 SqlQueryPhase::Cancelling | SqlQueryPhase::Cancelled => {
894 return CancelOutcome::AlreadyCancelling;
895 }
896 SqlQueryPhase::CommitCritical => return CancelOutcome::TooLate,
897 SqlQueryPhase::Completed | SqlQueryPhase::Failed => {
898 return CancelOutcome::AlreadyFinished;
899 }
900 }
901 }
902 }
903}
904
905#[derive(Debug, Clone)]
906pub struct RegisteredSqlQuery {
907 registry: Weak<SqlQueryRegistry>,
908 query: Arc<RegisteredQuery>,
909}
910
911enum TerminalFailure {
912 Error {
913 code: String,
914 category: QueryTerminalErrorCategory,
915 },
916 Serialization(String),
917}
918
919pub(crate) struct SqlTaskContext {
922 query: RegisteredSqlQuery,
923 test_hook: Option<SqlTestHook>,
924}
925
926impl SqlTaskContext {
927 pub(crate) fn new(query: RegisteredSqlQuery, test_hook: Option<SqlTestHook>) -> Self {
928 Self { query, test_hook }
929 }
930
931 pub(crate) fn query(&self) -> &RegisteredSqlQuery {
932 &self.query
933 }
934
935 pub(crate) fn test_hook(&self) -> Option<&SqlTestHook> {
936 self.test_hook.as_ref()
937 }
938}
939
940impl RegisteredSqlQuery {
941 pub fn id(&self) -> QueryId {
942 self.query.id
943 }
944
945 pub fn control(&self) -> &ExecutionControl {
946 &self.query.control
947 }
948
949 pub fn phase(&self) -> SqlQueryPhase {
950 self.query.phase()
951 }
952
953 pub fn status(&self) -> QueryStatus {
954 self.query.status()
955 }
956
957 pub fn set_sql_metadata(&self, sql: &str) {
958 let operation = sql
959 .split_whitespace()
960 .next()
961 .unwrap_or("UNKNOWN")
962 .chars()
963 .take(64)
964 .collect::<String>()
965 .to_ascii_uppercase();
966 *self.query.operation.lock() = operation;
967 *self.query.sql_fingerprint.lock() = crate::normalized_sql_fingerprint(sql);
968 }
969
970 pub fn transition(&self, expected: SqlQueryPhase, next: SqlQueryPhase) -> Result<()> {
971 self.query
972 .phase
973 .compare_exchange(
974 expected as u8,
975 next as u8,
976 Ordering::AcqRel,
977 Ordering::Acquire,
978 )
979 .map(|_| self.query.record_transition(expected))
980 .map_err(|actual| {
981 let actual = SqlQueryPhase::from_u8(actual);
982 if actual == SqlQueryPhase::Cancelling {
983 self.cancellation_error()
984 } else {
985 MongrelQueryError::InvalidQueryState(format!(
986 "query {} expected {expected:?}, found {actual:?}",
987 self.id()
988 ))
989 }
990 })
991 }
992
993 pub fn enter_commit_critical(&self) -> Result<()> {
994 if let Err(error) = self.checkpoint() {
995 self.query.trace.lock().commit_fence_outcome = CommitFenceOutcome::CancelWon;
996 return Err(error);
997 }
998 self.transition(SqlQueryPhase::Executing, SqlQueryPhase::CommitCritical)?;
999 self.query.trace.lock().commit_fence_outcome = CommitFenceOutcome::CommitWon;
1000 Ok(())
1001 }
1002
1003 pub fn exit_commit_critical(&self) -> Result<()> {
1004 self.transition(SqlQueryPhase::CommitCritical, SqlQueryPhase::Executing)
1005 }
1006
1007 pub fn begin_serialization(&self) -> Result<()> {
1008 self.checkpoint()?;
1009 match self.phase() {
1010 SqlQueryPhase::Executing => {
1011 self.transition(SqlQueryPhase::Executing, SqlQueryPhase::Serializing)
1012 }
1013 SqlQueryPhase::Streaming => {
1014 self.transition(SqlQueryPhase::Streaming, SqlQueryPhase::Serializing)
1015 }
1016 phase => Err(MongrelQueryError::InvalidQueryState(format!(
1017 "query {} cannot serialize from {phase:?}",
1018 self.id()
1019 ))),
1020 }?;
1021 self.query.outcome.lock().serialization = SerializationOutcome::InProgress;
1022 Ok(())
1023 }
1024
1025 pub fn record_commit(&self, statement_index: usize, epoch: u64) {
1027 self.query
1028 .outcome
1029 .lock()
1030 .durable
1031 .record_commit(statement_index, Some(epoch));
1032 }
1033
1034 pub fn durable_outcome(&self) -> DurableOutcome {
1035 self.query.outcome.lock().durable.clone()
1036 }
1037
1038 pub fn commit_outcome_error(&self, message: impl Into<String>) -> MongrelQueryError {
1039 let durable = self.durable_outcome();
1040 MongrelQueryError::CommitOutcome {
1041 query_id: self.id(),
1042 committed: durable.committed,
1043 committed_statements: durable.committed_statements,
1044 last_commit_epoch: durable.last_commit_epoch,
1045 first_commit_statement_index: durable.first_commit_statement_index,
1046 last_commit_statement_index: durable.last_commit_statement_index,
1047 completed_statements: self.query.completed_statements.load(Ordering::Acquire),
1048 statement_index: self.query.statement_index.load(Ordering::Acquire),
1049 message: message.into(),
1050 }
1051 }
1052
1053 pub fn result_limit_error(&self, message: impl Into<String>) -> MongrelQueryError {
1054 let durable = self.durable_outcome();
1055 MongrelQueryError::ResultLimitExceeded {
1056 query_id: self.id(),
1057 committed: durable.committed,
1058 committed_statements: durable.committed_statements,
1059 last_commit_epoch: durable.last_commit_epoch,
1060 first_commit_statement_index: durable.first_commit_statement_index,
1061 last_commit_statement_index: durable.last_commit_statement_index,
1062 completed_statements: self.query.completed_statements.load(Ordering::Acquire),
1063 statement_index: self.query.statement_index.load(Ordering::Acquire),
1064 message: message.into(),
1065 }
1066 }
1067
1068 pub fn outcome_unknown_error(&self, message: impl Into<String>) -> MongrelQueryError {
1071 let message = message.into();
1072 self.mark_outcome_unknown();
1073 MongrelQueryError::OutcomeUnknown {
1074 query_id: self.id(),
1075 message,
1076 }
1077 }
1078
1079 #[allow(clippy::too_many_arguments)]
1082 pub fn restore_replayed_outcome(
1083 &self,
1084 durable: DurableOutcome,
1085 completed_statements: usize,
1086 statement_index: usize,
1087 serialization: SerializationOutcome,
1088 terminal_error: Option<QueryTerminalError>,
1089 terminal_state: QueryTerminalState,
1090 cancellation_reason: CancellationReason,
1091 phase: SqlQueryPhase,
1092 ) {
1093 let mut outcome = self.query.outcome.lock();
1094 outcome.durable = durable;
1095 outcome.serialization = serialization;
1096 outcome.terminal_error = terminal_error;
1097 outcome.terminal_state_override = Some(terminal_state);
1098 outcome.cancellation_reason_override = Some(cancellation_reason);
1099 outcome.phase_override = Some(phase);
1100 self.query
1101 .completed_statements
1102 .store(completed_statements, Ordering::Release);
1103 self.query
1104 .statement_index
1105 .store(statement_index, Ordering::Release);
1106 }
1107
1108 pub fn mark_outcome_unknown(&self) {
1111 let mut outcome = self.query.outcome.lock();
1112 outcome.terminal_state_override = Some(QueryTerminalState::OutcomeUnknown);
1113 outcome.terminal_error = Some(QueryTerminalError {
1114 code: "QUERY_OUTCOME_UNKNOWN".into(),
1115 category: QueryTerminalErrorCategory::Execution,
1116 });
1117 }
1118
1119 pub fn record_terminal_error(
1120 &self,
1121 code: impl Into<String>,
1122 category: QueryTerminalErrorCategory,
1123 ) {
1124 Self::set_terminal_error(&mut self.query.outcome.lock(), code.into(), category);
1125 }
1126
1127 pub fn record_serialization_failure(&self, code: impl Into<String>) {
1128 let mut outcome = self.query.outcome.lock();
1129 Self::set_serialization_failure(&mut outcome, code.into());
1130 }
1131
1132 fn set_terminal_error(
1133 outcome: &mut QueryOutcomeState,
1134 code: String,
1135 category: QueryTerminalErrorCategory,
1136 ) {
1137 outcome.terminal_error = Some(QueryTerminalError { code, category });
1138 }
1139
1140 fn set_serialization_failure(outcome: &mut QueryOutcomeState, code: String) {
1141 let code = if outcome.durable.committed && code.starts_with("SERIALIZATION_") {
1142 "SERIALIZATION_FAILED_AFTER_COMMIT".into()
1143 } else {
1144 code
1145 };
1146 outcome.serialization = SerializationOutcome::Failed;
1147 outcome.terminal_error = Some(QueryTerminalError {
1148 code,
1149 category: QueryTerminalErrorCategory::Serialization,
1150 });
1151 }
1152
1153 pub fn begin_statement(&self, index: usize) {
1154 self.query.statement_index.store(index, Ordering::Release);
1155 }
1156
1157 pub fn complete_statement(&self, index: usize) {
1158 self.query
1159 .completed_statements
1160 .store(index.saturating_add(1), Ordering::Release);
1161 }
1162
1163 pub fn complete_current_statement(&self) {
1164 let index = self.query.statement_index.load(Ordering::Acquire);
1165 self.complete_statement(index);
1166 }
1167
1168 pub fn request_cancel(&self, reason: CancellationReason) -> CancelOutcome {
1169 self.query.request_cancel(reason)
1170 }
1171
1172 pub fn checkpoint(&self) -> Result<()> {
1173 let durable = self.durable_outcome();
1174 let result = self
1175 .query
1176 .control
1177 .checkpoint()
1178 .map_err(|error| match error {
1179 mongreldb_core::MongrelError::DeadlineExceeded => {
1180 MongrelQueryError::DeadlineExceeded {
1181 query_id: self.id(),
1182 timeout_ms: self.query.deadline.map(|deadline| {
1183 deadline
1184 .saturating_duration_since(self.query.started_at)
1185 .as_millis()
1186 .min(u128::from(u64::MAX)) as u64
1187 }),
1188 committed: durable.committed,
1189 committed_statements: durable.committed_statements,
1190 last_commit_epoch: durable.last_commit_epoch,
1191 first_commit_statement_index: durable.first_commit_statement_index,
1192 last_commit_statement_index: durable.last_commit_statement_index,
1193 completed_statements: self
1194 .query
1195 .completed_statements
1196 .load(Ordering::Acquire),
1197 cancelled_statement_index: self
1198 .query
1199 .statement_index
1200 .load(Ordering::Acquire),
1201 }
1202 }
1203 mongreldb_core::MongrelError::Cancelled => self.cancellation_error(),
1204 other => MongrelQueryError::Core(other),
1205 });
1206 if result.is_err() && self.query.control.is_cancelled() {
1207 let mut trace = self.query.trace.lock();
1208 trace.cancel_observed_phase = trace.cancel_requested_phase.or(Some(self.phase()));
1209 }
1210 result
1211 }
1212
1213 pub fn complete(&self) -> Result<()> {
1214 self.try_complete()
1215 }
1216
1217 pub fn try_complete(&self) -> Result<()> {
1218 if self.phase() == SqlQueryPhase::Completed {
1219 self.finish(SqlQueryPhase::Completed, None);
1220 return Ok(());
1221 }
1222 if let Err(error) = self.checkpoint() {
1223 self.fail();
1224 return Err(error);
1225 }
1226 let phase = self.finish(SqlQueryPhase::Completed, None);
1227 match phase {
1228 SqlQueryPhase::Completed => Ok(()),
1229 SqlQueryPhase::Cancelled => Err(self.cancellation_error()),
1230 other => Err(MongrelQueryError::InvalidQueryState(format!(
1231 "query {} completed from terminal phase {other:?}",
1232 self.id()
1233 ))),
1234 }
1235 }
1236
1237 pub fn fail(&self) {
1238 self.finish(SqlQueryPhase::Failed, None);
1239 }
1240
1241 pub fn fail_result_limit(&self) {
1242 self.fail_with_error(
1243 "RESULT_LIMIT_EXCEEDED",
1244 QueryTerminalErrorCategory::ResultLimit,
1245 );
1246 }
1247
1248 pub fn fail_with_error(&self, code: impl Into<String>, category: QueryTerminalErrorCategory) {
1249 self.finish(
1250 SqlQueryPhase::Failed,
1251 Some(TerminalFailure::Error {
1252 code: code.into(),
1253 category,
1254 }),
1255 );
1256 }
1257
1258 pub fn fail_serialization(&self) {
1259 self.finish(
1260 SqlQueryPhase::Failed,
1261 Some(TerminalFailure::Serialization(
1262 "SERIALIZATION_FAILED".into(),
1263 )),
1264 );
1265 }
1266
1267 fn finish(
1268 &self,
1269 requested: SqlQueryPhase,
1270 mut failure: Option<TerminalFailure>,
1271 ) -> SqlQueryPhase {
1272 debug_assert!(matches!(
1273 requested,
1274 SqlQueryPhase::Completed | SqlQueryPhase::Failed
1275 ));
1276 let explicit_failure = failure.is_some();
1277 let mut outcome = self.query.outcome.lock();
1278 let phase = loop {
1279 let current = self.phase();
1280 if current.is_terminal() {
1281 break current;
1282 }
1283 let terminal = if current == SqlQueryPhase::Cancelling
1284 || (!explicit_failure && self.query.control.is_cancelled())
1285 {
1286 while !self.query.control.is_cancelled() {
1287 std::hint::spin_loop();
1288 }
1289 SqlQueryPhase::Cancelled
1290 } else {
1291 requested
1292 };
1293 if self
1294 .query
1295 .phase
1296 .compare_exchange(
1297 current as u8,
1298 terminal as u8,
1299 Ordering::AcqRel,
1300 Ordering::Acquire,
1301 )
1302 .is_ok()
1303 {
1304 self.query.record_transition(current);
1305 if terminal == SqlQueryPhase::Cancelled && outcome.phase_override.is_some() {
1306 outcome.phase_override = None;
1307 outcome.terminal_state_override = None;
1308 outcome.cancellation_reason_override = None;
1309 outcome.terminal_error = None;
1310 outcome.serialization = SerializationOutcome::Failed;
1311 }
1312 if terminal == SqlQueryPhase::Failed {
1313 match failure.take() {
1314 Some(TerminalFailure::Error { code, category }) => {
1315 Self::set_terminal_error(&mut outcome, code, category);
1316 }
1317 Some(TerminalFailure::Serialization(code)) => {
1318 Self::set_serialization_failure(&mut outcome, code);
1319 }
1320 None => {}
1321 }
1322 }
1323 Self::finalize_outcome(&mut outcome, terminal, self.query.control.reason());
1324 break terminal;
1325 }
1326 };
1327 Self::finalize_outcome(&mut outcome, phase, self.query.control.reason());
1328 drop(outcome);
1329 if let Some(registry) = self.registry.upgrade() {
1330 registry.finish(&self.query);
1331 }
1332 phase
1333 }
1334
1335 fn finalize_outcome(
1336 outcome: &mut QueryOutcomeState,
1337 phase: SqlQueryPhase,
1338 reason: CancellationReason,
1339 ) {
1340 match (phase, outcome.serialization) {
1341 (SqlQueryPhase::Completed, SerializationOutcome::InProgress) => {
1342 outcome.serialization = SerializationOutcome::Succeeded;
1343 }
1344 (
1345 SqlQueryPhase::Failed | SqlQueryPhase::Cancelled,
1346 SerializationOutcome::InProgress,
1347 ) => {
1348 outcome.serialization = SerializationOutcome::Failed;
1349 }
1350 _ => {}
1351 }
1352 if phase == SqlQueryPhase::Completed || outcome.terminal_error.is_some() {
1353 return;
1354 }
1355 let committed = outcome.durable.committed;
1356 let (code, category) = match reason {
1357 CancellationReason::Deadline => (
1358 if committed {
1359 "DEADLINE_AFTER_COMMIT"
1360 } else {
1361 "DEADLINE_EXCEEDED"
1362 },
1363 QueryTerminalErrorCategory::Deadline,
1364 ),
1365 CancellationReason::None if outcome.serialization == SerializationOutcome::Failed => (
1366 if committed {
1367 "SERIALIZATION_FAILED_AFTER_COMMIT"
1368 } else {
1369 "SERIALIZATION_FAILED"
1370 },
1371 QueryTerminalErrorCategory::Serialization,
1372 ),
1373 CancellationReason::None => ("QUERY_FAILED", QueryTerminalErrorCategory::Execution),
1374 _ => (
1375 if committed {
1376 "QUERY_CANCELLED_AFTER_COMMIT"
1377 } else {
1378 "QUERY_CANCELLED"
1379 },
1380 QueryTerminalErrorCategory::Cancellation,
1381 ),
1382 };
1383 outcome.terminal_error = Some(QueryTerminalError {
1384 code: code.into(),
1385 category,
1386 });
1387 }
1388
1389 fn cancellation_error(&self) -> MongrelQueryError {
1390 let durable = self.durable_outcome();
1391 let mut reason = self.query.control.reason();
1392 while reason == CancellationReason::None && self.query.phase() == SqlQueryPhase::Cancelling
1393 {
1394 std::thread::yield_now();
1395 reason = self.query.control.reason();
1396 }
1397 match reason {
1398 CancellationReason::Deadline => MongrelQueryError::DeadlineExceeded {
1399 query_id: self.id(),
1400 timeout_ms: self.query.deadline.map(|deadline| {
1401 deadline
1402 .saturating_duration_since(self.query.started_at)
1403 .as_millis()
1404 .min(u128::from(u64::MAX)) as u64
1405 }),
1406 committed: durable.committed,
1407 committed_statements: durable.committed_statements,
1408 last_commit_epoch: durable.last_commit_epoch,
1409 first_commit_statement_index: durable.first_commit_statement_index,
1410 last_commit_statement_index: durable.last_commit_statement_index,
1411 completed_statements: self.query.completed_statements.load(Ordering::Acquire),
1412 cancelled_statement_index: self.query.statement_index.load(Ordering::Acquire),
1413 },
1414 reason => MongrelQueryError::QueryCancelled {
1415 query_id: self.id(),
1416 reason,
1417 committed: durable.committed,
1418 committed_statements: durable.committed_statements,
1419 last_commit_epoch: durable.last_commit_epoch,
1420 first_commit_statement_index: durable.first_commit_statement_index,
1421 last_commit_statement_index: durable.last_commit_statement_index,
1422 completed_statements: self.query.completed_statements.load(Ordering::Acquire),
1423 cancelled_statement_index: self.query.statement_index.load(Ordering::Acquire),
1424 },
1425 }
1426 }
1427}
1428
1429pub struct RegisteredQueryGuard {
1430 query: Option<RegisteredSqlQuery>,
1431}
1432
1433impl RegisteredQueryGuard {
1434 pub fn new(query: RegisteredSqlQuery) -> Self {
1435 Self { query: Some(query) }
1436 }
1437
1438 pub fn query(&self) -> &RegisteredSqlQuery {
1439 self.query
1440 .as_ref()
1441 .expect("registered query guard consumed")
1442 }
1443
1444 pub fn complete(self) -> Result<()> {
1445 self.try_complete()
1446 }
1447
1448 pub fn try_complete(mut self) -> Result<()> {
1449 if let Some(query) = self.query.take() {
1450 query.try_complete()
1451 } else {
1452 Ok(())
1453 }
1454 }
1455
1456 pub fn fail(mut self) {
1457 if let Some(query) = self.query.take() {
1458 query.fail();
1459 }
1460 }
1461
1462 pub fn fail_result_limit(mut self) {
1463 if let Some(query) = self.query.take() {
1464 query.fail_result_limit();
1465 }
1466 }
1467
1468 pub fn fail_with_error(
1469 mut self,
1470 code: impl Into<String>,
1471 category: QueryTerminalErrorCategory,
1472 ) {
1473 if let Some(query) = self.query.take() {
1474 query.fail_with_error(code, category);
1475 }
1476 }
1477
1478 pub fn fail_serialization(mut self) {
1479 if let Some(query) = self.query.take() {
1480 query.fail_serialization();
1481 }
1482 }
1483
1484 pub fn into_query(mut self) -> RegisteredSqlQuery {
1485 self.query.take().expect("registered query guard consumed")
1486 }
1487}
1488
1489impl Drop for RegisteredQueryGuard {
1490 fn drop(&mut self) {
1491 if let Some(query) = self.query.take() {
1492 query.fail();
1493 }
1494 }
1495}
1496
1497fn validate_metadata(name: &str, value: Option<&str>) -> Result<()> {
1498 if value.is_some_and(|value| value.len() > MAX_METADATA_BYTES) {
1499 return Err(MongrelQueryError::Core(
1500 mongreldb_core::MongrelError::InvalidArgument(format!(
1501 "{name} exceeds {MAX_METADATA_BYTES} bytes"
1502 )),
1503 ));
1504 }
1505 Ok(())
1506}
1507
1508#[cfg(test)]
1509mod tests {
1510 use super::*;
1511
1512 #[test]
1513 fn query_ids_are_random_strict_and_round_trip() {
1514 let first = QueryId::random().unwrap();
1515 let second = QueryId::random().unwrap();
1516 assert_ne!(first, second);
1517 assert_eq!(first.to_string().parse::<QueryId>().unwrap(), first);
1518 assert!("abc".parse::<QueryId>().is_err());
1519 assert!("0000000000000000000000000000000z"
1520 .parse::<QueryId>()
1521 .is_err());
1522 }
1523
1524 #[test]
1525 fn unrepresentable_timeout_is_rejected_without_panicking() {
1526 let registry = Arc::new(SqlQueryRegistry::default());
1527 assert!(matches!(
1528 registry.register(SqlQueryOptions {
1529 timeout: Some(Duration::MAX),
1530 ..SqlQueryOptions::default()
1531 }),
1532 Err(MongrelQueryError::Core(
1533 mongreldb_core::MongrelError::InvalidArgument(_)
1534 ))
1535 ));
1536 assert_eq!(registry.active_count(), 0);
1537 }
1538
1539 #[test]
1540 fn active_and_retained_query_ids_are_rejected() {
1541 let registry = Arc::new(SqlQueryRegistry::new(1, 1, 1024, Duration::from_secs(60)));
1542 let id = QueryId::random().unwrap();
1543 let query = registry
1544 .register(SqlQueryOptions {
1545 query_id: Some(id),
1546 ..SqlQueryOptions::default()
1547 })
1548 .unwrap();
1549 assert!(matches!(
1550 registry.register(SqlQueryOptions {
1551 query_id: Some(id),
1552 ..SqlQueryOptions::default()
1553 }),
1554 Err(MongrelQueryError::QueryIdConflict { .. })
1555 ));
1556 query.complete().unwrap();
1557 assert_eq!(registry.active_count(), 0);
1558 assert_eq!(registry.finished_count(), 1);
1559 assert!(matches!(
1560 registry.register(SqlQueryOptions {
1561 query_id: Some(id),
1562 ..SqlQueryOptions::default()
1563 }),
1564 Err(MongrelQueryError::QueryIdConflict { .. })
1565 ));
1566 assert_eq!(registry.cancel(id), CancelOutcome::AlreadyFinished);
1567 }
1568
1569 #[test]
1570 fn query_id_can_be_reused_after_its_tombstone_expires() {
1571 let registry = Arc::new(SqlQueryRegistry::new(1, 1, 1024, Duration::ZERO));
1572 let id = QueryId::random().unwrap();
1573 registry
1574 .register(SqlQueryOptions {
1575 query_id: Some(id),
1576 ..SqlQueryOptions::default()
1577 })
1578 .unwrap()
1579 .complete()
1580 .unwrap();
1581
1582 let replacement = registry
1583 .register(SqlQueryOptions {
1584 query_id: Some(id),
1585 ..SqlQueryOptions::default()
1586 })
1587 .unwrap();
1588 assert_eq!(registry.status(id).unwrap().phase, SqlQueryPhase::Queued);
1589 replacement.complete().unwrap();
1590 }
1591
1592 #[test]
1593 fn status_capacity_eviction_does_not_release_query_id() {
1594 for (max_finished, max_finished_bytes) in [(1, usize::MAX), (10, 1)] {
1595 let owner = "o".repeat(MAX_METADATA_BYTES);
1596 let session_id = "s".repeat(MAX_METADATA_BYTES);
1597 let registry = Arc::new(SqlQueryRegistry::new(
1598 1,
1599 max_finished,
1600 max_finished_bytes,
1601 Duration::from_secs(60),
1602 ));
1603 let first_id = QueryId::random().unwrap();
1604 registry
1605 .register(SqlQueryOptions {
1606 query_id: Some(first_id),
1607 owner: Some(owner.clone()),
1608 session_id: Some(session_id.clone()),
1609 ..SqlQueryOptions::default()
1610 })
1611 .unwrap()
1612 .complete()
1613 .unwrap();
1614 registry
1615 .register(SqlQueryOptions::default())
1616 .unwrap()
1617 .complete()
1618 .unwrap();
1619
1620 assert!(registry.status(first_id).is_none());
1621 let compact = registry.compact_finished_status(first_id).unwrap();
1622 assert_eq!(compact.owner, Some(owner));
1623 assert_eq!(compact.session_id, Some(session_id));
1624 assert_eq!(registry.cancel(first_id), CancelOutcome::AlreadyFinished);
1625 assert!(matches!(
1626 registry.register(SqlQueryOptions {
1627 query_id: Some(first_id),
1628 ..SqlQueryOptions::default()
1629 }),
1630 Err(MongrelQueryError::QueryIdConflict { .. })
1631 ));
1632 }
1633 }
1634
1635 #[test]
1636 fn compact_overflow_evicts_instead_of_rejecting_new_work() {
1637 let registry = Arc::new(SqlQueryRegistry::new_with_limits(
1638 1,
1639 0,
1640 0,
1641 1,
1642 usize::MAX,
1643 Duration::from_secs(60),
1644 ));
1645 let first_id = QueryId::random().unwrap();
1646 registry
1647 .register(SqlQueryOptions {
1648 query_id: Some(first_id),
1649 ..SqlQueryOptions::default()
1650 })
1651 .unwrap()
1652 .complete()
1653 .unwrap();
1654 registry
1655 .register(SqlQueryOptions::default())
1656 .unwrap()
1657 .complete()
1658 .unwrap();
1659
1660 assert_eq!(registry.finished_count(), 1);
1661 assert_eq!(registry.cancel(first_id), CancelOutcome::NotFound);
1662 let replacement = registry
1663 .register(SqlQueryOptions {
1664 query_id: Some(first_id),
1665 ..SqlQueryOptions::default()
1666 })
1667 .unwrap();
1668 replacement.complete().unwrap();
1669 assert_eq!(registry.stats().compact_evictions, 2);
1670 }
1671
1672 #[test]
1673 fn compact_identity_accounting_covers_maximum_metadata() {
1674 let registry = Arc::new(SqlQueryRegistry::new(1, 0, 0, Duration::from_secs(60)));
1675 let owner = "o".repeat(MAX_METADATA_BYTES);
1676 let session_id = "s".repeat(MAX_METADATA_BYTES);
1677 let query_id = QueryId::random().unwrap();
1678 registry
1679 .register(SqlQueryOptions {
1680 query_id: Some(query_id),
1681 owner: Some(owner.clone()),
1682 session_id: Some(session_id.clone()),
1683 ..SqlQueryOptions::default()
1684 })
1685 .unwrap()
1686 .complete()
1687 .unwrap();
1688
1689 let compact = registry.compact_finished_status(query_id).unwrap();
1690 assert_eq!(compact.owner, Some(owner));
1691 assert_eq!(compact.session_id, Some(session_id));
1692 assert_eq!(registry.approximate_bytes(), registry.stats().compact_bytes);
1693 }
1694
1695 #[test]
1696 fn ten_thousand_completed_queries_never_consume_active_capacity() {
1697 let registry = Arc::new(SqlQueryRegistry::new_with_limits(
1698 1,
1699 2,
1700 4096,
1701 10_000,
1702 8 * 1024 * 1024,
1703 Duration::from_secs(60),
1704 ));
1705 for _ in 0..10_000 {
1706 registry
1707 .register(SqlQueryOptions::default())
1708 .unwrap()
1709 .complete()
1710 .unwrap();
1711 }
1712 let stats = registry.stats();
1713 assert_eq!(stats.active, 0);
1714 assert_eq!(stats.detailed, 2);
1715 assert_eq!(stats.compact, 9_998);
1716 assert_eq!(stats.active_rejections, 0);
1717 assert!(stats.detailed_bytes <= 4096);
1718 assert!(stats.compact_bytes <= 8 * 1024 * 1024);
1719 }
1720
1721 #[test]
1722 #[ignore = "release qualification characterization; set MONGRELDB_REGISTRY_CHARACTERIZATION_SECONDS"]
1723 fn registry_high_qps_characterization() {
1724 let seconds = std::env::var("MONGRELDB_REGISTRY_CHARACTERIZATION_SECONDS")
1725 .ok()
1726 .and_then(|value| value.parse::<u64>().ok())
1727 .unwrap_or(300);
1728 for rate in [100_u64, 500, 1_000] {
1729 let registry = Arc::new(SqlQueryRegistry::new_with_limits(
1730 1_024,
1731 2_048,
1732 8 * 1024 * 1024,
1733 100_000,
1734 32 * 1024 * 1024,
1735 Duration::from_secs(60),
1736 ));
1737 let started = Instant::now();
1738 for second in 0..seconds {
1739 let deadline = started + Duration::from_secs(second + 1);
1740 for _ in 0..rate {
1741 registry
1742 .register(SqlQueryOptions::default())
1743 .unwrap()
1744 .complete()
1745 .unwrap();
1746 }
1747 std::thread::sleep(deadline.saturating_duration_since(Instant::now()));
1748 }
1749 let stats = registry.stats();
1750 assert_eq!(stats.active, 0);
1751 assert_eq!(stats.active_rejections, 0);
1752 assert!(stats.detailed <= 2_048);
1753 assert!(stats.detailed_bytes <= 8 * 1024 * 1024);
1754 assert!(stats.compact <= 100_000);
1755 assert!(stats.compact_bytes <= 32 * 1024 * 1024);
1756 eprintln!(
1757 "registry characterization: rate={rate} qps seconds={seconds} operations={} detailed={} compact={} detailed_bytes={} compact_bytes={} demotions={} compact_evictions={} active_rejections={}",
1758 rate * seconds,
1759 stats.detailed,
1760 stats.compact,
1761 stats.detailed_bytes,
1762 stats.compact_bytes,
1763 stats.demotions,
1764 stats.compact_evictions,
1765 stats.active_rejections,
1766 );
1767 }
1768 }
1769
1770 #[test]
1771 fn cancel_and_commit_fence_have_one_winner() {
1772 for cancel_first in [true, false] {
1773 let registry = Arc::new(SqlQueryRegistry::default());
1774 let query = registry.register(SqlQueryOptions::default()).unwrap();
1775 query
1776 .transition(SqlQueryPhase::Queued, SqlQueryPhase::Executing)
1777 .unwrap();
1778 if cancel_first {
1779 assert_eq!(
1780 query.request_cancel(CancellationReason::ClientRequest),
1781 CancelOutcome::Accepted
1782 );
1783 assert!(query.enter_commit_critical().is_err());
1784 let status = query.status();
1785 assert_eq!(
1786 status.cancel_requested_phase,
1787 Some(SqlQueryPhase::Executing)
1788 );
1789 assert_eq!(status.cancel_observed_phase, Some(SqlQueryPhase::Executing));
1790 assert_eq!(status.commit_fence_outcome, CommitFenceOutcome::CancelWon);
1791 } else {
1792 query.enter_commit_critical().unwrap();
1793 assert_eq!(
1794 query.request_cancel(CancellationReason::ClientRequest),
1795 CancelOutcome::TooLate
1796 );
1797 query.record_commit(0, 7);
1798 query.complete().unwrap();
1799 let status = registry.status(query.id()).unwrap();
1800 assert!(status.committed);
1801 assert_eq!(status.commit_fence_outcome, CommitFenceOutcome::CommitWon);
1802 }
1803 }
1804 }
1805
1806 #[test]
1807 fn cancel_and_terminal_completion_have_one_winner() {
1808 for _ in 0..100 {
1809 let registry = Arc::new(SqlQueryRegistry::default());
1810 let query = registry.register(SqlQueryOptions::default()).unwrap();
1811 query
1812 .transition(SqlQueryPhase::Queued, SqlQueryPhase::Executing)
1813 .unwrap();
1814 query.begin_serialization().unwrap();
1815
1816 let barrier = Arc::new(std::sync::Barrier::new(3));
1817 let cancel_query = query.clone();
1818 let cancel_barrier = Arc::clone(&barrier);
1819 let cancel = std::thread::spawn(move || {
1820 cancel_barrier.wait();
1821 cancel_query.request_cancel(CancellationReason::ClientRequest)
1822 });
1823 let complete_query = query.clone();
1824 let complete_barrier = Arc::clone(&barrier);
1825 let complete = std::thread::spawn(move || {
1826 complete_barrier.wait();
1827 complete_query.try_complete()
1828 });
1829 barrier.wait();
1830
1831 let cancel = cancel.join().unwrap();
1832 let complete = complete.join().unwrap();
1833 let status = registry.status(query.id()).unwrap();
1834 match cancel {
1835 CancelOutcome::Accepted => {
1836 assert!(matches!(
1837 complete,
1838 Err(MongrelQueryError::QueryCancelled { .. })
1839 ));
1840 assert_eq!(status.phase, SqlQueryPhase::Cancelled);
1841 }
1842 CancelOutcome::AlreadyFinished => {
1843 complete.unwrap();
1844 assert_eq!(status.phase, SqlQueryPhase::Completed);
1845 }
1846 other => panic!("unexpected cancel outcome {other:?}"),
1847 }
1848 }
1849 }
1850
1851 #[test]
1852 fn externally_claimed_completion_is_finalized_and_retained() {
1853 let registry = Arc::new(SqlQueryRegistry::default());
1854 let query = registry.register(SqlQueryOptions::default()).unwrap();
1855 query
1856 .transition(SqlQueryPhase::Queued, SqlQueryPhase::Executing)
1857 .unwrap();
1858 query.begin_serialization().unwrap();
1859 query
1860 .transition(SqlQueryPhase::Serializing, SqlQueryPhase::Completed)
1861 .unwrap();
1862
1863 query.try_complete().unwrap();
1864
1865 assert_eq!(registry.active_count(), 0);
1866 let status = registry.status(query.id()).unwrap();
1867 assert_eq!(status.phase, SqlQueryPhase::Completed);
1868 assert_eq!(
1869 status.serialization_outcome,
1870 SerializationOutcome::Succeeded
1871 );
1872 }
1873
1874 #[test]
1875 fn zero_batch_completion_checks_expired_deadline() {
1876 let registry = Arc::new(SqlQueryRegistry::default());
1877 let query = registry
1878 .register(SqlQueryOptions {
1879 timeout: Some(Duration::from_millis(1)),
1880 ..SqlQueryOptions::default()
1881 })
1882 .unwrap();
1883 query
1884 .transition(SqlQueryPhase::Queued, SqlQueryPhase::Executing)
1885 .unwrap();
1886 query.begin_serialization().unwrap();
1887 std::thread::sleep(Duration::from_millis(5));
1888
1889 assert!(matches!(
1890 query.try_complete(),
1891 Err(MongrelQueryError::DeadlineExceeded { .. })
1892 ));
1893 let status = registry.status(query.id()).unwrap();
1894 assert_eq!(status.phase, SqlQueryPhase::Cancelled);
1895 assert_eq!(
1896 status.terminal_state(),
1897 Some(QueryTerminalState::DeadlineBeforeCommit)
1898 );
1899 }
1900
1901 #[test]
1902 fn parent_cancellation_wins_terminal_completion() {
1903 let registry = Arc::new(SqlQueryRegistry::default());
1904 let parent = ExecutionControl::new(None);
1905 let query = registry
1906 .register(SqlQueryOptions {
1907 parent_control: Some(parent.clone()),
1908 ..SqlQueryOptions::default()
1909 })
1910 .unwrap();
1911 query
1912 .transition(SqlQueryPhase::Queued, SqlQueryPhase::Executing)
1913 .unwrap();
1914 query.begin_serialization().unwrap();
1915 parent.cancel(CancellationReason::SessionClosed);
1916
1917 assert!(matches!(
1918 query.try_complete(),
1919 Err(MongrelQueryError::QueryCancelled {
1920 reason: CancellationReason::SessionClosed,
1921 ..
1922 })
1923 ));
1924 assert_eq!(
1925 registry.status(query.id()).unwrap().phase,
1926 SqlQueryPhase::Cancelled
1927 );
1928 }
1929
1930 #[test]
1931 fn explicit_serialization_failure_beats_passive_parent_cancel() {
1932 let registry = Arc::new(SqlQueryRegistry::default());
1933 let parent = ExecutionControl::new(None);
1934 let query = registry
1935 .register(SqlQueryOptions {
1936 parent_control: Some(parent.clone()),
1937 ..SqlQueryOptions::default()
1938 })
1939 .unwrap();
1940 query
1941 .transition(SqlQueryPhase::Queued, SqlQueryPhase::Executing)
1942 .unwrap();
1943 query.begin_serialization().unwrap();
1944 parent.cancel(CancellationReason::SessionClosed);
1945 query.fail_serialization();
1946
1947 let status = registry.status(query.id()).unwrap();
1948 assert_eq!(status.phase, SqlQueryPhase::Failed);
1949 assert_eq!(status.terminal_error.unwrap().code, "SERIALIZATION_FAILED");
1950 }
1951
1952 #[test]
1953 fn serialization_failure_after_commit_keeps_exact_durable_outcome() {
1954 let registry = Arc::new(SqlQueryRegistry::default());
1955 let query = registry.register(SqlQueryOptions::default()).unwrap();
1956 query
1957 .transition(SqlQueryPhase::Queued, SqlQueryPhase::Executing)
1958 .unwrap();
1959 query.begin_statement(2);
1960 query.enter_commit_critical().unwrap();
1961 query.record_commit(2, 73);
1962 query.exit_commit_critical().unwrap();
1963 query.begin_serialization().unwrap();
1964 query.fail_serialization();
1965
1966 let status = registry.status(query.id()).unwrap();
1967 assert_eq!(status.phase, SqlQueryPhase::Failed);
1968 assert_eq!(status.durable_outcome.last_commit_epoch, Some(73));
1969 assert_eq!(status.durable_outcome.committed_statements, 1);
1970 assert_eq!(
1971 status.terminal_error.unwrap().code,
1972 "SERIALIZATION_FAILED_AFTER_COMMIT"
1973 );
1974 assert!(matches!(
1975 query.commit_outcome_error("encode failed"),
1976 MongrelQueryError::CommitOutcome {
1977 committed: true,
1978 committed_statements: 1,
1979 last_commit_epoch: Some(73),
1980 first_commit_statement_index: Some(2),
1981 last_commit_statement_index: Some(2),
1982 statement_index: 2,
1983 ..
1984 }
1985 ));
1986 }
1987
1988 #[test]
1989 fn guard_cleans_up_dropped_execution_without_raw_sql() {
1990 let registry = Arc::new(SqlQueryRegistry::default());
1991 let query = registry
1992 .register(SqlQueryOptions {
1993 owner: Some("alice".into()),
1994 session_id: Some("session".into()),
1995 ..SqlQueryOptions::default()
1996 })
1997 .unwrap();
1998 query.set_sql_metadata("SELECT secret FROM docs WHERE token = 'private'");
1999 let id = query.id();
2000 drop(RegisteredQueryGuard::new(query));
2001 let status = registry.status(id).unwrap();
2002 assert_eq!(status.phase, SqlQueryPhase::Failed);
2003 assert_eq!(status.operation, "SELECT");
2004 assert_ne!(status.sql_fingerprint, [0; 32]);
2005 }
2006
2007 #[test]
2008 fn sql_operation_metadata_is_bounded() {
2009 let registry = Arc::new(SqlQueryRegistry::default());
2010 let query = registry.register(SqlQueryOptions::default()).unwrap();
2011 let id = query.id();
2012 query.set_sql_metadata(&format!("{} value", "x".repeat(4096)));
2013 drop(RegisteredQueryGuard::new(query));
2014 assert_eq!(registry.status(id).unwrap().operation.len(), 64);
2015 }
2016
2017 #[test]
2018 fn tombstone_keeps_durable_and_terminal_outcomes() {
2019 let registry = Arc::new(SqlQueryRegistry::default());
2020 let query = registry.register(SqlQueryOptions::default()).unwrap();
2021 let id = query.id();
2022 query
2023 .transition(SqlQueryPhase::Queued, SqlQueryPhase::Executing)
2024 .unwrap();
2025 query.begin_statement(0);
2026 query.enter_commit_critical().unwrap();
2027 query.record_commit(0, 41);
2028 query.record_commit(0, 41);
2029 query.exit_commit_critical().unwrap();
2030 query.complete_statement(0);
2031 query.begin_statement(1);
2032 query.begin_serialization().unwrap();
2033 let active_outcome = query.status().durable_outcome;
2034
2035 assert_eq!(
2036 query.request_cancel(CancellationReason::ClientRequest),
2037 CancelOutcome::Accepted
2038 );
2039 query.fail();
2040
2041 let status = registry.status(id).unwrap();
2042 assert_eq!(status.durable_outcome, active_outcome);
2043 assert_eq!(
2044 status.durable_outcome,
2045 DurableOutcome {
2046 committed: true,
2047 committed_statements: 1,
2048 last_commit_epoch: Some(41),
2049 first_commit_statement_index: Some(0),
2050 last_commit_statement_index: Some(0),
2051 }
2052 );
2053 assert_eq!(status.serialization_outcome, SerializationOutcome::Failed);
2054 assert_eq!(status.commit_fence_outcome, CommitFenceOutcome::CommitWon);
2055 assert_eq!(
2056 status.terminal_error,
2057 Some(QueryTerminalError {
2058 code: "QUERY_CANCELLED_AFTER_COMMIT".into(),
2059 category: QueryTerminalErrorCategory::Cancellation,
2060 })
2061 );
2062 assert_eq!(
2063 status.terminal_state(),
2064 Some(QueryTerminalState::CancelledAfterCommit)
2065 );
2066 }
2067
2068 #[test]
2069 fn cancellation_error_carries_durable_receipt() {
2070 let registry = Arc::new(SqlQueryRegistry::default());
2071 let query = registry.register(SqlQueryOptions::default()).unwrap();
2072 query
2073 .transition(SqlQueryPhase::Queued, SqlQueryPhase::Executing)
2074 .unwrap();
2075 query.begin_statement(0);
2076 query.enter_commit_critical().unwrap();
2077 query.record_commit(0, 73);
2078 query.exit_commit_critical().unwrap();
2079 query.complete_statement(0);
2080 query.begin_statement(1);
2081 query.begin_serialization().unwrap();
2082 assert_eq!(
2083 query.request_cancel(CancellationReason::ClientRequest),
2084 CancelOutcome::Accepted
2085 );
2086
2087 assert!(matches!(
2088 query.checkpoint(),
2089 Err(MongrelQueryError::QueryCancelled {
2090 committed: true,
2091 committed_statements: 1,
2092 last_commit_epoch: Some(73),
2093 first_commit_statement_index: Some(0),
2094 last_commit_statement_index: Some(0),
2095 completed_statements: 1,
2096 cancelled_statement_index: 1,
2097 ..
2098 })
2099 ));
2100 query.fail();
2101 }
2102
2103 #[test]
2104 fn explicit_terminal_error_category_survives_tombstone() {
2105 let registry = Arc::new(SqlQueryRegistry::default());
2106 let query = registry.register(SqlQueryOptions::default()).unwrap();
2107 let id = query.id();
2108 query.record_terminal_error(
2109 "RESULT_LIMIT_EXCEEDED",
2110 QueryTerminalErrorCategory::ResultLimit,
2111 );
2112 query.fail();
2113
2114 let status = registry.status(id).unwrap();
2115 assert_eq!(
2116 status.terminal_error,
2117 Some(QueryTerminalError {
2118 code: "RESULT_LIMIT_EXCEEDED".into(),
2119 category: QueryTerminalErrorCategory::ResultLimit,
2120 })
2121 );
2122 assert_eq!(
2123 status.terminal_state(),
2124 Some(QueryTerminalState::FailedBeforeCommit)
2125 );
2126 }
2127
2128 #[test]
2129 fn serialization_failure_code_records_commit_outcome() {
2130 let registry = Arc::new(SqlQueryRegistry::default());
2131 let query = registry.register(SqlQueryOptions::default()).unwrap();
2132 let id = query.id();
2133 query.record_commit(0, 7);
2134 query.record_serialization_failure("SERIALIZATION_FAILED");
2135 query.fail();
2136
2137 let status = registry.status(id).unwrap();
2138 assert_eq!(
2139 status.terminal_error.as_ref().unwrap().code,
2140 "SERIALIZATION_FAILED_AFTER_COMMIT"
2141 );
2142 assert_eq!(
2143 status.terminal_state(),
2144 Some(QueryTerminalState::CommittedWithError)
2145 );
2146 }
2147}