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