1use crate::{MongrelQueryError, Result, SqlTestHook};
2use mongreldb_core::{CancellationReason, ExecutionControl};
3use sha2::{Digest, Sha256};
4use std::collections::{HashMap, VecDeque};
5use std::fmt;
6use std::str::FromStr;
7use std::sync::atomic::{AtomicBool, AtomicU8, AtomicUsize, Ordering};
8use std::sync::{Arc, Mutex, 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 = 2 * 1024 * 1024;
14const DEFAULT_FINISHED_TTL: Duration = Duration::from_secs(60);
15const MAX_METADATA_BYTES: usize = 256;
16
17#[derive(Clone, Copy, PartialEq, Eq, Hash)]
18pub struct QueryId([u8; 16]);
19
20impl QueryId {
21 pub fn random() -> Result<Self> {
22 let mut bytes = [0; 16];
23 getrandom::getrandom(&mut bytes).map_err(|error| {
24 MongrelQueryError::Core(mongreldb_core::MongrelError::Other(format!(
25 "query id randomness failed: {error}"
26 )))
27 })?;
28 Ok(Self(bytes))
29 }
30
31 pub fn as_bytes(&self) -> &[u8; 16] {
32 &self.0
33 }
34}
35
36impl fmt::Display for QueryId {
37 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
38 for byte in self.0 {
39 write!(formatter, "{byte:02x}")?;
40 }
41 Ok(())
42 }
43}
44
45impl fmt::Debug for QueryId {
46 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
47 fmt::Display::fmt(self, formatter)
48 }
49}
50
51impl FromStr for QueryId {
52 type Err = MongrelQueryError;
53
54 fn from_str(value: &str) -> Result<Self> {
55 if value.len() != 32 || !value.bytes().all(|byte| byte.is_ascii_hexdigit()) {
56 return Err(MongrelQueryError::Core(
57 mongreldb_core::MongrelError::InvalidArgument(
58 "query id must be exactly 32 hexadecimal characters".into(),
59 ),
60 ));
61 }
62 let mut bytes = [0; 16];
63 for (index, chunk) in value.as_bytes().chunks_exact(2).enumerate() {
64 let text = std::str::from_utf8(chunk).map_err(|_| {
65 MongrelQueryError::Core(mongreldb_core::MongrelError::InvalidArgument(
66 "query id is not valid UTF-8".into(),
67 ))
68 })?;
69 bytes[index] = u8::from_str_radix(text, 16).map_err(|_| {
70 MongrelQueryError::Core(mongreldb_core::MongrelError::InvalidArgument(
71 "query id contains invalid hexadecimal".into(),
72 ))
73 })?;
74 }
75 Ok(Self(bytes))
76 }
77}
78
79impl serde::Serialize for QueryId {
80 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
81 where
82 S: serde::Serializer,
83 {
84 serializer.serialize_str(&self.to_string())
85 }
86}
87
88impl<'de> serde::Deserialize<'de> for QueryId {
89 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
90 where
91 D: serde::Deserializer<'de>,
92 {
93 let value = <String as serde::Deserialize>::deserialize(deserializer)?;
94 value.parse().map_err(serde::de::Error::custom)
95 }
96}
97
98#[derive(Debug, Clone, Default)]
99pub struct SqlQueryOptions {
100 pub query_id: Option<QueryId>,
101 pub timeout: Option<Duration>,
102 pub owner: Option<String>,
103 pub session_id: Option<String>,
104 pub parent_control: Option<ExecutionControl>,
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108#[repr(u8)]
109pub enum SqlQueryPhase {
110 Queued = 0,
111 Planning = 1,
112 Executing = 2,
113 Streaming = 3,
114 Serializing = 4,
115 CommitCritical = 5,
116 Cancelling = 6,
117 Completed = 7,
118 Failed = 8,
119 Cancelled = 9,
120}
121
122impl SqlQueryPhase {
123 fn from_u8(value: u8) -> Self {
124 match value {
125 0 => Self::Queued,
126 1 => Self::Planning,
127 2 => Self::Executing,
128 3 => Self::Streaming,
129 4 => Self::Serializing,
130 5 => Self::CommitCritical,
131 6 => Self::Cancelling,
132 7 => Self::Completed,
133 8 => Self::Failed,
134 9 => Self::Cancelled,
135 _ => Self::Failed,
136 }
137 }
138
139 fn is_terminal(self) -> bool {
140 matches!(self, Self::Completed | Self::Failed | Self::Cancelled)
141 }
142}
143
144#[derive(Debug, Clone, Copy, PartialEq, Eq)]
145pub enum CancelOutcome {
146 Accepted,
147 AlreadyCancelling,
148 TooLate,
149 AlreadyFinished,
150 NotFound,
151}
152
153#[derive(Debug, Clone)]
154pub struct QueryStatus {
155 pub query_id: QueryId,
156 pub owner: Option<String>,
157 pub session_id: Option<String>,
158 pub phase: SqlQueryPhase,
159 pub started_at: Instant,
160 pub deadline: Option<Instant>,
161 pub operation: String,
162 pub sql_fingerprint: [u8; 32],
163 pub cancellation_reason: CancellationReason,
164 pub committed: bool,
165 pub completed_statements: usize,
166 pub statement_index: usize,
167 pub cancel_requested_at: Option<Instant>,
168 pub queue_duration: Duration,
169 pub planning_duration: Duration,
170 pub execution_duration: Duration,
171 pub serialization_duration: Duration,
172 pub cancel_requested_phase: Option<SqlQueryPhase>,
173 pub cancel_observed_phase: Option<SqlQueryPhase>,
174 pub commit_fence_outcome: CommitFenceOutcome,
175}
176
177#[derive(Debug, Clone, Copy, PartialEq, Eq)]
178pub enum CommitFenceOutcome {
179 NotReached,
180 CancelWon,
181 CommitWon,
182}
183
184#[derive(Debug, Clone)]
185struct QueryTrace {
186 phase_started_at: Instant,
187 queue_duration: Duration,
188 planning_duration: Duration,
189 execution_duration: Duration,
190 serialization_duration: Duration,
191 cancel_requested_phase: Option<SqlQueryPhase>,
192 cancel_observed_phase: Option<SqlQueryPhase>,
193 commit_fence_outcome: CommitFenceOutcome,
194}
195
196impl QueryTrace {
197 fn new(started_at: Instant) -> Self {
198 Self {
199 phase_started_at: started_at,
200 queue_duration: Duration::ZERO,
201 planning_duration: Duration::ZERO,
202 execution_duration: Duration::ZERO,
203 serialization_duration: Duration::ZERO,
204 cancel_requested_phase: None,
205 cancel_observed_phase: None,
206 commit_fence_outcome: CommitFenceOutcome::NotReached,
207 }
208 }
209
210 fn transition(&mut self, phase: SqlQueryPhase) {
211 let now = Instant::now();
212 let elapsed = now.saturating_duration_since(self.phase_started_at);
213 match phase {
214 SqlQueryPhase::Queued => self.queue_duration += elapsed,
215 SqlQueryPhase::Planning => self.planning_duration += elapsed,
216 SqlQueryPhase::Executing | SqlQueryPhase::Streaming | SqlQueryPhase::CommitCritical => {
217 self.execution_duration += elapsed
218 }
219 SqlQueryPhase::Serializing => self.serialization_duration += elapsed,
220 SqlQueryPhase::Cancelling
221 | SqlQueryPhase::Completed
222 | SqlQueryPhase::Failed
223 | SqlQueryPhase::Cancelled => {}
224 }
225 self.phase_started_at = now;
226 }
227}
228
229#[derive(Debug)]
230struct RegisteredQuery {
231 id: QueryId,
232 owner: Option<String>,
233 session_id: Option<String>,
234 control: ExecutionControl,
235 phase: AtomicU8,
236 started_at: Instant,
237 deadline: Option<Instant>,
238 operation: Mutex<String>,
239 sql_fingerprint: Mutex<[u8; 32]>,
240 committed: AtomicBool,
241 completed_statements: AtomicUsize,
242 statement_index: AtomicUsize,
243 cancel_requested_at: Mutex<Option<Instant>>,
244 trace: Mutex<QueryTrace>,
245}
246
247impl RegisteredQuery {
248 fn phase(&self) -> SqlQueryPhase {
249 SqlQueryPhase::from_u8(self.phase.load(Ordering::Acquire))
250 }
251
252 fn status(&self) -> QueryStatus {
253 let mut trace = self.trace.lock().unwrap().clone();
254 trace.transition(self.phase());
255 QueryStatus {
256 query_id: self.id,
257 owner: self.owner.clone(),
258 session_id: self.session_id.clone(),
259 phase: self.phase(),
260 started_at: self.started_at,
261 deadline: self.deadline,
262 operation: self.operation.lock().unwrap().clone(),
263 sql_fingerprint: *self.sql_fingerprint.lock().unwrap(),
264 cancellation_reason: self.control.reason(),
265 committed: self.committed.load(Ordering::Acquire),
266 completed_statements: self.completed_statements.load(Ordering::Acquire),
267 statement_index: self.statement_index.load(Ordering::Acquire),
268 cancel_requested_at: *self.cancel_requested_at.lock().unwrap(),
269 queue_duration: trace.queue_duration,
270 planning_duration: trace.planning_duration,
271 execution_duration: trace.execution_duration,
272 serialization_duration: trace.serialization_duration,
273 cancel_requested_phase: trace.cancel_requested_phase,
274 cancel_observed_phase: trace.cancel_observed_phase,
275 commit_fence_outcome: trace.commit_fence_outcome,
276 }
277 }
278
279 fn record_transition(&self, phase: SqlQueryPhase) {
280 self.trace.lock().unwrap().transition(phase);
281 }
282}
283
284#[derive(Debug, Clone)]
285struct FinishedQuery {
286 status: QueryStatus,
287 finished_at: Instant,
288 approximate_bytes: usize,
289}
290
291#[derive(Debug, Default)]
292struct RegistryState {
293 active: HashMap<QueryId, Arc<RegisteredQuery>>,
294 finished: VecDeque<FinishedQuery>,
295 finished_bytes: usize,
296}
297
298#[derive(Debug)]
299pub struct SqlQueryRegistry {
300 state: Mutex<RegistryState>,
301 max_active: usize,
302 max_finished: usize,
303 max_finished_bytes: usize,
304 finished_ttl: Duration,
305}
306
307impl Default for SqlQueryRegistry {
308 fn default() -> Self {
309 Self::new(
310 DEFAULT_MAX_ACTIVE,
311 DEFAULT_MAX_FINISHED,
312 DEFAULT_MAX_FINISHED_BYTES,
313 DEFAULT_FINISHED_TTL,
314 )
315 }
316}
317
318impl SqlQueryRegistry {
319 pub fn new(
320 max_active: usize,
321 max_finished: usize,
322 max_finished_bytes: usize,
323 finished_ttl: Duration,
324 ) -> Self {
325 Self {
326 state: Mutex::new(RegistryState::default()),
327 max_active: max_active.max(1),
328 max_finished,
329 max_finished_bytes,
330 finished_ttl,
331 }
332 }
333
334 pub fn register(self: &Arc<Self>, options: SqlQueryOptions) -> Result<RegisteredSqlQuery> {
335 validate_metadata("owner", options.owner.as_deref())?;
336 validate_metadata("session id", options.session_id.as_deref())?;
337 let id = match options.query_id {
338 Some(id) => id,
339 None => QueryId::random()?,
340 };
341 let deadline = options.timeout.map(|timeout| Instant::now() + timeout);
342 let control = match options.parent_control {
343 Some(parent) => parent.child_with_deadline(deadline),
344 None => ExecutionControl::new(deadline),
345 };
346 let started_at = Instant::now();
347 let query = Arc::new(RegisteredQuery {
348 id,
349 owner: options.owner,
350 session_id: options.session_id,
351 deadline: control.deadline(),
352 control,
353 phase: AtomicU8::new(SqlQueryPhase::Queued as u8),
354 started_at,
355 operation: Mutex::new("UNKNOWN".into()),
356 sql_fingerprint: Mutex::new([0; 32]),
357 committed: AtomicBool::new(false),
358 completed_statements: AtomicUsize::new(0),
359 statement_index: AtomicUsize::new(0),
360 cancel_requested_at: Mutex::new(None),
361 trace: Mutex::new(QueryTrace::new(started_at)),
362 });
363 let mut state = self.state.lock().unwrap();
364 self.prune_locked(&mut state);
365 if state.active.contains_key(&id) {
366 return Err(MongrelQueryError::QueryIdConflict { query_id: id });
367 }
368 if state.active.len() >= self.max_active {
369 return Err(MongrelQueryError::QueryRegistryFull);
370 }
371 state.active.insert(id, Arc::clone(&query));
372 Ok(RegisteredSqlQuery {
373 registry: Arc::downgrade(self),
374 query,
375 })
376 }
377
378 pub fn cancel(&self, query_id: QueryId) -> CancelOutcome {
379 let query = {
380 let mut state = self.state.lock().unwrap();
381 self.prune_locked(&mut state);
382 if let Some(query) = state.active.get(&query_id) {
383 Some(Arc::clone(query))
384 } else if state
385 .finished
386 .iter()
387 .any(|finished| finished.status.query_id == query_id)
388 {
389 return CancelOutcome::AlreadyFinished;
390 } else {
391 return CancelOutcome::NotFound;
392 }
393 };
394 query
395 .unwrap()
396 .request_cancel(CancellationReason::ClientRequest)
397 }
398
399 pub fn status(&self, query_id: QueryId) -> Option<QueryStatus> {
400 let mut state = self.state.lock().unwrap();
401 self.prune_locked(&mut state);
402 state
403 .active
404 .get(&query_id)
405 .map(|query| query.status())
406 .or_else(|| {
407 state
408 .finished
409 .iter()
410 .find(|finished| finished.status.query_id == query_id)
411 .map(|finished| finished.status.clone())
412 })
413 }
414
415 pub fn cancel_session(&self, session_id: &str, reason: CancellationReason) -> usize {
416 let queries = {
417 let state = self.state.lock().unwrap();
418 state
419 .active
420 .values()
421 .filter(|query| query.session_id.as_deref() == Some(session_id))
422 .cloned()
423 .collect::<Vec<_>>()
424 };
425 let mut accepted = 0;
426 for query in queries {
427 if query.request_cancel(reason) == CancelOutcome::Accepted {
428 accepted += 1;
429 }
430 }
431 accepted
432 }
433
434 pub fn cancel_all(&self, reason: CancellationReason) -> usize {
435 let queries = self
436 .state
437 .lock()
438 .unwrap()
439 .active
440 .values()
441 .cloned()
442 .collect::<Vec<_>>();
443 let mut accepted = 0;
444 for query in queries {
445 if query.request_cancel(reason) == CancelOutcome::Accepted {
446 accepted += 1;
447 }
448 }
449 accepted
450 }
451
452 pub fn active_count(&self) -> usize {
453 self.state.lock().unwrap().active.len()
454 }
455
456 pub fn active_statuses(&self) -> Vec<QueryStatus> {
457 self.state
458 .lock()
459 .unwrap()
460 .active
461 .values()
462 .map(|query| query.status())
463 .collect()
464 }
465
466 pub fn active_for_session(&self, session_id: &str) -> usize {
467 self.state
468 .lock()
469 .unwrap()
470 .active
471 .values()
472 .filter(|query| query.session_id.as_deref() == Some(session_id))
473 .count()
474 }
475
476 pub fn queued_count(&self) -> usize {
477 self.state
478 .lock()
479 .unwrap()
480 .active
481 .values()
482 .filter(|query| query.phase() == SqlQueryPhase::Queued)
483 .count()
484 }
485
486 pub fn entry_count(&self) -> usize {
487 let mut state = self.state.lock().unwrap();
488 self.prune_locked(&mut state);
489 state.active.len() + state.finished.len()
490 }
491
492 pub fn approximate_bytes(&self) -> usize {
493 let mut state = self.state.lock().unwrap();
494 self.prune_locked(&mut state);
495 state.finished_bytes
496 + state
497 .active
498 .len()
499 .saturating_mul(std::mem::size_of::<RegisteredQuery>())
500 }
501
502 pub fn finished_count(&self) -> usize {
503 let mut state = self.state.lock().unwrap();
504 self.prune_locked(&mut state);
505 state.finished.len()
506 }
507
508 fn finish(&self, query: &Arc<RegisteredQuery>, phase: SqlQueryPhase) {
509 debug_assert!(phase.is_terminal());
510 let previous = query.phase();
511 query.phase.store(phase as u8, Ordering::Release);
512 query.record_transition(previous);
513 let status = query.status();
514 let approximate_bytes = std::mem::size_of::<FinishedQuery>()
515 + status.owner.as_ref().map_or(0, String::len)
516 + status.session_id.as_ref().map_or(0, String::len)
517 + status.operation.len();
518 let mut state = self.state.lock().unwrap();
519 if state.active.remove(&query.id).is_none() {
520 return;
521 }
522 if self.max_finished > 0 && self.max_finished_bytes > 0 {
523 state.finished_bytes = state.finished_bytes.saturating_add(approximate_bytes);
524 state.finished.push_back(FinishedQuery {
525 status,
526 finished_at: Instant::now(),
527 approximate_bytes,
528 });
529 }
530 self.prune_locked(&mut state);
531 }
532
533 fn prune_locked(&self, state: &mut RegistryState) {
534 let now = Instant::now();
535 while state.finished.front().is_some_and(|entry| {
536 now.saturating_duration_since(entry.finished_at) >= self.finished_ttl
537 || state.finished.len() > self.max_finished
538 || state.finished_bytes > self.max_finished_bytes
539 }) {
540 if let Some(entry) = state.finished.pop_front() {
541 state.finished_bytes = state.finished_bytes.saturating_sub(entry.approximate_bytes);
542 }
543 }
544 }
545}
546
547impl RegisteredQuery {
548 fn request_cancel(&self, reason: CancellationReason) -> CancelOutcome {
549 loop {
550 let phase = self.phase();
551 match phase {
552 SqlQueryPhase::Queued
553 | SqlQueryPhase::Planning
554 | SqlQueryPhase::Executing
555 | SqlQueryPhase::Streaming
556 | SqlQueryPhase::Serializing => {
557 if self
558 .phase
559 .compare_exchange(
560 phase as u8,
561 SqlQueryPhase::Cancelling as u8,
562 Ordering::AcqRel,
563 Ordering::Acquire,
564 )
565 .is_ok()
566 {
567 *self.cancel_requested_at.lock().unwrap() = Some(Instant::now());
568 let mut trace = self.trace.lock().unwrap();
569 trace.transition(phase);
570 trace.cancel_requested_phase = Some(phase);
571 trace.commit_fence_outcome = CommitFenceOutcome::CancelWon;
572 self.control.cancel(reason);
573 return CancelOutcome::Accepted;
574 }
575 }
576 SqlQueryPhase::Cancelling | SqlQueryPhase::Cancelled => {
577 return CancelOutcome::AlreadyCancelling;
578 }
579 SqlQueryPhase::CommitCritical => return CancelOutcome::TooLate,
580 SqlQueryPhase::Completed | SqlQueryPhase::Failed => {
581 return CancelOutcome::AlreadyFinished;
582 }
583 }
584 }
585 }
586}
587
588#[derive(Debug, Clone)]
589pub struct RegisteredSqlQuery {
590 registry: Weak<SqlQueryRegistry>,
591 query: Arc<RegisteredQuery>,
592}
593
594pub(crate) struct SqlTaskContext {
597 query: RegisteredSqlQuery,
598 test_hook: Option<SqlTestHook>,
599}
600
601impl SqlTaskContext {
602 pub(crate) fn new(query: RegisteredSqlQuery, test_hook: Option<SqlTestHook>) -> Self {
603 Self { query, test_hook }
604 }
605
606 pub(crate) fn query(&self) -> &RegisteredSqlQuery {
607 &self.query
608 }
609
610 pub(crate) fn test_hook(&self) -> Option<&SqlTestHook> {
611 self.test_hook.as_ref()
612 }
613}
614
615impl RegisteredSqlQuery {
616 pub fn id(&self) -> QueryId {
617 self.query.id
618 }
619
620 pub fn control(&self) -> &ExecutionControl {
621 &self.query.control
622 }
623
624 pub fn phase(&self) -> SqlQueryPhase {
625 self.query.phase()
626 }
627
628 pub fn status(&self) -> QueryStatus {
629 self.query.status()
630 }
631
632 pub fn set_sql_metadata(&self, sql: &str) {
633 let normalized = sql.split_whitespace().collect::<Vec<_>>().join(" ");
634 let operation = normalized
635 .split_whitespace()
636 .next()
637 .unwrap_or("UNKNOWN")
638 .to_ascii_uppercase();
639 *self.query.operation.lock().unwrap() = operation;
640 *self.query.sql_fingerprint.lock().unwrap() = Sha256::digest(normalized.as_bytes()).into();
641 }
642
643 pub fn transition(&self, expected: SqlQueryPhase, next: SqlQueryPhase) -> Result<()> {
644 self.query
645 .phase
646 .compare_exchange(
647 expected as u8,
648 next as u8,
649 Ordering::AcqRel,
650 Ordering::Acquire,
651 )
652 .map(|_| self.query.record_transition(expected))
653 .map_err(|actual| {
654 let actual = SqlQueryPhase::from_u8(actual);
655 if actual == SqlQueryPhase::Cancelling {
656 self.cancellation_error()
657 } else {
658 MongrelQueryError::InvalidQueryState(format!(
659 "query {} expected {expected:?}, found {actual:?}",
660 self.id()
661 ))
662 }
663 })
664 }
665
666 pub fn enter_commit_critical(&self) -> Result<()> {
667 if let Err(error) = self.checkpoint() {
668 self.query.trace.lock().unwrap().commit_fence_outcome = CommitFenceOutcome::CancelWon;
669 return Err(error);
670 }
671 self.transition(SqlQueryPhase::Executing, SqlQueryPhase::CommitCritical)?;
672 self.query.trace.lock().unwrap().commit_fence_outcome = CommitFenceOutcome::CommitWon;
673 Ok(())
674 }
675
676 pub fn exit_commit_critical(&self) -> Result<()> {
677 self.transition(SqlQueryPhase::CommitCritical, SqlQueryPhase::Executing)
678 }
679
680 pub fn begin_serialization(&self) -> Result<()> {
681 match self.phase() {
682 SqlQueryPhase::Executing => {
683 self.transition(SqlQueryPhase::Executing, SqlQueryPhase::Serializing)
684 }
685 SqlQueryPhase::Streaming => {
686 self.transition(SqlQueryPhase::Streaming, SqlQueryPhase::Serializing)
687 }
688 SqlQueryPhase::CommitCritical => Ok(()),
690 phase => Err(MongrelQueryError::InvalidQueryState(format!(
691 "query {} cannot serialize from {phase:?}",
692 self.id()
693 ))),
694 }
695 }
696
697 pub fn mark_committed(&self) {
698 self.query.committed.store(true, Ordering::Release);
699 }
700
701 pub fn begin_statement(&self, index: usize) {
702 self.query.statement_index.store(index, Ordering::Release);
703 }
704
705 pub fn complete_statement(&self, index: usize) {
706 self.query
707 .completed_statements
708 .store(index.saturating_add(1), Ordering::Release);
709 }
710
711 pub fn complete_current_statement(&self) {
712 let index = self.query.statement_index.load(Ordering::Acquire);
713 self.complete_statement(index);
714 }
715
716 pub fn request_cancel(&self, reason: CancellationReason) -> CancelOutcome {
717 self.query.request_cancel(reason)
718 }
719
720 pub fn checkpoint(&self) -> Result<()> {
721 let result = self
722 .query
723 .control
724 .checkpoint()
725 .map_err(|error| match error {
726 mongreldb_core::MongrelError::DeadlineExceeded => {
727 MongrelQueryError::DeadlineExceeded {
728 query_id: self.id(),
729 timeout_ms: self.query.deadline.map(|deadline| {
730 deadline
731 .saturating_duration_since(self.query.started_at)
732 .as_millis()
733 .min(u128::from(u64::MAX)) as u64
734 }),
735 completed_statements: self
736 .query
737 .completed_statements
738 .load(Ordering::Acquire),
739 cancelled_statement_index: self
740 .query
741 .statement_index
742 .load(Ordering::Acquire),
743 }
744 }
745 mongreldb_core::MongrelError::Cancelled => self.cancellation_error(),
746 other => MongrelQueryError::Core(other),
747 });
748 if result.is_err() && self.query.control.is_cancelled() {
749 let mut trace = self.query.trace.lock().unwrap();
750 trace.cancel_observed_phase = trace.cancel_requested_phase.or(Some(self.phase()));
751 }
752 result
753 }
754
755 pub fn complete(&self) {
756 self.finish(SqlQueryPhase::Completed);
757 }
758
759 pub fn fail(&self) {
760 let phase = if self.query.control.is_cancelled() {
761 SqlQueryPhase::Cancelled
762 } else {
763 SqlQueryPhase::Failed
764 };
765 self.finish(phase);
766 }
767
768 fn finish(&self, phase: SqlQueryPhase) {
769 if let Some(registry) = self.registry.upgrade() {
770 registry.finish(&self.query, phase);
771 } else {
772 self.query.phase.store(phase as u8, Ordering::Release);
773 }
774 }
775
776 fn cancellation_error(&self) -> MongrelQueryError {
777 match self.query.control.reason() {
778 CancellationReason::Deadline => MongrelQueryError::DeadlineExceeded {
779 query_id: self.id(),
780 timeout_ms: self.query.deadline.map(|deadline| {
781 deadline
782 .saturating_duration_since(self.query.started_at)
783 .as_millis()
784 .min(u128::from(u64::MAX)) as u64
785 }),
786 completed_statements: self.query.completed_statements.load(Ordering::Acquire),
787 cancelled_statement_index: self.query.statement_index.load(Ordering::Acquire),
788 },
789 reason => MongrelQueryError::QueryCancelled {
790 query_id: self.id(),
791 reason,
792 completed_statements: self.query.completed_statements.load(Ordering::Acquire),
793 cancelled_statement_index: self.query.statement_index.load(Ordering::Acquire),
794 },
795 }
796 }
797}
798
799pub struct RegisteredQueryGuard {
800 query: Option<RegisteredSqlQuery>,
801}
802
803impl RegisteredQueryGuard {
804 pub fn new(query: RegisteredSqlQuery) -> Self {
805 Self { query: Some(query) }
806 }
807
808 pub fn query(&self) -> &RegisteredSqlQuery {
809 self.query
810 .as_ref()
811 .expect("registered query guard consumed")
812 }
813
814 pub fn complete(mut self) {
815 if let Some(query) = self.query.take() {
816 query.complete();
817 }
818 }
819
820 pub fn fail(mut self) {
821 if let Some(query) = self.query.take() {
822 query.fail();
823 }
824 }
825
826 pub fn into_query(mut self) -> RegisteredSqlQuery {
827 self.query.take().expect("registered query guard consumed")
828 }
829}
830
831impl Drop for RegisteredQueryGuard {
832 fn drop(&mut self) {
833 if let Some(query) = self.query.take() {
834 query.fail();
835 }
836 }
837}
838
839fn validate_metadata(name: &str, value: Option<&str>) -> Result<()> {
840 if value.is_some_and(|value| value.len() > MAX_METADATA_BYTES) {
841 return Err(MongrelQueryError::Core(
842 mongreldb_core::MongrelError::InvalidArgument(format!(
843 "{name} exceeds {MAX_METADATA_BYTES} bytes"
844 )),
845 ));
846 }
847 Ok(())
848}
849
850#[cfg(test)]
851mod tests {
852 use super::*;
853
854 #[test]
855 fn query_ids_are_random_strict_and_round_trip() {
856 let first = QueryId::random().unwrap();
857 let second = QueryId::random().unwrap();
858 assert_ne!(first, second);
859 assert_eq!(first.to_string().parse::<QueryId>().unwrap(), first);
860 assert!("abc".parse::<QueryId>().is_err());
861 assert!("0000000000000000000000000000000z"
862 .parse::<QueryId>()
863 .is_err());
864 }
865
866 #[test]
867 fn duplicate_active_ids_are_rejected_and_cleanup_is_bounded() {
868 let registry = Arc::new(SqlQueryRegistry::new(1, 1, 1024, Duration::from_secs(60)));
869 let id = QueryId::random().unwrap();
870 let query = registry
871 .register(SqlQueryOptions {
872 query_id: Some(id),
873 ..SqlQueryOptions::default()
874 })
875 .unwrap();
876 assert!(matches!(
877 registry.register(SqlQueryOptions {
878 query_id: Some(id),
879 ..SqlQueryOptions::default()
880 }),
881 Err(MongrelQueryError::QueryIdConflict { .. })
882 ));
883 query.complete();
884 assert_eq!(registry.active_count(), 0);
885 assert_eq!(registry.finished_count(), 1);
886 }
887
888 #[test]
889 fn cancel_and_commit_fence_have_one_winner() {
890 for cancel_first in [true, false] {
891 let registry = Arc::new(SqlQueryRegistry::default());
892 let query = registry.register(SqlQueryOptions::default()).unwrap();
893 query
894 .transition(SqlQueryPhase::Queued, SqlQueryPhase::Executing)
895 .unwrap();
896 if cancel_first {
897 assert_eq!(
898 query.request_cancel(CancellationReason::ClientRequest),
899 CancelOutcome::Accepted
900 );
901 assert!(query.enter_commit_critical().is_err());
902 let status = query.status();
903 assert_eq!(
904 status.cancel_requested_phase,
905 Some(SqlQueryPhase::Executing)
906 );
907 assert_eq!(status.cancel_observed_phase, Some(SqlQueryPhase::Executing));
908 assert_eq!(status.commit_fence_outcome, CommitFenceOutcome::CancelWon);
909 } else {
910 query.enter_commit_critical().unwrap();
911 assert_eq!(
912 query.request_cancel(CancellationReason::ClientRequest),
913 CancelOutcome::TooLate
914 );
915 query.mark_committed();
916 query.complete();
917 let status = registry.status(query.id()).unwrap();
918 assert!(status.committed);
919 assert_eq!(status.commit_fence_outcome, CommitFenceOutcome::CommitWon);
920 }
921 }
922 }
923
924 #[test]
925 fn guard_cleans_up_dropped_execution_without_raw_sql() {
926 let registry = Arc::new(SqlQueryRegistry::default());
927 let query = registry
928 .register(SqlQueryOptions {
929 owner: Some("alice".into()),
930 session_id: Some("session".into()),
931 ..SqlQueryOptions::default()
932 })
933 .unwrap();
934 query.set_sql_metadata("SELECT secret FROM docs WHERE token = 'private'");
935 let id = query.id();
936 drop(RegisteredQueryGuard::new(query));
937 let status = registry.status(id).unwrap();
938 assert_eq!(status.phase, SqlQueryPhase::Failed);
939 assert_eq!(status.operation, "SELECT");
940 assert_ne!(status.sql_fingerprint, [0; 32]);
941 }
942}