1use std::collections::{HashMap, HashSet};
4use std::sync::atomic::{AtomicU64, Ordering};
5use std::sync::Arc;
6
7use crate::runtime::audit_log::{AuditAuthSource, AuditEvent, AuditFieldEscaper, Outcome};
8use crate::runtime::impl_core::{
9 clear_current_auth_identity, clear_current_tenant, current_auth_identity, current_tenant,
10 set_current_auth_identity, set_current_tenant,
11};
12use crate::runtime::queue_telemetry::NackOutcomeLabel;
13use crate::storage::queue::QueueMode;
14use crate::storage::unified::entity::{QueueMessageData, RowData};
15use crate::storage::unified::{Metadata, MetadataValue, UnifiedStore};
16use crate::telemetry::operator_event::OperatorEvent;
17
18use super::*;
19
20use super::primary_queue_store::PrimaryQueueStore;
21use super::queue_lifecycle::{QueueLifecycle, RetirementOutcome};
22use crate::storage::queue::lifecycle::{
23 QueueSide as LcQueueSide, QueueStore as _, QueueStoreError, QueueTxn,
24};
25
26pub(super) fn runtime_lifecycle(
33 runtime: &RedDBRuntime,
34 queue: &str,
35) -> (
36 QueueLifecycle<PrimaryQueueStore>,
37 PrimaryQueueStore,
38 QueueTxn,
39) {
40 let primary_for_lookup = PrimaryQueueStore::new(runtime.clone());
41 let primary_for_lifecycle = PrimaryQueueStore::new(runtime.clone());
42 let txn = primary_for_lifecycle.new_txn();
43 let cfg = primary_for_lifecycle.lifecycle_config(queue);
44 (
45 QueueLifecycle::new(primary_for_lifecycle, cfg)
46 .with_telemetry(Arc::clone(&runtime.inner.queue_telemetry)),
47 primary_for_lookup,
48 txn,
49 )
50}
51
52pub(crate) const QUEUE_READ_WAIT_CANCELLED: &str =
58 "QUEUE READ WAIT cancelled — server shutting down";
59
60pub(crate) const QUEUE_MAX_WAIT_MS_CONFIG_KEY: &str = "red.config.queue.max_wait_ms";
64
65pub(crate) const QUEUE_MAX_WAIT_MS_DEFAULT: u64 = 60_000;
68
69#[derive(Debug)]
80pub(crate) enum RedwireWaitOutcome {
81 Delivered(Vec<crate::serde_json::Value>),
82 TimedOut,
83 Cancelled,
84}
85
86pub(super) fn queue_wait_scope() -> String {
91 crate::runtime::impl_core::current_tenant().unwrap_or_default()
92}
93
94fn with_redwire_wait_context<T>(
95 auth_identity: &Option<(String, crate::auth::Role)>,
96 tenant: &Option<String>,
97 f: impl FnOnce() -> T,
98) -> T {
99 let previous_auth = current_auth_identity();
100 let previous_tenant = current_tenant();
101 match tenant {
102 Some(t) => set_current_tenant(t.clone()),
103 None => clear_current_tenant(),
104 }
105 match auth_identity {
106 Some((username, role)) => set_current_auth_identity(username.clone(), *role),
107 None => clear_current_auth_identity(),
108 }
109 let result = f();
110 match previous_tenant {
111 Some(t) => set_current_tenant(t),
112 None => clear_current_tenant(),
113 }
114 match previous_auth {
115 Some((username, role)) => set_current_auth_identity(username, role),
116 None => clear_current_auth_identity(),
117 }
118 result
119}
120
121fn ast_side_to_lc(side: crate::storage::query::ast::QueueSide) -> LcQueueSide {
125 use crate::storage::query::ast::QueueSide as Ast;
126 match side {
127 Ast::Left => LcQueueSide::Left,
128 Ast::Right => LcQueueSide::Right,
129 }
130}
131
132fn map_qse(err: QueueStoreError) -> RedDBError {
135 match err {
136 QueueStoreError::UnknownDelivery(id) => RedDBError::NotFound(format!(
137 "delivery_id '{id}' does not resolve to a live pending delivery"
138 )),
139 QueueStoreError::UnknownQueue(q) => RedDBError::NotFound(format!("queue '{q}' not found")),
140 QueueStoreError::ReplicaImmutable => {
141 RedDBError::Internal("replica QueueStore is immutable".to_string())
142 }
143 }
144}
145
146pub static EVENTS_DRAIN_RETRIES_TOTAL: AtomicU64 = AtomicU64::new(0);
153
154pub static EVENTS_DLQ_TOTAL: AtomicU64 = AtomicU64::new(0);
156
157pub static EVENTS_ENQUEUED_TOTAL: AtomicU64 = AtomicU64::new(0);
159
160const OUTBOX_WARN_BYTES: u64 = 1 << 30;
162
163const OUTBOX_MAX_BYTES: u64 = 10 * (1 << 30);
165
166static OUTBOX_APPROX_BYTES: AtomicU64 = AtomicU64::new(0);
168
169const QUEUE_META_COLLECTION: &str = "red_queue_meta";
170const QUEUE_POSITION_CENTER: u64 = u64::MAX / 2;
171const WORK_DEFAULT_GROUP: &str = "_work_default";
172const FANOUT_GROUP_PREFIX: &str = "_fanout_";
173
174#[derive(Debug, Clone)]
175pub(super) struct QueueRuntimeConfig {
176 pub(super) mode: QueueMode,
177 pub(super) priority: bool,
178 pub(super) max_size: Option<usize>,
179 pub(super) ttl_ms: Option<u64>,
180 pub(super) dlq: Option<String>,
181 pub(super) max_attempts: u32,
182 pub(super) lock_deadline_ms: u64,
183 pub(super) in_flight_cap_per_group: u32,
184 pub(super) retry_delay_ms: Option<u64>,
189}
190
191#[derive(Debug, Clone)]
192struct QueueGroupEntry {
193 entity_id: EntityId,
194 group: String,
195}
196
197#[derive(Debug, Clone)]
198pub(super) struct QueuePendingEntry {
199 pub(super) entity_id: EntityId,
200 group: String,
201 pub(super) message_id: EntityId,
202 consumer: String,
203 pub(super) delivered_at_ns: u64,
204 pub(super) delivery_count: u32,
205}
206
207#[derive(Debug, Clone)]
208pub(super) struct QueueAckEntry {
209 entity_id: EntityId,
210 group: String,
211 pub(super) message_id: EntityId,
212}
213
214#[derive(Debug, Clone)]
215pub(super) struct QueueMessageView {
216 pub(super) id: EntityId,
217 position: u64,
218 priority: i32,
219 pub(super) payload: Value,
220 attempts: u32,
221 pub(super) max_attempts: u32,
222 enqueued_at_ns: u64,
223 pub(super) available_at_ns: Option<u64>,
227}
228
229impl QueueMessageView {
230 pub(super) fn is_available_now(&self) -> bool {
235 match self.available_at_ns {
236 Some(at) => at <= now_ns(),
237 None => true,
238 }
239 }
240}
241
242impl RedDBRuntime {
243 pub(super) fn group_read_with_optional_wait(
252 &self,
253 queue: &str,
254 group: &str,
255 consumer: &str,
256 count: usize,
257 wait_ms: Option<u64>,
258 ) -> RedDBResult<Vec<crate::runtime::queue_lifecycle::DeliveredMessage>> {
259 let do_read =
260 |runtime: &RedDBRuntime| -> RedDBResult<Vec<crate::runtime::queue_lifecycle::DeliveredMessage>> {
261 let read_lock = runtime
267 .inner
268 .rmw_locks
269 .lock_for(queue, "__queue_group_read__");
270 let _read_guard = read_lock.lock();
271 let (lifecycle, _ps, txn) = runtime_lifecycle(runtime, queue);
272 lifecycle
273 .group_read(&txn, queue, group, consumer, count)
274 .map_err(map_qse)
275 };
276
277 let delivered = do_read(self)?;
278 let Some(wait_ms) = wait_ms else {
279 return Ok(delivered);
280 };
281 if !delivered.is_empty() {
282 return Ok(delivered);
283 }
284 let registry = self.queue_wait_registry();
294 let scope = queue_wait_scope();
295 let deadline = std::time::Instant::now() + std::time::Duration::from_millis(wait_ms);
296 let telemetry = self.queue_telemetry();
297 telemetry.record_wait_started(&scope, queue);
298 let wait_start = std::time::Instant::now();
299 let observe = |outcome: crate::runtime::queue_telemetry::WaitOutcomeLabel| {
300 let elapsed_ms = wait_start.elapsed().as_millis().min(u128::from(u64::MAX)) as u64;
301 telemetry.record_wait_outcome(&scope, queue, outcome, elapsed_ms);
302 };
303 loop {
304 let snapshot = registry.snapshot(&scope, queue);
308 let delivered = do_read(self)?;
309 if !delivered.is_empty() {
310 observe(crate::runtime::queue_telemetry::WaitOutcomeLabel::Woken);
311 return Ok(delivered);
312 }
313 if registry.is_cancelled() {
314 observe(crate::runtime::queue_telemetry::WaitOutcomeLabel::Cancelled);
315 return Err(RedDBError::Query(QUEUE_READ_WAIT_CANCELLED.to_string()));
316 }
317 if std::time::Instant::now() >= deadline {
318 observe(crate::runtime::queue_telemetry::WaitOutcomeLabel::Timeout);
319 return Ok(Vec::new());
320 }
321 let park_deadline = match earliest_future_available_at(&self.inner.db.store(), queue) {
331 Some(at_ns) => {
332 let now_ns = now_ns();
333 if at_ns <= now_ns {
334 deadline.min(std::time::Instant::now())
336 } else {
337 let wait_ns = at_ns - now_ns;
338 let due_instant =
339 std::time::Instant::now() + std::time::Duration::from_nanos(wait_ns);
340 deadline.min(due_instant)
341 }
342 }
343 None => deadline,
344 };
345 match registry.wait_until(&snapshot, park_deadline) {
346 crate::runtime::queue_wait_registry::WaitOutcome::Woken => continue,
347 crate::runtime::queue_wait_registry::WaitOutcome::Timeout => {
348 if std::time::Instant::now() >= deadline {
352 observe(crate::runtime::queue_telemetry::WaitOutcomeLabel::Timeout);
353 return Ok(Vec::new());
354 }
355 continue;
356 }
357 crate::runtime::queue_wait_registry::WaitOutcome::Cancelled => {
358 observe(crate::runtime::queue_telemetry::WaitOutcomeLabel::Cancelled);
359 return Err(RedDBError::Query(QUEUE_READ_WAIT_CANCELLED.to_string()));
360 }
361 }
362 }
363 }
364
365 pub(crate) async fn redwire_queue_wait_json(
382 &self,
383 queue: &str,
384 group: Option<&str>,
385 consumer: &str,
386 count: usize,
387 wait_ms: u64,
388 auth_identity: Option<(String, crate::auth::Role)>,
389 tenant: Option<String>,
390 ) -> RedDBResult<RedwireWaitOutcome> {
391 let group_owned: RedDBResult<String> =
392 with_redwire_wait_context(&auth_identity, &tenant, || {
393 let expr = crate::storage::query::ast::QueryExpr::QueueCommand(
394 crate::storage::query::ast::QueueCommand::GroupRead {
395 queue: queue.to_string(),
396 group: group.map(str::to_string),
397 consumer: consumer.to_string(),
398 count,
399 wait_ms: Some(wait_ms),
400 },
401 );
402 self.check_query_privilege(&expr)
403 .map_err(RedDBError::Query)?;
404 let store = self.inner.db.store();
405 ensure_queue_exists(store.as_ref(), queue)?;
406 let config = load_queue_config(store.as_ref(), queue);
407 resolve_read_group(store.as_ref(), queue, group, consumer, &config)
408 });
409 let group_owned = group_owned?;
410 let group_ref = group_owned.as_str();
411
412 let do_read =
413 |runtime: &RedDBRuntime| -> RedDBResult<Vec<crate::runtime::queue_lifecycle::DeliveredMessage>> {
414 with_redwire_wait_context(&auth_identity, &tenant, || {
415 let (lifecycle, _ps, txn) = runtime_lifecycle(runtime, queue);
416 lifecycle
417 .group_read(&txn, queue, group_ref, consumer, count)
418 .map_err(map_qse)
419 })
420 };
421
422 let render = |delivered: Vec<crate::runtime::queue_lifecycle::DeliveredMessage>| {
423 RedwireWaitOutcome::Delivered(
424 delivered.into_iter().map(delivered_message_json).collect(),
425 )
426 };
427
428 let delivered = do_read(self)?;
430 if !delivered.is_empty() {
431 return Ok(render(delivered));
432 }
433
434 let registry = self.queue_wait_registry();
435 let scope = with_redwire_wait_context(&auth_identity, &tenant, queue_wait_scope);
436 let deadline = std::time::Instant::now() + std::time::Duration::from_millis(wait_ms);
437 let telemetry = self.queue_telemetry();
438 telemetry.record_wait_started(&scope, queue);
439 let wait_start = std::time::Instant::now();
440 tracing::debug!(
441 target: "reddb::redwire::queue_wait",
442 queue,
443 group = group_ref,
444 consumer,
445 count,
446 wait_ms,
447 scope = scope.as_str(),
448 "redwire queue wait parked"
449 );
450 let observe = |outcome: crate::runtime::queue_telemetry::WaitOutcomeLabel| {
451 let elapsed_ms = wait_start.elapsed().as_millis().min(u128::from(u64::MAX)) as u64;
452 telemetry.record_wait_outcome(&scope, queue, outcome, elapsed_ms);
453 tracing::debug!(
454 target: "reddb::redwire::queue_wait",
455 queue,
456 group = group_ref,
457 consumer,
458 count,
459 wait_ms,
460 scope = scope.as_str(),
461 outcome = outcome.as_str(),
462 duration_ms = elapsed_ms,
463 "redwire queue wait resolved"
464 );
465 };
466 loop {
467 let waiter = registry.async_waiter(&scope, queue);
471 let delivered = do_read(self)?;
472 if !delivered.is_empty() {
473 observe(crate::runtime::queue_telemetry::WaitOutcomeLabel::Woken);
474 return Ok(render(delivered));
475 }
476 if registry.is_cancelled() {
477 observe(crate::runtime::queue_telemetry::WaitOutcomeLabel::Cancelled);
478 return Ok(RedwireWaitOutcome::Cancelled);
479 }
480 if std::time::Instant::now() >= deadline {
481 observe(crate::runtime::queue_telemetry::WaitOutcomeLabel::Timeout);
482 return Ok(RedwireWaitOutcome::TimedOut);
483 }
484 let park_deadline = match earliest_future_available_at(&self.inner.db.store(), queue) {
485 Some(at_ns) => {
486 let now_ns = now_ns();
487 if at_ns <= now_ns {
488 deadline.min(std::time::Instant::now())
489 } else {
490 let wait_ns = at_ns - now_ns;
491 let due_instant =
492 std::time::Instant::now() + std::time::Duration::from_nanos(wait_ns);
493 deadline.min(due_instant)
494 }
495 }
496 None => deadline,
497 };
498 match registry.wait_until_async(&waiter, park_deadline).await {
503 crate::runtime::queue_wait_registry::WaitOutcome::Woken => continue,
504 crate::runtime::queue_wait_registry::WaitOutcome::Timeout => {
505 if std::time::Instant::now() >= deadline {
506 observe(crate::runtime::queue_telemetry::WaitOutcomeLabel::Timeout);
507 return Ok(RedwireWaitOutcome::TimedOut);
508 }
509 continue;
510 }
511 crate::runtime::queue_wait_registry::WaitOutcome::Cancelled => {
512 observe(crate::runtime::queue_telemetry::WaitOutcomeLabel::Cancelled);
513 return Ok(RedwireWaitOutcome::Cancelled);
514 }
515 }
516 }
517 }
518
519 pub(crate) fn redwire_queue_wait_cap_check(&self, wait_ms: u64) -> Result<(), String> {
528 let cap = self.config_u64(QUEUE_MAX_WAIT_MS_CONFIG_KEY, QUEUE_MAX_WAIT_MS_DEFAULT);
529 if wait_ms > cap {
530 Err(format!(
531 "queue-wait WAIT {wait_ms}ms exceeds server cap {QUEUE_MAX_WAIT_MS_CONFIG_KEY} = {cap}ms"
532 ))
533 } else {
534 Ok(())
535 }
536 }
537
538 pub(crate) fn enqueue_event_payload(
539 &self,
540 queue: &str,
541 payload: Value,
542 ) -> RedDBResult<EntityId> {
543 let store = self.inner.db.store();
544 if store.get_collection(queue).is_none() {
546 crate::runtime::impl_ddl::ensure_event_target_queue_pub(self, queue)?;
547 }
548
549 let payload_bytes = estimate_payload_bytes(&payload);
551 let outbox_bytes = OUTBOX_APPROX_BYTES.fetch_add(payload_bytes, Ordering::Relaxed);
552
553 if outbox_bytes > OUTBOX_MAX_BYTES {
555 OUTBOX_APPROX_BYTES.fetch_sub(payload_bytes, Ordering::Relaxed);
556 EVENTS_DRAIN_RETRIES_TOTAL.fetch_add(1, Ordering::Relaxed);
557 return self.route_event_to_outbox_dlq(queue, payload, "outbox_max_bytes_exceeded");
558 }
559
560 if outbox_bytes > OUTBOX_WARN_BYTES && outbox_bytes - payload_bytes <= OUTBOX_WARN_BYTES {
562 tracing::warn!(
563 outbox_bytes,
564 warn_threshold = OUTBOX_WARN_BYTES,
565 "event outbox approaching capacity warning threshold"
566 );
567 crate::telemetry::operator_event::OperatorEvent::OutboxDlqActivated {
568 queue: queue.to_string(),
569 dlq: format!("{queue}_outbox_dlq"),
570 reason: "outbox_warn_bytes_exceeded".to_string(),
571 }
572 .emit_global();
573 }
574
575 let config = load_queue_config(store.as_ref(), queue);
576
577 if let Some(max_size) = config.max_size {
579 let current_len = load_queue_message_views(store.as_ref(), queue)
580 .unwrap_or_default()
581 .len();
582 if current_len >= max_size {
583 OUTBOX_APPROX_BYTES.fetch_sub(payload_bytes, Ordering::Relaxed);
584 EVENTS_DRAIN_RETRIES_TOTAL.fetch_add(1, Ordering::Relaxed);
585 return self.route_event_to_outbox_dlq(queue, payload, "queue_full");
586 }
587 if current_len * 10 >= max_size * 8 {
589 tracing::warn!(
590 queue = %queue,
591 size = current_len,
592 max = max_size,
593 "event target queue near capacity"
594 );
595 }
596 }
597
598 let id = self.enqueue_event_payload_raw(store.as_ref(), queue, &config, payload)?;
599 EVENTS_ENQUEUED_TOTAL.fetch_add(1, Ordering::Relaxed);
600 Ok(id)
601 }
602
603 fn route_event_to_outbox_dlq(
605 &self,
606 queue: &str,
607 payload: Value,
608 reason: &str,
609 ) -> RedDBResult<EntityId> {
610 let dlq_name = format!("{queue}_outbox_dlq");
611 EVENTS_DLQ_TOTAL.fetch_add(1, Ordering::Relaxed);
612
613 crate::telemetry::operator_event::OperatorEvent::OutboxDlqActivated {
614 queue: queue.to_string(),
615 dlq: dlq_name.clone(),
616 reason: reason.to_string(),
617 }
618 .emit_global();
619
620 let store = self.inner.db.store();
621 if store.get_collection(&dlq_name).is_none() {
622 crate::runtime::impl_ddl::ensure_event_target_queue_pub(self, &dlq_name)?;
623 }
624 let dlq_config = load_queue_config(store.as_ref(), &dlq_name);
625 let id = self.enqueue_event_payload_raw(store.as_ref(), &dlq_name, &dlq_config, payload)?;
626 EVENTS_ENQUEUED_TOTAL.fetch_add(1, Ordering::Relaxed);
627 Ok(id)
628 }
629
630 fn enqueue_event_payload_raw(
632 &self,
633 store: &UnifiedStore,
634 queue: &str,
635 config: &QueueRuntimeConfig,
636 payload: Value,
637 ) -> RedDBResult<EntityId> {
638 let position = next_queue_position(store, queue, QueueSide::Right)?;
639 let mut entity = UnifiedEntity::new(
640 EntityId::new(0),
641 EntityKind::QueueMessage {
642 queue: queue.to_string(),
643 position,
644 },
645 EntityData::QueueMessage(QueueMessageData {
646 payload,
647 priority: None,
648 enqueued_at_ns: now_ns(),
649 attempts: 0,
650 max_attempts: config.max_attempts,
651 acked: false,
652 }),
653 );
654 if let Some(xid) = self.current_xid() {
655 entity.set_xmin(xid);
656 }
657 let id = store
658 .insert_auto(queue, entity)
659 .map_err(|err| RedDBError::Internal(err.to_string()))?;
660 if let Some(ttl_ms) = config.ttl_ms {
661 store
662 .set_metadata(queue, id, queue_message_ttl_metadata(ttl_ms))
663 .map_err(|err| RedDBError::Internal(err.to_string()))?;
664 }
665 self.invalidate_result_cache_for_table(queue);
666 Ok(id)
667 }
668
669 pub fn execute_create_queue(
670 &self,
671 raw_query: &str,
672 query: &CreateQueueQuery,
673 ) -> RedDBResult<RuntimeQueryResult> {
674 self.check_write(crate::runtime::write_gate::WriteKind::Ddl)?;
675 if query.dlq.as_deref() == Some(query.name.as_str()) {
676 return Err(RedDBError::Query(
677 "dead-letter queue must be different from the source queue".to_string(),
678 ));
679 }
680
681 let store = self.inner.db.store();
682 let exists = store.get_collection(&query.name).is_some();
683 if exists {
684 if query.if_not_exists {
685 return Ok(RuntimeQueryResult::ok_message(
686 raw_query.to_string(),
687 &format!("queue '{}' already exists", query.name),
688 "create",
689 ));
690 }
691 return Err(RedDBError::Query(format!(
692 "queue '{}' already exists",
693 query.name
694 )));
695 }
696
697 store
698 .create_collection(&query.name)
699 .map_err(|err| RedDBError::Internal(err.to_string()))?;
700 if let Some(ttl_ms) = query.ttl_ms {
701 self.inner
702 .db
703 .set_collection_default_ttl_ms(&query.name, ttl_ms);
704 }
705 self.inner
706 .db
707 .save_collection_contract(queue_collection_contract(
708 &query.name,
709 query.priority,
710 query.ttl_ms,
711 ))
712 .map_err(|err| RedDBError::Internal(err.to_string()))?;
713 save_queue_config(
714 store.as_ref(),
715 &query.name,
716 &QueueRuntimeConfig {
717 mode: query.mode,
718 priority: query.priority,
719 max_size: query.max_size,
720 ttl_ms: query.ttl_ms,
721 dlq: query.dlq.clone(),
722 max_attempts: query.max_attempts,
723 lock_deadline_ms: query.lock_deadline_ms,
724 in_flight_cap_per_group: query.in_flight_cap_per_group,
725 retry_delay_ms: query.retry_delay_ms,
726 },
727 )?;
728
729 if let Some(dlq) = &query.dlq {
730 if store.get_collection(dlq).is_none() {
731 store
732 .create_collection(dlq)
733 .map_err(|err| RedDBError::Internal(err.to_string()))?;
734 self.inner
735 .db
736 .save_collection_contract(queue_collection_contract(dlq, false, None))
737 .map_err(|err| RedDBError::Internal(err.to_string()))?;
738 }
739 }
740
741 self.invalidate_result_cache();
742 self.inner
743 .db
744 .persist_metadata()
745 .map_err(|err| RedDBError::Internal(err.to_string()))?;
746 let mut type_tags = Vec::new();
751 if let Some(dlq) = &query.dlq {
752 type_tags.push(format!("dlq:{}", dlq));
753 }
754 self.schema_vocabulary_apply(
755 crate::runtime::schema_vocabulary::DdlEvent::CreateCollection {
756 collection: query.name.clone(),
757 columns: vec!["payload".to_string()],
758 type_tags,
759 description: None,
760 },
761 );
762
763 let mut msg = format!("queue '{}' created", query.name);
764 msg.push_str(&format!(" (mode={})", query.mode.as_str()));
765 if query.priority {
766 msg.push_str(" (priority)");
767 }
768 if let Some(max_size) = query.max_size {
769 msg.push_str(&format!(" (max_size={max_size})"));
770 }
771 if let Some(ttl_ms) = query.ttl_ms {
772 msg.push_str(&format!(" (ttl={ttl_ms}ms)"));
773 }
774 if let Some(dlq) = &query.dlq {
775 msg.push_str(&format!(
776 " (dlq={dlq}, max_attempts={})",
777 query.max_attempts
778 ));
779 }
780
781 Ok(RuntimeQueryResult::ok_message(
782 raw_query.to_string(),
783 &msg,
784 "create",
785 ))
786 }
787
788 pub fn execute_alter_queue(
789 &self,
790 raw_query: &str,
791 query: &AlterQueueQuery,
792 ) -> RedDBResult<RuntimeQueryResult> {
793 self.check_write(crate::runtime::write_gate::WriteKind::Ddl)?;
794 let store = self.inner.db.store();
795 ensure_queue_exists(store.as_ref(), &query.name)?;
796
797 let mut config = load_queue_config(store.as_ref(), &query.name);
798 let mut summary: Vec<String> = Vec::new();
799
800 if let Some(new_mode) = query.mode {
801 let pending =
802 load_pending_entries(store.as_ref(), &query.name, None, None).unwrap_or_default();
803 if !pending.is_empty() {
804 tracing::warn!(
805 queue = %query.name,
806 pending_count = pending.len(),
807 new_mode = %new_mode.as_str(),
808 "ALTER QUEUE SET MODE: {} in-flight messages will drain with old mode; \
809 new reads use {}",
810 pending.len(),
811 new_mode.as_str(),
812 );
813 }
814 config.mode = new_mode;
815 summary.push(format!("mode={}", new_mode.as_str()));
816 }
817 if let Some(max_attempts) = query.max_attempts {
818 config.max_attempts = max_attempts;
819 summary.push(format!("max_attempts={max_attempts}"));
820 }
821 if let Some(lock_deadline_ms) = query.lock_deadline_ms {
822 config.lock_deadline_ms = lock_deadline_ms;
823 summary.push(format!("lock_deadline_ms={lock_deadline_ms}"));
824 }
825 if let Some(in_flight_cap) = query.in_flight_cap_per_group {
826 config.in_flight_cap_per_group = in_flight_cap;
827 summary.push(format!("in_flight_cap_per_group={in_flight_cap}"));
828 }
829 if let Some(dlq) = &query.dlq {
830 if dlq == &query.name {
831 return Err(RedDBError::Query(
832 "dead-letter queue must be different from the source queue".to_string(),
833 ));
834 }
835 config.dlq = Some(dlq.clone());
836 summary.push(format!("dlq={dlq}"));
837 }
838 if let Some(retry_delay_ms) = query.retry_delay_ms {
839 config.retry_delay_ms = if retry_delay_ms == 0 {
840 None
841 } else {
842 Some(retry_delay_ms)
843 };
844 summary.push(format!("retry_delay_ms={retry_delay_ms}"));
845 }
846
847 save_queue_config(store.as_ref(), &query.name, &config)?;
848
849 self.invalidate_result_cache();
850 self.inner
851 .db
852 .persist_metadata()
853 .map_err(|err| RedDBError::Internal(err.to_string()))?;
854
855 Ok(RuntimeQueryResult::ok_message(
856 raw_query.to_string(),
857 &format!("queue '{}' altered: {}", query.name, summary.join(", ")),
858 "alter",
859 ))
860 }
861
862 pub fn execute_drop_queue(
863 &self,
864 raw_query: &str,
865 query: &DropQueueQuery,
866 ) -> RedDBResult<RuntimeQueryResult> {
867 self.check_write(crate::runtime::write_gate::WriteKind::Ddl)?;
868 let store = self.inner.db.store();
869 if super::impl_ddl::is_system_schema_name(&query.name) {
870 return Err(RedDBError::Query("system schema is read-only".to_string()));
871 }
872 if store.get_collection(&query.name).is_none() {
873 if query.if_exists {
874 return Ok(RuntimeQueryResult::ok_message(
875 raw_query.to_string(),
876 &format!("queue '{}' does not exist", query.name),
877 "drop",
878 ));
879 }
880 return Err(RedDBError::NotFound(format!(
881 "queue '{}' not found",
882 query.name
883 )));
884 }
885 let actual = crate::runtime::ddl::polymorphic_resolver::resolve(
886 &query.name,
887 &self.inner.db.catalog_model_snapshot(),
888 )?;
889 crate::runtime::ddl::polymorphic_resolver::ensure_model_match(
890 crate::catalog::CollectionModel::Queue,
891 actual,
892 )?;
893
894 store
895 .drop_collection(&query.name)
896 .map_err(|err| RedDBError::Internal(err.to_string()))?;
897 self.inner.db.clear_collection_default_ttl_ms(&query.name);
898 self.inner
899 .db
900 .remove_collection_contract(&query.name)
901 .map_err(|err| RedDBError::Internal(err.to_string()))?;
902 remove_queue_metadata(store.as_ref(), &query.name);
903 self.invalidate_result_cache();
904 self.inner
905 .db
906 .persist_metadata()
907 .map_err(|err| RedDBError::Internal(err.to_string()))?;
908 self.schema_vocabulary_apply(
910 crate::runtime::schema_vocabulary::DdlEvent::DropCollection {
911 collection: query.name.clone(),
912 },
913 );
914
915 Ok(RuntimeQueryResult::ok_message(
916 raw_query.to_string(),
917 &format!("queue '{}' dropped", query.name),
918 "drop",
919 ))
920 }
921
922 pub fn execute_queue_command(
923 &self,
924 raw_query: &str,
925 cmd: &QueueCommand,
926 ) -> RedDBResult<RuntimeQueryResult> {
927 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
928 match cmd {
929 QueueCommand::Push {
930 queue,
931 value,
932 side,
933 priority,
934 available,
935 } => {
936 let store = self.inner.db.store();
937 ensure_queue_exists(store.as_ref(), queue)?;
938 let config = load_queue_config(store.as_ref(), queue);
939 if priority.is_some() && !config.priority {
940 return Err(RedDBError::Query(format!(
941 "queue '{}' is not a priority queue",
942 queue
943 )));
944 }
945 if let Some(max_size) = config.max_size {
946 let current_len =
947 load_queue_message_views_with_runtime(Some(self), store.as_ref(), queue)?
948 .len();
949 if current_len >= max_size {
950 return Err(RedDBError::Query(format!(
951 "queue '{}' is full (max_size={max_size})",
952 queue
953 )));
954 }
955 }
956
957 let position = next_queue_position(store.as_ref(), queue, *side)?;
958 let mut entity = UnifiedEntity::new(
959 EntityId::new(0),
960 EntityKind::QueueMessage {
961 queue: queue.clone(),
962 position,
963 },
964 EntityData::QueueMessage(QueueMessageData {
965 payload: value.clone(),
966 priority: if config.priority { *priority } else { None },
967 enqueued_at_ns: now_ns(),
968 attempts: 0,
969 max_attempts: config.max_attempts,
970 acked: false,
971 }),
972 );
973 if let Some(xid) = self.current_xid() {
976 entity.set_xmin(xid);
977 }
978 let id = store
979 .insert_auto(queue, entity)
980 .map_err(|err| RedDBError::Internal(err.to_string()))?;
981 let available_at_ns = available.map(|a| match a {
986 crate::storage::query::ast::QueueAvailability::DelayMs(ms) => {
987 now_ns().saturating_add(ms.saturating_mul(1_000_000))
988 }
989 crate::storage::query::ast::QueueAvailability::AtUnixMs(ms) => {
990 ms.saturating_mul(1_000_000)
991 }
992 });
993 if config.ttl_ms.is_some() || available_at_ns.is_some() {
994 store
995 .set_metadata(
996 queue,
997 id,
998 queue_message_metadata(config.ttl_ms, available_at_ns),
999 )
1000 .map_err(|err| RedDBError::Internal(err.to_string()))?;
1001 }
1002 self.record_queue_wake(&queue_wait_scope(), queue);
1007 self.invalidate_result_cache();
1008
1009 let mut result = UnifiedResult::with_columns(vec![
1010 "message_id".into(),
1011 "side".into(),
1012 "queue".into(),
1013 ]);
1014 let mut record = UnifiedRecord::new();
1015 record.set("message_id", Value::text(message_id_string(id)));
1016 record.set(
1017 "side",
1018 Value::text(match side {
1019 QueueSide::Left => "left".to_string(),
1020 QueueSide::Right => "right".to_string(),
1021 }),
1022 );
1023 record.set("queue", Value::text(queue.clone()));
1024 result.push(record);
1025
1026 Ok(RuntimeQueryResult {
1027 query: raw_query.to_string(),
1028 mode: QueryMode::Sql,
1029 statement: "queue_push",
1030 engine: "runtime-queue",
1031 result,
1032 affected_rows: 1,
1033 statement_type: "insert",
1034 bookmark: None,
1035 })
1036 }
1037 QueueCommand::Pop { queue, side, count } => {
1038 let store = self.inner.db.store();
1039 ensure_queue_exists(store.as_ref(), queue)?;
1040 let (lifecycle, _ps, txn) = runtime_lifecycle(self, queue);
1041 let popped = lifecycle
1042 .pop(queue, ast_side_to_lc(*side), *count, &txn)
1043 .map_err(map_qse)?;
1044
1045 let mut result =
1046 UnifiedResult::with_columns(vec!["message_id".into(), "payload".into()]);
1047 for (message_id, payload) in &popped {
1048 let mut record = UnifiedRecord::new();
1049 record.set(
1050 "message_id",
1051 Value::text(message_id_string(EntityId::new(*message_id))),
1052 );
1053 record.set("payload", payload.clone());
1054 result.push(record);
1055 }
1056 let popped_count = popped.len() as u64;
1057 if popped_count > 0 {
1058 self.invalidate_result_cache();
1059 }
1060
1061 Ok(RuntimeQueryResult {
1062 query: raw_query.to_string(),
1063 mode: QueryMode::Sql,
1064 statement: "queue_pop",
1065 engine: "runtime-queue",
1066 result,
1067 affected_rows: popped_count,
1068 statement_type: "delete",
1069 bookmark: None,
1070 })
1071 }
1072 QueueCommand::Peek { queue, count } => {
1073 let store = self.inner.db.store();
1074 ensure_queue_exists(store.as_ref(), queue)?;
1075 let (lifecycle, _ps, txn) = runtime_lifecycle(self, queue);
1076 let messages = lifecycle.peek(queue, *count, &txn);
1077
1078 let mut result =
1079 UnifiedResult::with_columns(vec!["message_id".into(), "payload".into()]);
1080 for message in messages {
1081 let mut record = UnifiedRecord::new();
1082 record.set(
1083 "message_id",
1084 Value::text(message_id_string(EntityId::new(message.message_id))),
1085 );
1086 record.set("payload", message.payload);
1087 result.push(record);
1088 }
1089
1090 Ok(RuntimeQueryResult {
1091 query: raw_query.to_string(),
1092 mode: QueryMode::Sql,
1093 statement: "queue_peek",
1094 engine: "runtime-queue",
1095 result,
1096 affected_rows: 0,
1097 statement_type: "select",
1098 bookmark: None,
1099 })
1100 }
1101 QueueCommand::Len { queue } => {
1102 let store = self.inner.db.store();
1103 ensure_queue_exists(store.as_ref(), queue)?;
1104 let count =
1105 load_queue_message_views_with_runtime(Some(self), store.as_ref(), queue)?.len()
1106 as u64;
1107 let mut result = UnifiedResult::with_columns(vec!["len".into()]);
1108 let mut record = UnifiedRecord::new();
1109 record.set("len", Value::UnsignedInteger(count));
1110 result.push(record);
1111
1112 Ok(RuntimeQueryResult {
1113 query: raw_query.to_string(),
1114 mode: QueryMode::Sql,
1115 statement: "queue_len",
1116 engine: "runtime-queue",
1117 result,
1118 affected_rows: 0,
1119 statement_type: "select",
1120 bookmark: None,
1121 })
1122 }
1123 QueueCommand::Purge { queue } => {
1124 let store = self.inner.db.store();
1125 ensure_queue_exists(store.as_ref(), queue)?;
1126 let (lifecycle, _ps, txn) = runtime_lifecycle(self, queue);
1127 let count = lifecycle.purge(queue, &txn).map_err(map_qse)?;
1128 if count > 0 {
1129 self.invalidate_result_cache();
1130 }
1131
1132 Ok(RuntimeQueryResult::ok_message(
1133 raw_query.to_string(),
1134 &format!("{count} messages purged from queue '{queue}'"),
1135 "delete",
1136 ))
1137 }
1138 QueueCommand::GroupCreate { queue, group } => {
1139 let store = self.inner.db.store();
1140 ensure_queue_exists(store.as_ref(), queue)?;
1141 if queue_group_exists(store.as_ref(), queue, group)? {
1142 return Ok(RuntimeQueryResult::ok_message(
1143 raw_query.to_string(),
1144 &format!(
1145 "consumer group '{}' already exists on queue '{}'",
1146 group, queue
1147 ),
1148 "create",
1149 ));
1150 }
1151 save_queue_group(store.as_ref(), queue, group)?;
1152 self.invalidate_result_cache();
1153
1154 Ok(RuntimeQueryResult::ok_message(
1155 raw_query.to_string(),
1156 &format!("consumer group '{}' created on queue '{}'", group, queue),
1157 "create",
1158 ))
1159 }
1160 QueueCommand::GroupRead {
1161 queue,
1162 group,
1163 consumer,
1164 count,
1165 wait_ms,
1166 } => {
1167 let store = self.inner.db.store();
1168 ensure_queue_exists(store.as_ref(), queue)?;
1169 if let Some(ms) = *wait_ms {
1175 if self.current_xid().is_some() {
1176 return Err(RedDBError::Query(
1177 "QUEUE READ … WAIT is autocommit-only: refusing to park inside an explicit transaction (BEGIN/COMMIT)"
1178 .to_string(),
1179 ));
1180 }
1181 let cap =
1182 self.config_u64(QUEUE_MAX_WAIT_MS_CONFIG_KEY, QUEUE_MAX_WAIT_MS_DEFAULT);
1183 if ms > cap {
1184 return Err(RedDBError::Query(format!(
1185 "QUEUE READ … WAIT {ms}ms exceeds server cap {QUEUE_MAX_WAIT_MS_CONFIG_KEY} = {cap}ms"
1186 )));
1187 }
1188 }
1189 let config = load_queue_config(store.as_ref(), queue);
1193 let group_owned =
1194 resolve_read_group(store.as_ref(), queue, group.as_deref(), consumer, &config)?;
1195 let group_ref = group_owned.as_str();
1196 let delivered = self
1197 .group_read_with_optional_wait(queue, group_ref, consumer, *count, *wait_ms)?;
1198
1199 {
1203 let lease_count = u32::try_from(delivered.len()).unwrap_or(u32::MAX);
1204 let now_ns = std::time::SystemTime::now()
1205 .duration_since(std::time::UNIX_EPOCH)
1206 .map(|d| d.as_nanos() as u64)
1207 .unwrap_or(0);
1208 self.queue_presence().heartbeat(
1209 queue,
1210 group_ref,
1211 consumer,
1212 lease_count,
1213 now_ns,
1214 );
1215 }
1216
1217 let mut result = UnifiedResult::with_columns(vec![
1218 "message_id".into(),
1219 "payload".into(),
1220 "consumer".into(),
1221 "delivery_count".into(),
1222 "attempts".into(),
1223 ]);
1224
1225 for message in delivered {
1226 let mut record = UnifiedRecord::new();
1227 record.set(
1228 "message_id",
1229 Value::text(message_id_string(EntityId::new(message.message_id))),
1230 );
1231 record.set("payload", message.payload);
1232 record.set("consumer", Value::text(message.consumer));
1233 record.set(
1234 "delivery_count",
1235 Value::UnsignedInteger(u64::from(message.delivery_count)),
1236 );
1237 record.set(
1238 "attempts",
1239 Value::UnsignedInteger(u64::from(message.delivery_count)),
1240 );
1241 result.push(record);
1242 }
1243 if !result.records.is_empty() {
1244 self.invalidate_result_cache();
1245 }
1246
1247 Ok(RuntimeQueryResult {
1248 query: raw_query.to_string(),
1249 mode: QueryMode::Sql,
1250 statement: "queue_group_read",
1251 engine: "runtime-queue",
1252 result,
1253 affected_rows: 0,
1254 statement_type: "select",
1255 bookmark: None,
1256 })
1257 }
1258 QueueCommand::Pending { queue, group } => {
1259 let store = self.inner.db.store();
1260 ensure_queue_exists(store.as_ref(), queue)?;
1261 require_queue_group(store.as_ref(), queue, group)?;
1262 let mut pending = load_pending_entries(store.as_ref(), queue, Some(group), None)?;
1263 pending.sort_by_key(|entry| entry.delivered_at_ns);
1264 let current_time_ns = now_ns();
1265
1266 let mut result = UnifiedResult::with_columns(vec![
1267 "message_id".into(),
1268 "consumer".into(),
1269 "delivered_at_ns".into(),
1270 "delivery_count".into(),
1271 "idle_ms".into(),
1272 ]);
1273 for entry in pending {
1274 let mut record = UnifiedRecord::new();
1275 record.set(
1276 "message_id",
1277 Value::text(message_id_string(entry.message_id)),
1278 );
1279 record.set("consumer", Value::text(entry.consumer));
1280 record.set(
1281 "delivered_at_ns",
1282 Value::UnsignedInteger(entry.delivered_at_ns),
1283 );
1284 record.set(
1285 "delivery_count",
1286 Value::UnsignedInteger(u64::from(entry.delivery_count)),
1287 );
1288 record.set(
1289 "idle_ms",
1290 Value::UnsignedInteger(
1291 current_time_ns.saturating_sub(entry.delivered_at_ns) / 1_000_000,
1292 ),
1293 );
1294 result.push(record);
1295 }
1296
1297 Ok(RuntimeQueryResult {
1298 query: raw_query.to_string(),
1299 mode: QueryMode::Sql,
1300 statement: "queue_pending",
1301 engine: "runtime-queue",
1302 result,
1303 affected_rows: 0,
1304 statement_type: "select",
1305 bookmark: None,
1306 })
1307 }
1308 QueueCommand::Claim {
1309 queue,
1310 group,
1311 consumer,
1312 min_idle_ms,
1313 } => {
1314 let store = self.inner.db.store();
1315 ensure_queue_exists(store.as_ref(), queue)?;
1316 require_queue_group(store.as_ref(), queue, group)?;
1317 let (lifecycle, _ps, txn) = runtime_lifecycle(self, queue);
1318 let delivered = lifecycle
1319 .claim_delivering(queue, consumer, *min_idle_ms, &txn)
1320 .map_err(map_qse)?;
1321
1322 let mut result = UnifiedResult::with_columns(vec![
1323 "message_id".into(),
1324 "delivery_id".into(),
1325 "payload".into(),
1326 "consumer".into(),
1327 "delivery_count".into(),
1328 ]);
1329
1330 for message in delivered {
1331 let mut record = UnifiedRecord::new();
1332 record.set(
1333 "message_id",
1334 Value::text(message_id_string(EntityId::new(message.message_id))),
1335 );
1336 record.set("delivery_id", Value::text(message.delivery_id));
1337 record.set("payload", message.payload);
1338 record.set("consumer", Value::text(message.consumer));
1339 record.set(
1340 "delivery_count",
1341 Value::UnsignedInteger(u64::from(message.delivery_count)),
1342 );
1343 result.push(record);
1344 }
1345 if !result.records.is_empty() {
1346 self.invalidate_result_cache();
1347 }
1348 let affected_rows = result.records.len() as u64;
1349
1350 Ok(RuntimeQueryResult {
1351 query: raw_query.to_string(),
1352 mode: QueryMode::Sql,
1353 statement: "queue_claim",
1354 engine: "runtime-queue",
1355 result,
1356 affected_rows,
1357 statement_type: "update",
1358 bookmark: None,
1359 })
1360 }
1361 QueueCommand::Ack {
1362 queue,
1363 group,
1364 message_id,
1365 delivery_id,
1366 } => {
1367 let store = self.inner.db.store();
1368 ensure_queue_exists(store.as_ref(), queue)?;
1369 let (group_owned, message_entity) = resolve_ack_nack_handle(
1370 store.as_ref(),
1371 queue,
1372 group,
1373 message_id,
1374 delivery_id.as_deref(),
1375 )?;
1376 let group_ref = group_owned.as_str();
1377 require_queue_group(store.as_ref(), queue, group_ref)?;
1378 let (lifecycle, ps, txn) = runtime_lifecycle(self, queue);
1379 let did = match delivery_id.as_deref() {
1380 Some(d) => d.to_string(),
1381 None => ps
1382 .find_pending_by_key(queue, message_entity.raw(), group_ref)
1383 .ok_or_else(|| {
1384 RedDBError::NotFound(format!(
1385 "no pending delivery for message '{}' on queue '{}' (group '{}')",
1386 message_entity.raw(),
1387 queue,
1388 group_ref
1389 ))
1390 })?,
1391 };
1392 lifecycle.ack(&txn, &did).map_err(map_qse)?;
1393 self.invalidate_result_cache();
1394
1395 Ok(RuntimeQueryResult::ok_message(
1396 raw_query.to_string(),
1397 "message acknowledged",
1398 "update",
1399 ))
1400 }
1401 QueueCommand::Nack {
1402 queue,
1403 group,
1404 message_id,
1405 delivery_id,
1406 delay_ms,
1407 } => {
1408 let store = self.inner.db.store();
1409 ensure_queue_exists(store.as_ref(), queue)?;
1410 let config = load_queue_config(store.as_ref(), queue);
1411 if delay_ms.is_some() {
1417 if let Some((_, role)) = current_auth_identity() {
1418 if !role.can_write() {
1419 return Err(RedDBError::InvalidOperation(format!(
1420 "role '{role}' is not authorized to override NACK retry delay on queue '{queue}'"
1421 )));
1422 }
1423 }
1424 }
1425 let (group_owned, message_entity) = resolve_ack_nack_handle(
1426 store.as_ref(),
1427 queue,
1428 group,
1429 message_id,
1430 delivery_id.as_deref(),
1431 )?;
1432 let group_ref = group_owned.as_str();
1433 require_queue_group(store.as_ref(), queue, group_ref)?;
1434 let (lifecycle, ps, txn) = runtime_lifecycle(self, queue);
1435 let did = match delivery_id.as_deref() {
1436 Some(d) => d.to_string(),
1437 None => ps
1438 .find_pending_by_key(queue, message_entity.raw(), group_ref)
1439 .ok_or_else(|| {
1440 RedDBError::NotFound(format!(
1441 "no pending delivery for message '{}' on queue '{}' (group '{}')",
1442 message_entity.raw(),
1443 queue,
1444 group_ref
1445 ))
1446 })?,
1447 };
1448 let effective_delay_ms = delay_ms.or(config.retry_delay_ms).unwrap_or(0);
1452 let pending_attempt = ps.read_pending_attempt(&did).map_err(map_qse)?;
1453 let nack_attempts = pending_attempt.attempts.saturating_add(1);
1454 let outcome = lifecycle.nack(&txn, &did).map_err(map_qse)?;
1455 if matches!(outcome, RetirementOutcome::Requeued) && effective_delay_ms > 0 {
1459 let at_ns =
1460 now_ns().saturating_add(effective_delay_ms.saturating_mul(1_000_000));
1461 set_message_available_at_ns(
1462 store.as_ref(),
1463 queue,
1464 message_entity,
1465 Some(at_ns),
1466 config.ttl_ms,
1467 )?;
1468 }
1469 self.maybe_emit_nack_audit(
1476 queue,
1477 group_ref,
1478 &did,
1479 *delay_ms,
1480 config.retry_delay_ms,
1481 &outcome,
1482 );
1483 let outcome_label = match &outcome {
1484 RetirementOutcome::Requeued => NackOutcomeLabel::Retry,
1485 RetirementOutcome::MovedToDlq(_) => NackOutcomeLabel::Dlq,
1486 RetirementOutcome::Dropped => NackOutcomeLabel::Drop,
1487 };
1488 self.queue_telemetry().record_nacked(
1489 queue,
1490 group_ref,
1491 config.mode.as_str(),
1492 outcome_label,
1493 );
1494 if let RetirementOutcome::MovedToDlq(dlq) = &outcome {
1495 OperatorEvent::QueueDlqPromoted {
1496 queue: queue.to_string(),
1497 group: group_ref.to_string(),
1498 dlq: dlq.clone(),
1499 message_id: pending_attempt.message_id,
1500 attempts: nack_attempts,
1501 reason: format!("lifecycle_nack:{did}"),
1502 }
1503 .emit(self.audit_log());
1504 }
1505 let message = match outcome {
1506 RetirementOutcome::Requeued => {
1507 if effective_delay_ms > 0 {
1508 format!("message requeued (delay={effective_delay_ms}ms)")
1509 } else {
1510 "message requeued".to_string()
1511 }
1512 }
1513 RetirementOutcome::MovedToDlq(dlq) => {
1514 format!("message moved to dead-letter queue '{}'", dlq)
1515 }
1516 RetirementOutcome::Dropped => "message dropped after max attempts".to_string(),
1517 };
1518 self.invalidate_result_cache();
1519
1520 Ok(RuntimeQueryResult::ok_message(
1521 raw_query.to_string(),
1522 &message,
1523 "update",
1524 ))
1525 }
1526 QueueCommand::Move {
1527 source,
1528 destination,
1529 filter,
1530 limit,
1531 } => self.execute_queue_move(raw_query, source, destination, filter.as_ref(), *limit),
1532 }
1533 }
1534
1535 pub fn execute_queue_select(
1536 &self,
1537 raw_query: &str,
1538 query: &QueueSelectQuery,
1539 ) -> RedDBResult<RuntimeQueryResult> {
1540 let store = self.inner.db.store();
1541 ensure_queue_exists(store.as_ref(), &query.queue)?;
1542 let config = load_queue_config(store.as_ref(), &query.queue);
1543 let dlq = queue_is_dead_letter_target(store.as_ref(), &query.queue);
1544 let columns = if query.columns.is_empty() {
1545 queue_projection_default_columns()
1546 } else {
1547 query.columns.clone()
1548 };
1549
1550 let mut messages =
1551 load_queue_message_views_with_runtime(Some(self), store.as_ref(), &query.queue)?;
1552 sort_queue_messages(&mut messages, &config, QueueSide::Left);
1553
1554 let mut result = UnifiedResult::with_columns(columns.clone());
1555 for message in messages {
1556 if query
1557 .filter
1558 .as_ref()
1559 .is_some_and(|filter| !queue_message_matches_filter(&message, dlq, filter))
1560 {
1561 continue;
1562 }
1563 let record = queue_projection_record(&columns, &message, dlq)?;
1564 result.push(record);
1565 if query
1566 .limit
1567 .is_some_and(|limit| result.records.len() >= limit as usize)
1568 {
1569 break;
1570 }
1571 }
1572
1573 Ok(RuntimeQueryResult {
1574 query: raw_query.to_string(),
1575 mode: QueryMode::Sql,
1576 statement: "queue_select",
1577 engine: "runtime-queue",
1578 result,
1579 affected_rows: 0,
1580 statement_type: "select",
1581 bookmark: None,
1582 })
1583 }
1584
1585 fn execute_queue_move(
1586 &self,
1587 raw_query: &str,
1588 source: &str,
1589 destination: &str,
1590 filter: Option<&Filter>,
1591 limit: usize,
1592 ) -> RedDBResult<RuntimeQueryResult> {
1593 if source == destination {
1594 return Err(RedDBError::Query(
1595 "QUEUE MOVE source and destination must be different".to_string(),
1596 ));
1597 }
1598 let store = self.inner.db.store();
1599 ensure_queue_exists(store.as_ref(), source)?;
1600 ensure_queue_exists(store.as_ref(), destination)?;
1601 let source_config = load_queue_config(store.as_ref(), source);
1602 let destination_config = load_queue_config(store.as_ref(), destination);
1603 let source_dlq = queue_is_dead_letter_target(store.as_ref(), source);
1604
1605 let mut messages =
1606 load_queue_message_views_with_runtime(Some(self), store.as_ref(), source)?;
1607 sort_queue_messages(&mut messages, &source_config, QueueSide::Left);
1608 let selected = messages
1609 .into_iter()
1610 .filter(|message| {
1611 filter
1612 .map(|f| queue_message_matches_filter(message, source_dlq, f))
1613 .unwrap_or(true)
1614 })
1615 .take(limit)
1616 .collect::<Vec<_>>();
1617
1618 if let Some(max_size) = destination_config.max_size {
1619 let current_len =
1620 load_queue_message_views_with_runtime(Some(self), store.as_ref(), destination)?
1621 .len();
1622 if current_len + selected.len() > max_size {
1623 return Err(RedDBError::Query(format!(
1624 "queue '{}' is full (max_size={max_size})",
1625 destination
1626 )));
1627 }
1628 }
1629
1630 for message in &selected {
1631 let lock = queue_message_lock_handle(self, source, message.id);
1632 let Some(_guard) = lock.try_lock() else {
1633 return Err(RedDBError::Query(format!(
1634 "message '{}' is locked on queue '{}'",
1635 message.id.raw(),
1636 source
1637 )));
1638 };
1639 if queue_message_view_by_id(store.as_ref(), source, message.id)?.is_none() {
1640 return Err(RedDBError::Query(format!(
1641 "message '{}' is no longer available on queue '{}'",
1642 message.id.raw(),
1643 source
1644 )));
1645 }
1646 }
1647
1648 let mut inserted = Vec::new();
1649 for message in &selected {
1650 match insert_moved_queue_message(
1651 store.as_ref(),
1652 destination,
1653 &destination_config,
1654 message,
1655 ) {
1656 Ok(id) => inserted.push(id),
1657 Err(err) => {
1658 for id in inserted {
1659 let _ = store.delete(destination, id);
1660 }
1661 return Err(err);
1662 }
1663 }
1664 }
1665
1666 let (move_lifecycle, _move_ps, move_txn) = runtime_lifecycle(self, source);
1667 for message in &selected {
1668 move_lifecycle
1669 .delete_with_state(source, message.id.raw(), &move_txn)
1670 .map_err(map_qse)?;
1671 }
1672 if !selected.is_empty() {
1673 self.invalidate_result_cache();
1674 }
1675
1676 let selected_count = selected.len() as u64;
1677 self.audit_log().record_event(
1678 AuditEvent::builder("queue/move")
1679 .source(AuditAuthSource::System)
1680 .outcome(Outcome::Success)
1681 .resource(format!("queue:{source}->{destination}"))
1682 .fields([
1683 AuditFieldEscaper::field("source", source),
1684 AuditFieldEscaper::field("destination", destination),
1685 AuditFieldEscaper::field("selected", selected_count),
1686 AuditFieldEscaper::field("committed", selected_count),
1687 ])
1688 .build(),
1689 );
1690
1691 let mut result = UnifiedResult::with_columns(vec![
1692 "source".into(),
1693 "destination".into(),
1694 "selected".into(),
1695 "committed".into(),
1696 ]);
1697 let mut record = UnifiedRecord::new();
1698 record.set("source", Value::text(source.to_string()));
1699 record.set("destination", Value::text(destination.to_string()));
1700 record.set("selected", Value::UnsignedInteger(selected_count));
1701 record.set("committed", Value::UnsignedInteger(selected_count));
1702 result.push(record);
1703
1704 Ok(RuntimeQueryResult {
1705 query: raw_query.to_string(),
1706 mode: QueryMode::Sql,
1707 statement: "queue_move",
1708 engine: "runtime-queue",
1709 result,
1710 affected_rows: selected_count,
1711 statement_type: "update",
1712 bookmark: None,
1713 })
1714 }
1715
1716 fn maybe_emit_nack_audit(
1731 &self,
1732 queue: &str,
1733 group: &str,
1734 delivery_id: &str,
1735 override_ms: Option<u64>,
1736 default_ms: Option<u64>,
1737 outcome: &RetirementOutcome,
1738 ) {
1739 let Some(override_ms) = override_ms else {
1740 return;
1741 };
1742 let outcome_label = match outcome {
1743 RetirementOutcome::Requeued => "requeued",
1744 RetirementOutcome::MovedToDlq(_) => "dlq",
1745 RetirementOutcome::Dropped => "dropped",
1746 };
1747 const SIGNIFICANT_DELAY_MS: u64 = 60_000;
1748 let destination_changed = !matches!(outcome, RetirementOutcome::Requeued);
1749 if override_ms < SIGNIFICANT_DELAY_MS && !destination_changed {
1750 return;
1751 }
1752 self.audit_log().record_event(
1753 AuditEvent::builder("queue/nack/override")
1754 .source(AuditAuthSource::System)
1755 .outcome(Outcome::Success)
1756 .resource(format!("queue:{queue}"))
1757 .fields([
1758 AuditFieldEscaper::field("queue", queue),
1759 AuditFieldEscaper::field("group", group),
1760 AuditFieldEscaper::field("delivery_id", delivery_id),
1761 AuditFieldEscaper::field("override_delay_ms", override_ms),
1762 AuditFieldEscaper::field("default_delay_ms", default_ms.unwrap_or(0)),
1763 AuditFieldEscaper::field("outcome", outcome_label),
1764 ])
1765 .build(),
1766 );
1767 }
1768
1769 pub(super) fn is_queue_collection(&self, collection: &str) -> bool {
1773 self.db()
1774 .collection_contract_arc(collection)
1775 .map(|contract| contract.declared_model == crate::catalog::CollectionModel::Queue)
1776 .unwrap_or(false)
1777 }
1778
1779 pub(super) fn execute_queue_shaped_claim(
1793 &self,
1794 raw_query: &str,
1795 query: &UpdateQuery,
1796 ) -> RedDBResult<RuntimeQueryResult> {
1797 validate_queue_claim_shape(query)?;
1798
1799 let queue = query.table.as_str();
1800 let store = self.inner.db.store();
1801 ensure_queue_exists(store.as_ref(), queue)?;
1802
1803 let config = load_queue_config(store.as_ref(), queue);
1808 if config.mode != QueueMode::Work {
1809 return Err(RedDBError::InvalidOperation(format!(
1810 "CLAIM on queue '{queue}' cannot be expressed: FANOUT delivery \
1811 must be consumed through GROUP READ, not a queue-shaped CLAIM"
1812 )));
1813 }
1814
1815 let group = resolve_read_group(store.as_ref(), queue, None, "", &config)?;
1819 let count = query.claim_limit.unwrap_or(0) as usize;
1820 let (lifecycle, _ps, txn) = runtime_lifecycle(self, queue);
1821 let delivered = lifecycle
1822 .deliver(&txn, queue, &group, count)
1823 .map_err(map_qse)?;
1824
1825 let mut result = UnifiedResult::with_columns(vec!["delivery_id".into(), "payload".into()]);
1826 for message in &delivered {
1827 let mut record = UnifiedRecord::new();
1828 record.set("delivery_id", Value::text(message.delivery_id.clone()));
1829 record.set("payload", message.payload.clone());
1830 result.push(record);
1831 }
1832 let affected_rows = delivered.len() as u64;
1833 if affected_rows > 0 {
1834 self.invalidate_result_cache();
1835 }
1836
1837 Ok(RuntimeQueryResult {
1838 query: raw_query.to_string(),
1839 mode: QueryMode::Sql,
1840 statement: "queue_claim",
1841 engine: "runtime-queue",
1842 result,
1843 affected_rows,
1844 statement_type: "update",
1845 bookmark: None,
1846 })
1847 }
1848}
1849
1850fn validate_queue_claim_shape(query: &UpdateQuery) -> RedDBResult<()> {
1861 if query.order_by.iter().any(|clause| !clause.ascending) {
1862 return Err(RedDBError::InvalidOperation(format!(
1863 "CLAIM on queue '{}' cannot be expressed: a descending ORDER BY \
1864 conflicts with FIFO queue delivery order",
1865 query.table
1866 )));
1867 }
1868 if query.claim_exact {
1869 return Err(RedDBError::InvalidOperation(format!(
1870 "CLAIM EXACT on queue '{}' cannot be expressed: queue delivery \
1871 acquires up to N available messages, not an exact-or-nothing batch",
1872 query.table
1873 )));
1874 }
1875 Ok(())
1876}
1877
1878fn ensure_queue_exists(store: &UnifiedStore, queue: &str) -> RedDBResult<()> {
1879 if store.get_collection(queue).is_some() {
1880 Ok(())
1881 } else {
1882 Err(RedDBError::NotFound(format!("queue '{}' not found", queue)))
1883 }
1884}
1885
1886pub(super) fn load_queue_config(store: &UnifiedStore, queue: &str) -> QueueRuntimeConfig {
1887 let default = QueueRuntimeConfig {
1888 mode: QueueMode::Work,
1889 priority: false,
1890 max_size: None,
1891 ttl_ms: None,
1892 dlq: None,
1893 max_attempts: crate::storage::query::DEFAULT_QUEUE_MAX_ATTEMPTS,
1894 lock_deadline_ms: crate::storage::query::DEFAULT_QUEUE_LOCK_DEADLINE_MS,
1895 in_flight_cap_per_group: crate::storage::query::DEFAULT_QUEUE_IN_FLIGHT_CAP_PER_GROUP,
1896 retry_delay_ms: None,
1897 };
1898
1899 let Some(manager) = store.get_collection(QUEUE_META_COLLECTION) else {
1900 return default;
1901 };
1902 manager
1903 .query_all(|entity| {
1904 entity.data.as_row().is_some_and(|row| {
1905 row_text(row, "kind").as_deref() == Some("queue_config")
1906 && row_text(row, "queue").as_deref() == Some(queue)
1907 })
1908 })
1909 .into_iter()
1910 .find_map(|entity| {
1911 let row = entity.data.as_row()?;
1912 Some(QueueRuntimeConfig {
1913 mode: row_text(row, "mode")
1914 .as_deref()
1915 .and_then(QueueMode::parse)
1916 .unwrap_or_default(),
1917 priority: row_bool(row, "priority").unwrap_or(false),
1918 max_size: row_u64(row, "max_size").map(|value| value as usize),
1919 ttl_ms: row_u64(row, "ttl_ms"),
1920 dlq: row_text(row, "dlq"),
1921 max_attempts: row_u64(row, "max_attempts")
1922 .map(|value| value as u32)
1923 .unwrap_or(crate::storage::query::DEFAULT_QUEUE_MAX_ATTEMPTS),
1924 lock_deadline_ms: row_u64(row, "lock_deadline_ms")
1925 .unwrap_or(crate::storage::query::DEFAULT_QUEUE_LOCK_DEADLINE_MS),
1926 in_flight_cap_per_group: row_u64(row, "in_flight_cap_per_group")
1927 .map(|value| value as u32)
1928 .unwrap_or(crate::storage::query::DEFAULT_QUEUE_IN_FLIGHT_CAP_PER_GROUP),
1929 retry_delay_ms: row_u64(row, "retry_delay_ms").filter(|v| *v > 0),
1930 })
1931 })
1932 .unwrap_or(default)
1933}
1934
1935pub(super) fn queue_mode_str(store: &UnifiedStore, queue: &str) -> &'static str {
1936 load_queue_config(store, queue).mode.as_str()
1937}
1938
1939fn save_queue_config(
1940 store: &UnifiedStore,
1941 queue: &str,
1942 config: &QueueRuntimeConfig,
1943) -> RedDBResult<()> {
1944 remove_meta_rows(store, |row| {
1945 row_text(row, "kind").as_deref() == Some("queue_config")
1946 && row_text(row, "queue").as_deref() == Some(queue)
1947 });
1948
1949 let mut fields = HashMap::new();
1950 fields.insert("kind".to_string(), Value::text("queue_config".to_string()));
1951 fields.insert("queue".to_string(), Value::text(queue.to_string()));
1952 fields.insert(
1953 "mode".to_string(),
1954 Value::text(config.mode.as_str().to_string()),
1955 );
1956 fields.insert("priority".to_string(), Value::Boolean(config.priority));
1957 fields.insert(
1958 "max_size".to_string(),
1959 config
1960 .max_size
1961 .map(|value| Value::UnsignedInteger(value as u64))
1962 .unwrap_or(Value::Null),
1963 );
1964 fields.insert(
1965 "ttl_ms".to_string(),
1966 config
1967 .ttl_ms
1968 .map(Value::UnsignedInteger)
1969 .unwrap_or(Value::Null),
1970 );
1971 fields.insert(
1972 "dlq".to_string(),
1973 config.dlq.clone().map(Value::text).unwrap_or(Value::Null),
1974 );
1975 fields.insert(
1976 "max_attempts".to_string(),
1977 Value::UnsignedInteger(u64::from(config.max_attempts)),
1978 );
1979 fields.insert(
1980 "lock_deadline_ms".to_string(),
1981 Value::UnsignedInteger(config.lock_deadline_ms),
1982 );
1983 fields.insert(
1984 "in_flight_cap_per_group".to_string(),
1985 Value::UnsignedInteger(u64::from(config.in_flight_cap_per_group)),
1986 );
1987 fields.insert(
1988 "retry_delay_ms".to_string(),
1989 config
1990 .retry_delay_ms
1991 .map(Value::UnsignedInteger)
1992 .unwrap_or(Value::Null),
1993 );
1994 insert_meta_row(store, fields)
1995}
1996
1997fn remove_queue_metadata(store: &UnifiedStore, queue: &str) {
1998 remove_meta_rows(store, |row| {
1999 row_text(row, "queue").as_deref() == Some(queue)
2000 });
2001}
2002
2003fn queue_group_exists(store: &UnifiedStore, queue: &str, group: &str) -> RedDBResult<bool> {
2004 Ok(load_queue_groups(store, queue)?
2005 .into_iter()
2006 .any(|entry| entry.group == group))
2007}
2008
2009pub(super) fn require_queue_group(
2010 store: &UnifiedStore,
2011 queue: &str,
2012 group: &str,
2013) -> RedDBResult<()> {
2014 if queue_group_exists(store, queue, group)? {
2015 Ok(())
2016 } else {
2017 Err(RedDBError::NotFound(format!(
2018 "consumer group '{}' not found on queue '{}'",
2019 group, queue
2020 )))
2021 }
2022}
2023
2024pub(super) fn resolve_read_group(
2025 store: &UnifiedStore,
2026 queue: &str,
2027 group: Option<&str>,
2028 consumer: &str,
2029 config: &QueueRuntimeConfig,
2030) -> RedDBResult<String> {
2031 if let Some(group) = group {
2032 require_queue_group(store, queue, group)?;
2033 return Ok(group.to_string());
2034 }
2035
2036 match config.mode {
2037 QueueMode::Work => {
2038 if !queue_group_exists(store, queue, WORK_DEFAULT_GROUP)? {
2039 save_queue_group(store, queue, WORK_DEFAULT_GROUP)?;
2040 }
2041 Ok(WORK_DEFAULT_GROUP.to_string())
2042 }
2043 QueueMode::Fanout => {
2044 let fanout_group = format!("{FANOUT_GROUP_PREFIX}{consumer}");
2045 if !queue_group_exists(store, queue, &fanout_group)? {
2046 save_queue_group(store, queue, &fanout_group)?;
2047 }
2048 Ok(fanout_group)
2049 }
2050 }
2051}
2052
2053fn load_queue_groups(store: &UnifiedStore, queue: &str) -> RedDBResult<Vec<QueueGroupEntry>> {
2054 let Some(manager) = store.get_collection(QUEUE_META_COLLECTION) else {
2055 return Ok(Vec::new());
2056 };
2057 Ok(manager
2058 .query_all(|entity| {
2059 entity.data.as_row().is_some_and(|row| {
2060 row_text(row, "kind").as_deref() == Some("queue_group")
2061 && row_text(row, "queue").as_deref() == Some(queue)
2062 })
2063 })
2064 .into_iter()
2065 .filter_map(|entity| {
2066 let row = entity.data.as_row()?;
2067 Some(QueueGroupEntry {
2068 entity_id: entity.id,
2069 group: row_text(row, "group")?,
2070 })
2071 })
2072 .collect())
2073}
2074
2075fn save_queue_group(store: &UnifiedStore, queue: &str, group: &str) -> RedDBResult<()> {
2076 let mut fields = HashMap::new();
2077 fields.insert("kind".to_string(), Value::text("queue_group".to_string()));
2078 fields.insert("queue".to_string(), Value::text(queue.to_string()));
2079 fields.insert("group".to_string(), Value::text(group.to_string()));
2080 fields.insert(
2081 "created_at_ns".to_string(),
2082 Value::UnsignedInteger(now_ns()),
2083 );
2084 insert_meta_row(store, fields)
2085}
2086
2087pub(super) fn load_pending_entries(
2088 store: &UnifiedStore,
2089 queue: &str,
2090 group: Option<&str>,
2091 message_id: Option<EntityId>,
2092) -> RedDBResult<Vec<QueuePendingEntry>> {
2093 let Some(manager) = store.get_collection(QUEUE_META_COLLECTION) else {
2094 return Ok(Vec::new());
2095 };
2096 let lock_deadline_ns = load_queue_config(store, queue)
2097 .lock_deadline_ms
2098 .saturating_mul(1_000_000);
2099 let attempts_by_key: HashMap<(String, String, u64), u64> = manager
2100 .query_all(|entity| {
2101 entity.data.as_row().is_some_and(|row| {
2102 row_text(row, "kind").as_deref() == Some("queue_attempts_lc")
2103 && row_text(row, "queue").as_deref() == Some(queue)
2104 })
2105 })
2106 .into_iter()
2107 .filter_map(|entity| {
2108 let row = entity.data.as_row()?;
2109 Some((
2110 (
2111 row_text(row, "queue")?,
2112 row_text(row, "group")?,
2113 row_u64(row, "message_id")?,
2114 ),
2115 row_u64(row, "attempts").unwrap_or(1),
2116 ))
2117 })
2118 .collect();
2119 Ok(manager
2120 .query_all(|entity| {
2121 entity.data.as_row().is_some_and(|row| {
2122 matches!(
2123 row_text(row, "kind").as_deref(),
2124 Some("queue_pending") | Some("queue_pending_lc")
2125 ) && row_text(row, "queue").as_deref() == Some(queue)
2126 && group
2127 .map(|group_name| row_text(row, "group").as_deref() == Some(group_name))
2128 .unwrap_or(true)
2129 && message_id
2130 .map(|candidate| row_u64(row, "message_id") == Some(candidate.raw()))
2131 .unwrap_or(true)
2132 })
2133 })
2134 .into_iter()
2135 .filter_map(|entity| {
2136 let row = entity.data.as_row()?;
2137 let group = row_text(row, "group")?;
2138 let message_id = row_u64(row, "message_id")?;
2139 let kind = row_text(row, "kind")?;
2140 let delivered_at_ns = if kind == "queue_pending_lc" {
2141 row_u64(row, "lock_deadline_ns")
2142 .unwrap_or(0)
2143 .saturating_sub(lock_deadline_ns)
2144 } else {
2145 row_u64(row, "delivered_at_ns")?
2146 };
2147 let delivery_count = if kind == "queue_pending_lc" {
2148 attempts_by_key
2149 .get(&(queue.to_string(), group.clone(), message_id))
2150 .copied()
2151 .unwrap_or(1)
2152 } else {
2153 row_u64(row, "delivery_count").unwrap_or(1)
2154 };
2155 Some(QueuePendingEntry {
2156 entity_id: entity.id,
2157 group,
2158 message_id: EntityId::new(message_id),
2159 consumer: row_text(row, "consumer").unwrap_or_default(),
2160 delivered_at_ns,
2161 delivery_count: delivery_count as u32,
2162 })
2163 })
2164 .collect())
2165}
2166
2167pub(super) fn save_queue_pending(
2168 store: &UnifiedStore,
2169 queue: &str,
2170 group: &str,
2171 message_id: EntityId,
2172 consumer: &str,
2173 delivered_at_ns: u64,
2174 delivery_count: u32,
2175) -> RedDBResult<()> {
2176 remove_meta_rows(store, |row| {
2177 row_text(row, "kind").as_deref() == Some("queue_pending")
2178 && row_text(row, "queue").as_deref() == Some(queue)
2179 && row_text(row, "group").as_deref() == Some(group)
2180 && row_u64(row, "message_id") == Some(message_id.raw())
2181 });
2182
2183 let mut fields = HashMap::new();
2184 fields.insert("kind".to_string(), Value::text("queue_pending".to_string()));
2185 fields.insert("queue".to_string(), Value::text(queue.to_string()));
2186 fields.insert("group".to_string(), Value::text(group.to_string()));
2187 fields.insert(
2188 "message_id".to_string(),
2189 Value::UnsignedInteger(message_id.raw()),
2190 );
2191 fields.insert("consumer".to_string(), Value::text(consumer.to_string()));
2192 fields.insert(
2193 "delivered_at_ns".to_string(),
2194 Value::UnsignedInteger(delivered_at_ns),
2195 );
2196 fields.insert(
2197 "delivery_count".to_string(),
2198 Value::UnsignedInteger(u64::from(delivery_count)),
2199 );
2200 insert_meta_row(store, fields)
2201}
2202
2203pub(super) fn require_pending_entry(
2204 store: &UnifiedStore,
2205 queue: &str,
2206 group: &str,
2207 message_id: EntityId,
2208) -> RedDBResult<QueuePendingEntry> {
2209 load_pending_entries(store, queue, Some(group), Some(message_id))?
2210 .into_iter()
2211 .next()
2212 .ok_or_else(|| {
2213 RedDBError::NotFound(format!(
2214 "message '{}' is not pending in group '{}' on queue '{}'",
2215 message_id.raw(),
2216 group,
2217 queue
2218 ))
2219 })
2220}
2221
2222pub(super) fn load_ack_entries(
2223 store: &UnifiedStore,
2224 queue: &str,
2225 group: Option<&str>,
2226 message_id: Option<EntityId>,
2227) -> RedDBResult<Vec<QueueAckEntry>> {
2228 let Some(manager) = store.get_collection(QUEUE_META_COLLECTION) else {
2229 return Ok(Vec::new());
2230 };
2231 Ok(manager
2232 .query_all(|entity| {
2233 entity.data.as_row().is_some_and(|row| {
2234 row_text(row, "kind").as_deref() == Some("queue_ack")
2235 && row_text(row, "queue").as_deref() == Some(queue)
2236 && group
2237 .map(|group_name| row_text(row, "group").as_deref() == Some(group_name))
2238 .unwrap_or(true)
2239 && message_id
2240 .map(|candidate| row_u64(row, "message_id") == Some(candidate.raw()))
2241 .unwrap_or(true)
2242 })
2243 })
2244 .into_iter()
2245 .filter_map(|entity| {
2246 let row = entity.data.as_row()?;
2247 Some(QueueAckEntry {
2248 entity_id: entity.id,
2249 group: row_text(row, "group")?,
2250 message_id: EntityId::new(row_u64(row, "message_id")?),
2251 })
2252 })
2253 .collect())
2254}
2255
2256pub(super) fn save_queue_ack(
2257 store: &UnifiedStore,
2258 queue: &str,
2259 group: &str,
2260 message_id: EntityId,
2261) -> RedDBResult<()> {
2262 let existing = load_ack_entries(store, queue, Some(group), Some(message_id))?;
2263 if !existing.is_empty() {
2264 return Ok(());
2265 }
2266
2267 let mut fields = HashMap::new();
2268 fields.insert("kind".to_string(), Value::text("queue_ack".to_string()));
2269 fields.insert("queue".to_string(), Value::text(queue.to_string()));
2270 fields.insert("group".to_string(), Value::text(group.to_string()));
2271 fields.insert(
2272 "message_id".to_string(),
2273 Value::UnsignedInteger(message_id.raw()),
2274 );
2275 fields.insert("acked_at_ns".to_string(), Value::UnsignedInteger(now_ns()));
2276 insert_meta_row(store, fields)
2277}
2278
2279pub(super) fn queue_message_completed_for_all_groups(
2280 store: &UnifiedStore,
2281 queue: &str,
2282 message_id: EntityId,
2283) -> RedDBResult<bool> {
2284 let groups = load_queue_groups(store, queue)?;
2285 let pending = load_pending_entries(store, queue, None, Some(message_id))?;
2286 if !pending.is_empty() {
2287 return Ok(false);
2288 }
2289 if groups.is_empty() {
2290 return Ok(true);
2291 }
2292
2293 let acked_groups = load_ack_entries(store, queue, None, Some(message_id))?
2294 .into_iter()
2295 .map(|entry| entry.group)
2296 .collect::<HashSet<_>>();
2297 Ok(groups
2298 .into_iter()
2299 .all(|group| acked_groups.contains(&group.group)))
2300}
2301
2302fn load_queue_message_views(
2303 store: &UnifiedStore,
2304 queue: &str,
2305) -> RedDBResult<Vec<QueueMessageView>> {
2306 load_queue_message_views_with_runtime(None, store, queue)
2307}
2308
2309pub(super) fn load_queue_message_views_with_runtime(
2316 runtime: Option<&RedDBRuntime>,
2317 store: &UnifiedStore,
2318 queue: &str,
2319) -> RedDBResult<Vec<QueueMessageView>> {
2320 let manager = store
2321 .get_collection(queue)
2322 .ok_or_else(|| RedDBError::NotFound(format!("queue '{}' not found", queue)))?;
2323 let snap_ctx = crate::runtime::impl_core::capture_current_snapshot();
2327 let rls_filter = runtime.and_then(|rt| {
2328 crate::runtime::impl_core::rls_policy_filter_for_kind(
2329 rt,
2330 queue,
2331 crate::storage::query::ast::PolicyAction::Select,
2332 crate::storage::query::ast::PolicyTargetKind::Messages,
2333 )
2334 });
2335 let rls_enabled_but_denied = runtime.map(|rt| rt.is_rls_enabled(queue)).unwrap_or(false)
2336 && rls_filter.is_none()
2337 && runtime.is_some();
2338 if rls_enabled_but_denied {
2339 return Ok(Vec::new());
2341 }
2342 let filter_arc = rls_filter.map(std::sync::Arc::new);
2343 let rt_arc = runtime;
2344 Ok(manager
2345 .query_all(move |entity| {
2346 if !matches!(entity.kind, EntityKind::QueueMessage { .. }) {
2347 return false;
2348 }
2349 if !crate::runtime::impl_core::entity_visible_with_context(snap_ctx.as_ref(), entity) {
2350 return false;
2351 }
2352 if let (Some(filter), Some(rt)) = (filter_arc.as_ref(), rt_arc) {
2353 return crate::runtime::query_exec::evaluate_entity_filter_with_db(
2354 Some(&rt.inner.db),
2355 entity,
2356 filter,
2357 queue,
2358 queue,
2359 );
2360 }
2361 true
2362 })
2363 .into_iter()
2364 .filter_map(queue_message_view_from_entity)
2365 .map(|mut view| {
2366 view.available_at_ns = read_message_available_at_ns(store, queue, view.id);
2367 view
2368 })
2369 .collect())
2370}
2371
2372fn queue_message_view_from_entity(entity: UnifiedEntity) -> Option<QueueMessageView> {
2373 let (position, _) = match &entity.kind {
2374 EntityKind::QueueMessage { position, queue } => (*position, queue),
2375 _ => return None,
2376 };
2377 let data = match entity.data {
2378 EntityData::QueueMessage(data) => data,
2379 _ => return None,
2380 };
2381 Some(QueueMessageView {
2382 id: entity.id,
2383 position,
2384 priority: data.priority.unwrap_or(0),
2385 payload: data.payload,
2386 attempts: data.attempts,
2387 max_attempts: data.max_attempts,
2388 enqueued_at_ns: data.enqueued_at_ns,
2389 available_at_ns: None,
2390 })
2391}
2392
2393pub(super) fn insert_moved_queue_message_payload(
2400 store: &UnifiedStore,
2401 queue: &str,
2402 payload: &Value,
2403) -> RedDBResult<EntityId> {
2404 let config = load_queue_config(store, queue);
2405 let position = next_queue_position(store, queue, QueueSide::Right)?;
2406 let enqueued_at_ns = std::time::SystemTime::now()
2407 .duration_since(std::time::UNIX_EPOCH)
2408 .map(|d| d.as_nanos() as u64)
2409 .unwrap_or(0);
2410 let entity = UnifiedEntity::new(
2411 EntityId::new(0),
2412 EntityKind::QueueMessage {
2413 queue: queue.to_string(),
2414 position,
2415 },
2416 EntityData::QueueMessage(QueueMessageData {
2417 payload: payload.clone(),
2418 priority: None,
2419 enqueued_at_ns,
2420 attempts: 0,
2421 max_attempts: config.max_attempts,
2422 acked: false,
2423 }),
2424 );
2425 let id = store
2426 .insert_auto(queue, entity)
2427 .map_err(|err| RedDBError::Internal(err.to_string()))?;
2428 if let Some(ttl_ms) = config.ttl_ms {
2429 store
2430 .set_metadata(queue, id, queue_message_ttl_metadata(ttl_ms))
2431 .map_err(|err| RedDBError::Internal(err.to_string()))?;
2432 }
2433 Ok(id)
2434}
2435
2436fn insert_moved_queue_message(
2437 store: &UnifiedStore,
2438 queue: &str,
2439 config: &QueueRuntimeConfig,
2440 message: &QueueMessageView,
2441) -> RedDBResult<EntityId> {
2442 let position = next_queue_position(store, queue, QueueSide::Right)?;
2443 let entity = UnifiedEntity::new(
2444 EntityId::new(0),
2445 EntityKind::QueueMessage {
2446 queue: queue.to_string(),
2447 position,
2448 },
2449 EntityData::QueueMessage(QueueMessageData {
2450 payload: message.payload.clone(),
2451 priority: if config.priority {
2452 Some(message.priority)
2453 } else {
2454 None
2455 },
2456 enqueued_at_ns: message.enqueued_at_ns,
2457 attempts: message.attempts,
2458 max_attempts: message.max_attempts,
2459 acked: false,
2460 }),
2461 );
2462 let id = store
2463 .insert_auto(queue, entity)
2464 .map_err(|err| RedDBError::Internal(err.to_string()))?;
2465 if let Some(ttl_ms) = config.ttl_ms {
2466 store
2467 .set_metadata(queue, id, queue_message_ttl_metadata(ttl_ms))
2468 .map_err(|err| RedDBError::Internal(err.to_string()))?;
2469 }
2470 Ok(id)
2471}
2472
2473fn queue_projection_default_columns() -> Vec<String> {
2474 [
2475 "id",
2476 "payload",
2477 "priority",
2478 "attempts",
2479 "last_error",
2480 "enqueued_at",
2481 "available_at",
2482 "dlq",
2483 "tenant",
2484 ]
2485 .into_iter()
2486 .map(str::to_string)
2487 .collect()
2488}
2489
2490fn queue_projection_record(
2491 columns: &[String],
2492 message: &QueueMessageView,
2493 dlq: bool,
2494) -> RedDBResult<UnifiedRecord> {
2495 let mut record = UnifiedRecord::new();
2496 for column in columns {
2497 let value = queue_projection_value(message, dlq, column).ok_or_else(|| {
2498 RedDBError::Query(format!("unknown queue projection column '{}'", column))
2499 })?;
2500 record.set(column, value);
2501 }
2502 Ok(record)
2503}
2504
2505fn queue_projection_value(message: &QueueMessageView, dlq: bool, column: &str) -> Option<Value> {
2506 match column {
2507 "id" => Some(Value::text(message_id_string(message.id))),
2508 "payload" => Some(message.payload.clone()),
2509 "priority" => Some(Value::Integer(i64::from(message.priority))),
2510 "attempts" => Some(Value::UnsignedInteger(u64::from(message.attempts))),
2511 "last_error" => Some(Value::Null),
2512 "enqueued_at" => Some(Value::UnsignedInteger(message.enqueued_at_ns)),
2513 "available_at" => Some(Value::UnsignedInteger(
2514 message.available_at_ns.unwrap_or(message.enqueued_at_ns),
2515 )),
2516 "dlq" => Some(Value::Boolean(dlq)),
2517 "tenant" => queue_message_tenant(&message.payload).or(Some(Value::Null)),
2518 _ => None,
2519 }
2520}
2521
2522fn queue_message_tenant(payload: &Value) -> Option<Value> {
2523 let Value::Json(bytes) = payload else {
2524 return None;
2525 };
2526 let json: crate::json::Value = crate::json::from_slice(bytes).ok()?;
2527 json.get("tenant")
2528 .and_then(crate::json::Value::as_str)
2529 .map(|tenant| Value::text(tenant.to_string()))
2530}
2531
2532fn queue_is_dead_letter_target(store: &UnifiedStore, queue: &str) -> bool {
2533 let Some(manager) = store.get_collection(QUEUE_META_COLLECTION) else {
2534 return false;
2535 };
2536 !manager
2537 .query_all(|entity| {
2538 entity.data.as_row().is_some_and(|row| {
2539 row_text(row, "kind").as_deref() == Some("queue_config")
2540 && row_text(row, "dlq").as_deref() == Some(queue)
2541 })
2542 })
2543 .is_empty()
2544}
2545
2546fn queue_message_matches_filter(message: &QueueMessageView, dlq: bool, filter: &Filter) -> bool {
2547 match filter {
2548 Filter::Compare { field, op, value } => queue_filter_field_value(message, dlq, field)
2549 .is_some_and(|candidate| queue_compare_values(&candidate, value, *op)),
2550 Filter::CompareFields { left, op, right } => {
2551 match (
2552 queue_filter_field_value(message, dlq, left),
2553 queue_filter_field_value(message, dlq, right),
2554 ) {
2555 (Some(left), Some(right)) => queue_compare_values(&left, &right, *op),
2556 _ => false,
2557 }
2558 }
2559 Filter::And(left, right) => {
2560 queue_message_matches_filter(message, dlq, left)
2561 && queue_message_matches_filter(message, dlq, right)
2562 }
2563 Filter::Or(left, right) => {
2564 queue_message_matches_filter(message, dlq, left)
2565 || queue_message_matches_filter(message, dlq, right)
2566 }
2567 Filter::Not(inner) => !queue_message_matches_filter(message, dlq, inner),
2568 Filter::IsNull(field) => queue_filter_field_value(message, dlq, field)
2569 .is_none_or(|value| matches!(value, Value::Null)),
2570 Filter::IsNotNull(field) => queue_filter_field_value(message, dlq, field)
2571 .is_some_and(|value| !matches!(value, Value::Null)),
2572 Filter::In { field, values } => {
2573 queue_filter_field_value(message, dlq, field).is_some_and(|candidate| {
2574 values
2575 .iter()
2576 .any(|value| queue_values_equal(&candidate, value))
2577 })
2578 }
2579 Filter::Between { field, low, high } => queue_filter_field_value(message, dlq, field)
2580 .is_some_and(|candidate| {
2581 queue_compare_values(&candidate, low, CompareOp::Ge)
2582 && queue_compare_values(&candidate, high, CompareOp::Le)
2583 }),
2584 Filter::Like { field, pattern } => queue_filter_text(message, dlq, field)
2585 .is_some_and(|value| queue_like_matches(&value, pattern)),
2586 Filter::StartsWith { field, prefix } => {
2587 queue_filter_text(message, dlq, field).is_some_and(|value| value.starts_with(prefix))
2588 }
2589 Filter::EndsWith { field, suffix } => {
2590 queue_filter_text(message, dlq, field).is_some_and(|value| value.ends_with(suffix))
2591 }
2592 Filter::Contains { field, substring } => {
2593 queue_filter_text(message, dlq, field).is_some_and(|value| value.contains(substring))
2594 }
2595 Filter::CompareExpr { .. } => false,
2596 }
2597}
2598
2599fn queue_filter_field_value(
2600 message: &QueueMessageView,
2601 dlq: bool,
2602 field: &FieldRef,
2603) -> Option<Value> {
2604 match field {
2605 FieldRef::TableColumn { table, column } if table.is_empty() => {
2606 queue_projection_value(message, dlq, column)
2607 .or_else(|| queue_payload_field_value(&message.payload, column))
2608 }
2609 FieldRef::TableColumn { column, .. } => queue_projection_value(message, dlq, column)
2610 .or_else(|| queue_payload_field_value(&message.payload, column)),
2611 _ => None,
2612 }
2613}
2614
2615fn queue_payload_field_value(payload: &Value, field: &str) -> Option<Value> {
2616 let Value::Json(bytes) = payload else {
2617 return None;
2618 };
2619 let json: crate::json::Value = crate::json::from_slice(bytes).ok()?;
2620 let value = json.get(field)?;
2621 json_value_to_schema_value(value)
2622}
2623
2624fn json_value_to_schema_value(value: &crate::json::Value) -> Option<Value> {
2625 if matches!(value, crate::json::Value::Null) {
2626 Some(Value::Null)
2627 } else if let Some(value) = value.as_bool() {
2628 Some(Value::Boolean(value))
2629 } else if let Some(value) = value.as_i64() {
2630 Some(Value::Integer(value))
2631 } else if let Some(value) = value.as_u64() {
2632 Some(Value::UnsignedInteger(value))
2633 } else if let Some(value) = value.as_f64() {
2634 Some(Value::Float(value))
2635 } else if let Some(value) = value.as_str() {
2636 Some(Value::text(value.to_string()))
2637 } else {
2638 Some(Value::Json(value.to_string_compact().into_bytes()))
2639 }
2640}
2641
2642fn queue_filter_text(message: &QueueMessageView, dlq: bool, field: &FieldRef) -> Option<String> {
2643 queue_filter_field_value(message, dlq, field).and_then(|value| match value {
2644 Value::Text(value) => Some(value.to_string()),
2645 Value::NodeRef(value) | Value::EdgeRef(value) | Value::TableRef(value) => Some(value),
2646 Value::Integer(value) => Some(value.to_string()),
2647 Value::UnsignedInteger(value) => Some(value.to_string()),
2648 Value::Float(value) => Some(value.to_string()),
2649 Value::Boolean(value) => Some(value.to_string()),
2650 _ => None,
2651 })
2652}
2653
2654fn queue_compare_values(left: &Value, right: &Value, op: CompareOp) -> bool {
2655 match op {
2656 CompareOp::Eq => queue_values_equal(left, right),
2657 CompareOp::Ne => !queue_values_equal(left, right),
2658 CompareOp::Lt => queue_partial_cmp(left, right).is_some_and(|ord| ord.is_lt()),
2659 CompareOp::Le => queue_partial_cmp(left, right).is_some_and(|ord| !ord.is_gt()),
2660 CompareOp::Gt => queue_partial_cmp(left, right).is_some_and(|ord| ord.is_gt()),
2661 CompareOp::Ge => queue_partial_cmp(left, right).is_some_and(|ord| !ord.is_lt()),
2662 }
2663}
2664
2665fn queue_values_equal(left: &Value, right: &Value) -> bool {
2666 if let (Some(left), Some(right)) = (queue_value_number(left), queue_value_number(right)) {
2667 return (left - right).abs() < f64::EPSILON;
2668 }
2669 match (left, right) {
2670 (Value::Text(left), Value::Text(right)) => left == right,
2671 (Value::Boolean(left), Value::Boolean(right)) => left == right,
2672 _ => left == right,
2673 }
2674}
2675
2676fn queue_partial_cmp(left: &Value, right: &Value) -> Option<std::cmp::Ordering> {
2677 if let (Some(left), Some(right)) = (queue_value_number(left), queue_value_number(right)) {
2678 return left.partial_cmp(&right);
2679 }
2680 match (left, right) {
2681 (Value::Text(left), Value::Text(right)) => Some(left.cmp(right)),
2682 _ => None,
2683 }
2684}
2685
2686fn queue_value_number(value: &Value) -> Option<f64> {
2687 match value {
2688 Value::Integer(value) => Some(*value as f64),
2689 Value::UnsignedInteger(value) => Some(*value as f64),
2690 Value::Float(value) => Some(*value),
2691 Value::Text(value) => value.parse().ok(),
2692 _ => None,
2693 }
2694}
2695
2696fn queue_like_matches(value: &str, pattern: &str) -> bool {
2697 if pattern == "%" {
2698 return true;
2699 }
2700 let starts_wild = pattern.starts_with('%');
2701 let ends_wild = pattern.ends_with('%');
2702 let needle = pattern.trim_matches('%');
2703 match (starts_wild, ends_wild) {
2704 (true, true) => value.contains(needle),
2705 (true, false) => value.ends_with(needle),
2706 (false, true) => value.starts_with(needle),
2707 (false, false) => value == needle,
2708 }
2709}
2710
2711pub(super) fn queue_message_view_by_id(
2712 store: &UnifiedStore,
2713 queue: &str,
2714 message_id: EntityId,
2715) -> RedDBResult<Option<QueueMessageView>> {
2716 let manager = queue_manager(store, queue)?;
2717 Ok(manager
2718 .get(message_id)
2719 .and_then(queue_message_view_from_entity)
2720 .map(|mut view| {
2721 view.available_at_ns = read_message_available_at_ns(store, queue, view.id);
2722 view
2723 }))
2724}
2725
2726pub(super) fn sort_queue_messages(
2727 messages: &mut [QueueMessageView],
2728 config: &QueueRuntimeConfig,
2729 side: QueueSide,
2730) {
2731 messages.sort_by(|left, right| {
2732 if config.priority {
2733 right
2734 .priority
2735 .cmp(&left.priority)
2736 .then_with(|| match side {
2737 QueueSide::Left => left.position.cmp(&right.position),
2738 QueueSide::Right => right.position.cmp(&left.position),
2739 })
2740 .then_with(|| left.id.raw().cmp(&right.id.raw()))
2741 } else {
2742 match side {
2743 QueueSide::Left => left.position.cmp(&right.position),
2744 QueueSide::Right => right.position.cmp(&left.position),
2745 }
2746 .then_with(|| left.id.raw().cmp(&right.id.raw()))
2747 }
2748 });
2749}
2750
2751pub(super) fn next_queue_position(
2752 store: &UnifiedStore,
2753 queue: &str,
2754 side: QueueSide,
2755) -> RedDBResult<u64> {
2756 let messages = load_queue_message_views(store, queue)?;
2757 if messages.is_empty() {
2758 return Ok(QUEUE_POSITION_CENTER);
2759 }
2760 match side {
2761 QueueSide::Left => Ok(messages
2762 .iter()
2763 .map(|message| message.position)
2764 .min()
2765 .unwrap_or(QUEUE_POSITION_CENTER)
2766 .saturating_sub(1)),
2767 QueueSide::Right => Ok(messages
2768 .iter()
2769 .map(|message| message.position)
2770 .max()
2771 .unwrap_or(QUEUE_POSITION_CENTER)
2772 .saturating_add(1)),
2773 }
2774}
2775
2776pub(super) fn increment_queue_attempts(
2777 store: &UnifiedStore,
2778 queue: &str,
2779 message_id: EntityId,
2780) -> RedDBResult<u32> {
2781 let manager = queue_manager(store, queue)?;
2782 let mut entity = manager
2783 .get(message_id)
2784 .ok_or_else(|| RedDBError::NotFound(format!("message '{}' not found", message_id.raw())))?;
2785 match &mut entity.data {
2786 EntityData::QueueMessage(message) => {
2787 message.attempts = message.attempts.saturating_add(1);
2788 let attempts = message.attempts;
2789 manager
2790 .update(entity)
2791 .map_err(|err| RedDBError::Internal(err.to_string()))?;
2792 Ok(attempts)
2793 }
2794 _ => Err(RedDBError::Query(format!(
2795 "entity '{}' is not a queue message",
2796 message_id.raw()
2797 ))),
2798 }
2799}
2800
2801pub(super) fn queue_message_attempts(
2802 store: &UnifiedStore,
2803 queue: &str,
2804 message_id: EntityId,
2805) -> RedDBResult<u32> {
2806 Ok(queue_message_data(store, queue, message_id)?.attempts)
2807}
2808
2809pub(super) fn queue_message_max_attempts(
2810 store: &UnifiedStore,
2811 queue: &str,
2812 message_id: EntityId,
2813) -> RedDBResult<u32> {
2814 Ok(queue_message_data(store, queue, message_id)?.max_attempts)
2815}
2816
2817pub(super) fn queue_message_payload(
2818 store: &UnifiedStore,
2819 queue: &str,
2820 message_id: EntityId,
2821) -> RedDBResult<Value> {
2822 Ok(queue_message_data(store, queue, message_id)?.payload)
2823}
2824
2825pub(super) fn queue_message_pending_any(
2826 store: &UnifiedStore,
2827 queue: &str,
2828 message_id: EntityId,
2829) -> RedDBResult<bool> {
2830 Ok(!load_pending_entries(store, queue, None, Some(message_id))?.is_empty())
2831}
2832
2833pub(super) fn queue_message_pending_for_group(
2834 store: &UnifiedStore,
2835 queue: &str,
2836 group: &str,
2837 message_id: EntityId,
2838) -> RedDBResult<bool> {
2839 Ok(!load_pending_entries(store, queue, Some(group), Some(message_id))?.is_empty())
2840}
2841
2842pub(super) fn queue_message_acked_for_group(
2843 store: &UnifiedStore,
2844 queue: &str,
2845 group: &str,
2846 message_id: EntityId,
2847) -> RedDBResult<bool> {
2848 Ok(!load_ack_entries(store, queue, Some(group), Some(message_id))?.is_empty())
2849}
2850
2851fn queue_manager(
2852 store: &UnifiedStore,
2853 queue: &str,
2854) -> RedDBResult<Arc<crate::storage::unified::SegmentManager>> {
2855 store
2856 .get_collection(queue)
2857 .ok_or_else(|| RedDBError::NotFound(format!("queue '{}' not found", queue)))
2858}
2859
2860pub(super) fn queue_message_data(
2861 store: &UnifiedStore,
2862 queue: &str,
2863 message_id: EntityId,
2864) -> RedDBResult<QueueMessageData> {
2865 let manager = queue_manager(store, queue)?;
2866 let entity = manager
2867 .get(message_id)
2868 .ok_or_else(|| RedDBError::NotFound(format!("message '{}' not found", message_id.raw())))?;
2869 match entity.data {
2870 EntityData::QueueMessage(message) => Ok(message),
2871 _ => Err(RedDBError::Query(format!(
2872 "entity '{}' is not a queue message",
2873 message_id.raw()
2874 ))),
2875 }
2876}
2877
2878fn insert_meta_row(store: &UnifiedStore, fields: HashMap<String, Value>) -> RedDBResult<()> {
2879 let _ = store.get_or_create_collection(QUEUE_META_COLLECTION);
2880 store
2881 .insert_auto(
2882 QUEUE_META_COLLECTION,
2883 UnifiedEntity::new(
2884 EntityId::new(0),
2885 EntityKind::TableRow {
2886 table: Arc::from(QUEUE_META_COLLECTION),
2887 row_id: 0,
2888 },
2889 EntityData::Row(RowData {
2890 columns: Vec::new(),
2891 named: Some(fields),
2892 schema: None,
2893 }),
2894 ),
2895 )
2896 .map_err(|err| RedDBError::Internal(err.to_string()))?;
2897 Ok(())
2898}
2899
2900pub(super) fn remove_meta_rows(store: &UnifiedStore, predicate: impl Fn(&RowData) -> bool + Sync) {
2901 let Some(manager) = store.get_collection(QUEUE_META_COLLECTION) else {
2902 return;
2903 };
2904 let rows = manager.query_all(|entity| entity.data.as_row().is_some_and(&predicate));
2905 for row in rows {
2906 let _ = store.delete(QUEUE_META_COLLECTION, row.id);
2907 }
2908}
2909
2910pub(super) fn delete_meta_entity(store: &UnifiedStore, entity_id: EntityId) {
2911 let _ = store.delete(QUEUE_META_COLLECTION, entity_id);
2912}
2913
2914fn queue_message_lock_key(queue: &str, message_id: EntityId) -> String {
2915 format!("{queue}:{}", message_id.raw())
2916}
2917
2918pub(super) fn queue_message_lock_handle(
2919 runtime: &RedDBRuntime,
2920 queue: &str,
2921 message_id: EntityId,
2922) -> Arc<parking_lot::Mutex<()>> {
2923 let key = queue_message_lock_key(queue, message_id);
2924 if let Some(lock) = runtime.inner.queue_message_locks.read().get(&key).cloned() {
2925 return lock;
2926 }
2927
2928 let mut locks = runtime.inner.queue_message_locks.write();
2929 locks
2930 .entry(key)
2931 .or_insert_with(|| Arc::new(parking_lot::Mutex::new(())))
2932 .clone()
2933}
2934
2935pub(super) fn forget_queue_message_lock(runtime: &RedDBRuntime, queue: &str, message_id: EntityId) {
2936 runtime
2937 .inner
2938 .queue_message_locks
2939 .write()
2940 .remove(&queue_message_lock_key(queue, message_id));
2941}
2942
2943fn parse_message_id(value: &str) -> RedDBResult<EntityId> {
2944 let raw = value.strip_prefix('e').unwrap_or(value);
2945 raw.parse::<u64>()
2946 .map(EntityId::new)
2947 .map_err(|_| RedDBError::Query(format!("invalid message id '{}'", value)))
2948}
2949
2950pub(super) fn resolve_ack_nack_handle(
2956 store: &UnifiedStore,
2957 queue: &str,
2958 group_hint: &str,
2959 message_id_hint: &str,
2960 delivery_id: Option<&str>,
2961) -> RedDBResult<(String, EntityId)> {
2962 if let Some(did) = delivery_id {
2963 return resolve_delivery_id(store, queue, did);
2964 }
2965 if group_hint.is_empty() || message_id_hint.is_empty() {
2966 return Err(RedDBError::Query(
2967 "ACK/NACK requires either GROUP <group> '<message_id>' or WITH delivery_id = '<id>'"
2968 .to_string(),
2969 ));
2970 }
2971 log_tuple_deprecation(queue);
2972 let entity = parse_message_id(message_id_hint)?;
2973 Ok((group_hint.to_string(), entity))
2974}
2975
2976fn resolve_delivery_id(
2977 store: &UnifiedStore,
2978 queue: &str,
2979 delivery_id: &str,
2980) -> RedDBResult<(String, EntityId)> {
2981 let Some(manager) = store.get_collection(QUEUE_META_COLLECTION) else {
2982 return Err(RedDBError::Query(format!(
2983 "delivery_id '{}' does not resolve to a live pending delivery",
2984 delivery_id
2985 )));
2986 };
2987 for entity in manager.query_all(|entity| {
2988 entity.data.as_row().is_some_and(|row| {
2989 row_text(row, "kind").as_deref() == Some("queue_pending_lc")
2990 && row_text(row, "delivery_id").as_deref() == Some(delivery_id)
2991 })
2992 }) {
2993 if let Some(row) = entity.data.as_row() {
2994 let row_queue = row_text(row, "queue").unwrap_or_default();
2995 let row_group = row_text(row, "group").unwrap_or_default();
2996 let row_message = row_u64(row, "message_id").unwrap_or(0);
2997 if row_queue != queue {
2998 return Err(RedDBError::Query(format!(
2999 "delivery_id '{}' belongs to queue '{}', not '{}'",
3000 delivery_id, row_queue, queue
3001 )));
3002 }
3003 return Ok((row_group, EntityId::new(row_message)));
3004 }
3005 }
3006 Err(RedDBError::Query(format!(
3007 "delivery_id '{}' does not resolve to a live pending delivery",
3008 delivery_id
3009 )))
3010}
3011
3012fn log_tuple_deprecation(queue: &str) {
3015 use std::sync::atomic::Ordering;
3016 use std::sync::{Mutex, OnceLock};
3017 use std::time::Instant;
3018
3019 static LAST_EMIT: OnceLock<Mutex<HashMap<(u64, String), Instant>>> = OnceLock::new();
3020 const COOLDOWN: std::time::Duration = std::time::Duration::from_secs(60);
3021
3022 let map = LAST_EMIT.get_or_init(|| Mutex::new(HashMap::new()));
3023 let key = (super::impl_core::current_connection_id(), queue.to_string());
3024 let now = Instant::now();
3025 let mut guard = match map.lock() {
3026 Ok(g) => g,
3027 Err(_) => return,
3028 };
3029 let should_emit =
3030 !matches!(guard.get(&key), Some(prev) if now.duration_since(*prev) < COOLDOWN);
3031 if should_emit {
3032 guard.insert(key.clone(), now);
3033 drop(guard);
3034 TUPLE_DEPRECATION_EMITS.fetch_add(1, Ordering::Relaxed);
3035 tracing::warn!(
3036 target: "reddb::queue_lifecycle",
3037 queue = queue,
3038 connection_id = key.0,
3039 "ACK/NACK by (queue, group, message_id) tuple is deprecated; \
3040 switch to the server-issued delivery_id (ADR 0026). \
3041 The tuple path will be removed one minor release after introduction.",
3042 );
3043 }
3044}
3045
3046pub static TUPLE_DEPRECATION_EMITS: std::sync::atomic::AtomicU64 =
3052 std::sync::atomic::AtomicU64::new(0);
3053
3054fn message_id_string(message_id: EntityId) -> String {
3055 message_id.raw().to_string()
3056}
3057
3058fn delivered_message_json(
3064 message: crate::runtime::queue_lifecycle::DeliveredMessage,
3065) -> crate::serde_json::Value {
3066 use crate::serde_json::{Map, Value as JsonValue};
3067 let mut obj = Map::new();
3068 obj.insert(
3069 "message_id".to_string(),
3070 JsonValue::String(message_id_string(EntityId::new(message.message_id))),
3071 );
3072 obj.insert(
3073 "payload".to_string(),
3074 crate::presentation::entity_json::storage_value_to_json(&message.payload),
3075 );
3076 obj.insert("consumer".to_string(), JsonValue::String(message.consumer));
3077 obj.insert(
3078 "delivery_count".to_string(),
3079 JsonValue::Number(message.delivery_count as f64),
3080 );
3081 JsonValue::Object(obj)
3082}
3083
3084pub(crate) fn pending_counts_by_group(
3089 store: &UnifiedStore,
3090) -> std::collections::BTreeMap<(String, String), u64> {
3091 let mut counts: std::collections::BTreeMap<(String, String), u64> =
3092 std::collections::BTreeMap::new();
3093 let Some(manager) = store.get_collection(QUEUE_META_COLLECTION) else {
3094 return counts;
3095 };
3096 for entity in manager.query_all(|entity| {
3097 entity
3098 .data
3099 .as_row()
3100 .is_some_and(|row| row_text(row, "kind").as_deref() == Some("queue_pending"))
3101 }) {
3102 if let Some(row) = entity.data.as_row() {
3103 let queue = row_text(row, "queue");
3104 let group = row_text(row, "group");
3105 if let (Some(q), Some(g)) = (queue, group) {
3106 *counts.entry((q, g)).or_insert(0) += 1;
3107 }
3108 }
3109 }
3110 counts
3111}
3112
3113pub(super) fn row_text(row: &RowData, field: &str) -> Option<String> {
3114 match row.get_field(field)?.clone() {
3115 Value::Text(value) => Some(value.to_string()),
3116 Value::NodeRef(value) => Some(value),
3117 Value::EdgeRef(value) => Some(value),
3118 Value::TableRef(value) => Some(value),
3119 _ => None,
3120 }
3121}
3122
3123pub(super) fn row_u64(row: &RowData, field: &str) -> Option<u64> {
3124 match row.get_field(field)?.clone() {
3125 Value::UnsignedInteger(value) => Some(value),
3126 Value::Integer(value) if value >= 0 => Some(value as u64),
3127 Value::Float(value) if value >= 0.0 => Some(value as u64),
3128 Value::Text(value) => value.parse().ok(),
3129 _ => None,
3130 }
3131}
3132
3133fn row_bool(row: &RowData, field: &str) -> Option<bool> {
3134 match row.get_field(field)?.clone() {
3135 Value::Boolean(value) => Some(value),
3136 Value::Text(value) => match value.to_ascii_lowercase().as_str() {
3137 "true" => Some(true),
3138 "false" => Some(false),
3139 _ => None,
3140 },
3141 _ => None,
3142 }
3143}
3144
3145fn queue_collection_contract(
3146 name: &str,
3147 priority: bool,
3148 ttl_ms: Option<u64>,
3149) -> crate::physical::CollectionContract {
3150 let now = current_unix_ms();
3151 let mut context_index_fields = Vec::new();
3152 if priority {
3153 context_index_fields.push("priority".to_string());
3154 }
3155
3156 crate::physical::CollectionContract {
3157 name: name.to_string(),
3158 declared_model: crate::catalog::CollectionModel::Queue,
3159 schema_mode: crate::catalog::SchemaMode::Dynamic,
3160 origin: crate::physical::ContractOrigin::Explicit,
3161 version: 1,
3162 created_at_unix_ms: now,
3163 updated_at_unix_ms: now,
3164 default_ttl_ms: ttl_ms,
3165 vector_dimension: None,
3166 vector_metric: None,
3167 context_index_fields,
3168 declared_columns: Vec::new(),
3169 table_def: None,
3170 timestamps_enabled: false,
3171 context_index_enabled: false,
3172 metrics_raw_retention_ms: None,
3173 metrics_rollup_policies: Vec::new(),
3174 metrics_tenant_identity: None,
3175 metrics_namespace: None,
3176 append_only: true,
3180 subscriptions: Vec::new(),
3181 analytics_config: Vec::new(),
3182 session_key: None,
3183 session_gap_ms: None,
3184 retention_duration_ms: None,
3185 analytical_storage: None,
3186
3187 ai_policy: None,
3188 }
3189}
3190
3191fn current_unix_ms() -> u128 {
3192 std::time::SystemTime::now()
3193 .duration_since(std::time::UNIX_EPOCH)
3194 .unwrap_or_default()
3195 .as_millis()
3196}
3197
3198pub(super) fn now_ns() -> u64 {
3199 std::time::SystemTime::now()
3200 .duration_since(std::time::UNIX_EPOCH)
3201 .unwrap_or_default()
3202 .as_nanos() as u64
3203}
3204
3205pub(super) fn queue_message_ttl_metadata(ttl_ms: u64) -> Metadata {
3206 queue_message_metadata(Some(ttl_ms), None)
3207}
3208
3209pub(super) fn queue_message_metadata(
3215 ttl_ms: Option<u64>,
3216 available_at_ns: Option<u64>,
3217) -> Metadata {
3218 let mut fields = HashMap::new();
3219 if let Some(ttl_ms) = ttl_ms {
3220 fields.insert(
3221 "_ttl_ms".to_string(),
3222 if ttl_ms <= i64::MAX as u64 {
3223 MetadataValue::Int(ttl_ms as i64)
3224 } else {
3225 MetadataValue::Timestamp(ttl_ms)
3226 },
3227 );
3228 }
3229 if let Some(at_ns) = available_at_ns {
3230 fields.insert(
3231 "_available_at_ns".to_string(),
3232 MetadataValue::Timestamp(at_ns),
3233 );
3234 }
3235 Metadata::with_fields(fields)
3236}
3237
3238pub(super) fn earliest_future_available_at(store: &UnifiedStore, queue: &str) -> Option<u64> {
3246 let now_ns = now_ns();
3247 let views = load_queue_message_views_with_runtime(None, store, queue).ok()?;
3248 views
3249 .iter()
3250 .filter_map(|v| v.available_at_ns)
3251 .filter(|at| *at > now_ns)
3252 .min()
3253}
3254
3255pub(super) fn set_message_available_at_ns(
3266 store: &UnifiedStore,
3267 queue: &str,
3268 message_id: EntityId,
3269 available_at_ns: Option<u64>,
3270 fallback_ttl_ms: Option<u64>,
3271) -> RedDBResult<()> {
3272 let existing_ttl_ms = store
3273 .get_metadata(queue, message_id)
3274 .and_then(|md| match md.get("_ttl_ms")? {
3275 MetadataValue::Int(i) if *i >= 0 => Some(*i as u64),
3276 MetadataValue::Timestamp(t) => Some(*t),
3277 _ => None,
3278 })
3279 .or(fallback_ttl_ms);
3280 let metadata = queue_message_metadata(existing_ttl_ms, available_at_ns);
3281 match store.set_metadata(queue, message_id, metadata) {
3282 Ok(()) => Ok(()),
3283 Err(crate::storage::StoreError::CollectionNotFound(_)) => Ok(()),
3284 Err(err) => Err(RedDBError::Internal(err.to_string())),
3285 }
3286}
3287
3288pub(super) fn read_message_available_at_ns(
3292 store: &UnifiedStore,
3293 queue: &str,
3294 message_id: EntityId,
3295) -> Option<u64> {
3296 let md = store.get_metadata(queue, message_id)?;
3297 match md.get("_available_at_ns")? {
3298 MetadataValue::Timestamp(t) => Some(*t),
3299 MetadataValue::Int(i) if *i >= 0 => Some(*i as u64),
3300 _ => None,
3301 }
3302}
3303
3304fn estimate_payload_bytes(payload: &Value) -> u64 {
3306 match payload {
3307 Value::Json(v) => v.len() as u64,
3308 Value::Text(s) => s.len() as u64,
3309 _ => 64,
3310 }
3311}
3312
3313#[cfg(test)]
3314mod presence_integration_tests {
3315 use super::*;
3316 use crate::storage::queue::presence::{PresenceState, DEFAULT_PRESENCE_TTL_MS};
3317 use crate::{RedDBOptions, RedDBRuntime};
3318
3319 #[test]
3323 fn queue_read_emits_consumer_presence_heartbeat() {
3324 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3325 rt.execute_query("CREATE QUEUE tasks").unwrap();
3326 rt.execute_query("QUEUE GROUP CREATE tasks workers")
3327 .unwrap();
3328 rt.execute_query("QUEUE PUSH tasks {'job':'a'}").unwrap();
3329 rt.execute_query("QUEUE READ tasks GROUP workers CONSUMER w1")
3330 .unwrap();
3331
3332 let snap = rt.queue_consumer_presence_snapshot(DEFAULT_PRESENCE_TTL_MS);
3333 assert_eq!(snap.len(), 1, "exactly one heartbeat recorded");
3334 let row = &snap[0];
3335 assert_eq!(row.queue, "tasks");
3336 assert_eq!(row.group, "workers");
3337 assert_eq!(row.consumer, "w1");
3338 assert_eq!(row.state, PresenceState::Active);
3339
3340 let counts = rt.queue_active_consumer_counts(DEFAULT_PRESENCE_TTL_MS);
3341 assert_eq!(
3342 counts[&("tasks".to_string(), "workers".to_string())],
3343 1,
3344 "active count reflects the live consumer"
3345 );
3346 }
3347
3348 #[test]
3352 fn empty_queue_read_still_heartbeats() {
3353 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3354 rt.execute_query("CREATE QUEUE empty_q").unwrap();
3355 rt.execute_query("QUEUE GROUP CREATE empty_q workers")
3356 .unwrap();
3357 rt.execute_query("QUEUE READ empty_q GROUP workers CONSUMER w1")
3359 .unwrap();
3360
3361 let snap = rt.queue_consumer_presence_snapshot(DEFAULT_PRESENCE_TTL_MS);
3362 assert_eq!(
3363 snap.len(),
3364 1,
3365 "empty read still registers consumer presence"
3366 );
3367 assert_eq!(snap[0].state, PresenceState::Active);
3368 assert_eq!(
3369 snap[0].lease_count, 0,
3370 "no messages delivered → zero leases"
3371 );
3372 }
3373}