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,
10 current_connection_id, current_tenant, set_current_auth_principal, 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 = "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 auth_principal: &Option<crate::auth::UserId>,
97 tenant: &Option<String>,
98 f: impl FnOnce() -> T,
99) -> T {
100 let previous_auth = current_auth_identity();
101 let previous_principal = super::execution_context::current_auth_principal();
102 let previous_tenant = current_tenant();
103 match tenant {
104 Some(t) => set_current_tenant(t.clone()),
105 None => clear_current_tenant(),
106 }
107 match (auth_identity, auth_principal) {
108 (Some((_, role)), Some(principal)) => set_current_auth_principal(principal.clone(), *role),
109 _ => clear_current_auth_identity(),
110 }
111 let result = f();
112 match previous_tenant {
113 Some(t) => set_current_tenant(t),
114 None => clear_current_tenant(),
115 }
116 match (previous_auth, previous_principal) {
117 (Some((_, role)), Some(principal)) => set_current_auth_principal(principal, role),
118 _ => clear_current_auth_identity(),
119 }
120 result
121}
122
123fn ast_side_to_lc(side: reddb_rql::ast::QueueSide) -> LcQueueSide {
127 use reddb_rql::ast::QueueSide as Ast;
128 match side {
129 Ast::Left => LcQueueSide::Left,
130 Ast::Right => LcQueueSide::Right,
131 }
132}
133
134fn map_qse(err: QueueStoreError) -> RedDBError {
137 match err {
138 QueueStoreError::UnknownDelivery(id) => RedDBError::NotFound(format!(
139 "delivery_id '{id}' does not resolve to a live pending delivery"
140 )),
141 QueueStoreError::UnknownQueue(q) => RedDBError::NotFound(format!("queue '{q}' not found")),
142 }
143}
144
145pub static EVENTS_DRAIN_RETRIES_TOTAL: AtomicU64 = AtomicU64::new(0);
152
153pub static EVENTS_DLQ_TOTAL: AtomicU64 = AtomicU64::new(0);
155
156pub static EVENTS_ENQUEUED_TOTAL: AtomicU64 = AtomicU64::new(0);
158
159const OUTBOX_WARN_BYTES: u64 = 1 << 30;
161
162const OUTBOX_MAX_BYTES: u64 = 10 * (1 << 30);
164
165static OUTBOX_APPROX_BYTES: AtomicU64 = AtomicU64::new(0);
167
168pub(super) const QUEUE_META_COLLECTION: &str = "red.queue.meta";
169pub(super) const KIND_QUEUE_PUSH_DEDUP: &str = "queue_push_dedup";
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 pub(super) dedup_window_ms: Option<u64>,
192}
193
194#[derive(Debug, Clone)]
195struct QueueGroupEntry {
196 entity_id: EntityId,
197 group: String,
198}
199
200#[derive(Debug, Clone)]
201pub(super) struct QueuePendingEntry {
202 pub(super) entity_id: EntityId,
203 group: String,
204 pub(super) message_id: EntityId,
205 consumer: String,
206 pub(super) delivered_at_ns: u64,
207 pub(super) delivery_count: u32,
208}
209
210#[derive(Debug, Clone)]
211pub(super) struct QueueAckEntry {
212 entity_id: EntityId,
213 group: String,
214 pub(super) message_id: EntityId,
215}
216
217#[derive(Debug, Clone)]
218pub(super) struct QueueMessageView {
219 pub(super) id: EntityId,
220 position: u64,
221 priority: i32,
222 pub(super) payload: Value,
223 attempts: u32,
224 pub(super) max_attempts: u32,
225 enqueued_at_ns: u64,
226 pub(super) available_at_ns: Option<u64>,
230 pub(super) ordering_key: Option<String>,
232}
233
234impl QueueMessageView {
235 pub(super) fn is_available_now(&self) -> bool {
240 match self.available_at_ns {
241 Some(at) => at <= now_ns(),
242 None => true,
243 }
244 }
245}
246
247impl RedDBRuntime {
248 pub(super) fn group_read_with_optional_wait(
257 &self,
258 queue: &str,
259 group: &str,
260 consumer: &str,
261 count: usize,
262 wait_ms: Option<u64>,
263 ) -> RedDBResult<Vec<crate::runtime::queue_lifecycle::DeliveredMessage>> {
264 let do_read =
265 |runtime: &RedDBRuntime| -> RedDBResult<Vec<crate::runtime::queue_lifecycle::DeliveredMessage>> {
266 super::vcs_queue::with_operation(runtime, &[queue], true, || {
267 let _snapshot_guard = wait_ms
271 .and_then(|_| crate::runtime::impl_core::capture_current_snapshot())
272 .map(|mut context| {
273 context.snapshot = context.manager.fresh_read_snapshot();
274 crate::runtime::execution_context::CurrentSnapshotGuard::install(context)
275 });
276 let adapter = PrimaryQueueStore::new(runtime.clone());
277 if let Some(delivered) = adapter
278 .try_group_read_batched(queue, group, consumer, count)
279 .map_err(map_qse)?
280 {
281 return Ok(delivered);
282 }
283 let read_lock = runtime
289 .inner
290 .rmw_locks
291 .lock_for(queue, "__queue_group_read__");
292 let _read_guard = read_lock.lock();
293 let store = runtime.inner.db.store();
296 let _publication = store.vault_publication_lock.lock();
297 let (lifecycle, _ps, txn) = runtime_lifecycle(runtime, queue);
298 lifecycle
299 .group_read(&txn, queue, group, consumer, count)
300 .map_err(map_qse)
301 })
302 };
303
304 let delivered = do_read(self)?;
305 let Some(wait_ms) = wait_ms else {
306 return Ok(delivered);
307 };
308 if !delivered.is_empty() {
309 return Ok(delivered);
310 }
311 let registry = self.queue_wait_registry();
321 let scope = queue_wait_scope();
322 let deadline = std::time::Instant::now() + std::time::Duration::from_millis(wait_ms);
323 let telemetry = self.queue_telemetry();
324 telemetry.record_wait_started(&scope, queue);
325 let wait_start = std::time::Instant::now();
326 let observe = |outcome: crate::runtime::queue_telemetry::WaitOutcomeLabel| {
327 let elapsed_ms = wait_start.elapsed().as_millis().min(u128::from(u64::MAX)) as u64;
328 telemetry.record_wait_outcome(&scope, queue, outcome, elapsed_ms);
329 };
330 loop {
331 let snapshot = registry.snapshot(&scope, queue);
335 let delivered = do_read(self)?;
336 if !delivered.is_empty() {
337 observe(crate::runtime::queue_telemetry::WaitOutcomeLabel::Woken);
338 return Ok(delivered);
339 }
340 if registry.is_cancelled() {
341 observe(crate::runtime::queue_telemetry::WaitOutcomeLabel::Cancelled);
342 return Err(RedDBError::Query(QUEUE_READ_WAIT_CANCELLED.to_string()));
343 }
344 if std::time::Instant::now() >= deadline {
345 observe(crate::runtime::queue_telemetry::WaitOutcomeLabel::Timeout);
346 return Ok(Vec::new());
347 }
348 let park_deadline = match earliest_future_available_at(&self.inner.db.store(), queue) {
358 Some(at_ns) => {
359 let now_ns = now_ns();
360 if at_ns <= now_ns {
361 deadline.min(std::time::Instant::now())
363 } else {
364 let wait_ns = at_ns - now_ns;
365 let due_instant =
366 std::time::Instant::now() + std::time::Duration::from_nanos(wait_ns);
367 deadline.min(due_instant)
368 }
369 }
370 None => deadline,
371 };
372 match registry.wait_until(&snapshot, park_deadline) {
373 crate::runtime::queue_wait_registry::WaitOutcome::Woken => continue,
374 crate::runtime::queue_wait_registry::WaitOutcome::Timeout => {
375 if std::time::Instant::now() >= deadline {
379 observe(crate::runtime::queue_telemetry::WaitOutcomeLabel::Timeout);
380 return Ok(Vec::new());
381 }
382 continue;
383 }
384 crate::runtime::queue_wait_registry::WaitOutcome::Cancelled => {
385 observe(crate::runtime::queue_telemetry::WaitOutcomeLabel::Cancelled);
386 return Err(RedDBError::Query(QUEUE_READ_WAIT_CANCELLED.to_string()));
387 }
388 }
389 }
390 }
391
392 pub(crate) async fn redwire_queue_wait_json(
409 &self,
410 queue: &str,
411 group: Option<&str>,
412 consumer: &str,
413 count: usize,
414 wait_ms: u64,
415 auth_identity: Option<(String, crate::auth::Role)>,
416 auth_principal: Option<crate::auth::UserId>,
417 tenant: Option<String>,
418 ) -> RedDBResult<RedwireWaitOutcome> {
419 let group_owned: RedDBResult<String> =
420 with_redwire_wait_context(&auth_identity, &auth_principal, &tenant, || {
421 let expr = reddb_rql::ast::QueryExpr::QueueCommand(
422 reddb_rql::ast::QueueCommand::GroupRead {
423 queue: queue.to_string(),
424 group: group.map(str::to_string),
425 consumer: consumer.to_string(),
426 count,
427 wait_ms: Some(wait_ms),
428 },
429 );
430 self.check_query_privilege(&expr)
431 .map_err(RedDBError::Query)?;
432 let store = self.inner.db.store();
433 ensure_queue_exists(store.as_ref(), queue)?;
434 let config = load_queue_config(store.as_ref(), queue);
435 resolve_read_group(store.as_ref(), queue, group, consumer, &config)
436 });
437 let group_owned = group_owned?;
438 let group_ref = group_owned.as_str();
439
440 let do_read =
441 |runtime: &RedDBRuntime| -> RedDBResult<Vec<crate::runtime::queue_lifecycle::DeliveredMessage>> {
442 with_redwire_wait_context(&auth_identity, &auth_principal, &tenant, || {
443 let (lifecycle, _ps, txn) = runtime_lifecycle(runtime, queue);
444 lifecycle
445 .group_read(&txn, queue, group_ref, consumer, count)
446 .map_err(map_qse)
447 })
448 };
449
450 let render = |delivered: Vec<crate::runtime::queue_lifecycle::DeliveredMessage>| {
451 RedwireWaitOutcome::Delivered(
452 delivered.into_iter().map(delivered_message_json).collect(),
453 )
454 };
455
456 let delivered = do_read(self)?;
458 if !delivered.is_empty() {
459 return Ok(render(delivered));
460 }
461
462 let registry = self.queue_wait_registry();
463 let scope =
464 with_redwire_wait_context(&auth_identity, &auth_principal, &tenant, queue_wait_scope);
465 let deadline = std::time::Instant::now() + std::time::Duration::from_millis(wait_ms);
466 let telemetry = self.queue_telemetry();
467 telemetry.record_wait_started(&scope, queue);
468 let wait_start = std::time::Instant::now();
469 tracing::debug!(
470 target: "reddb::redwire::queue_wait",
471 queue,
472 group = group_ref,
473 consumer,
474 count,
475 wait_ms,
476 scope = scope.as_str(),
477 "redwire queue wait parked"
478 );
479 let observe = |outcome: crate::runtime::queue_telemetry::WaitOutcomeLabel| {
480 let elapsed_ms = wait_start.elapsed().as_millis().min(u128::from(u64::MAX)) as u64;
481 telemetry.record_wait_outcome(&scope, queue, outcome, elapsed_ms);
482 tracing::debug!(
483 target: "reddb::redwire::queue_wait",
484 queue,
485 group = group_ref,
486 consumer,
487 count,
488 wait_ms,
489 scope = scope.as_str(),
490 outcome = outcome.as_str(),
491 duration_ms = elapsed_ms,
492 "redwire queue wait resolved"
493 );
494 };
495 loop {
496 let waiter = registry.async_waiter(&scope, queue);
500 let delivered = do_read(self)?;
501 if !delivered.is_empty() {
502 observe(crate::runtime::queue_telemetry::WaitOutcomeLabel::Woken);
503 return Ok(render(delivered));
504 }
505 if registry.is_cancelled() {
506 observe(crate::runtime::queue_telemetry::WaitOutcomeLabel::Cancelled);
507 return Ok(RedwireWaitOutcome::Cancelled);
508 }
509 if std::time::Instant::now() >= deadline {
510 observe(crate::runtime::queue_telemetry::WaitOutcomeLabel::Timeout);
511 return Ok(RedwireWaitOutcome::TimedOut);
512 }
513 let park_deadline = match earliest_future_available_at(&self.inner.db.store(), queue) {
514 Some(at_ns) => {
515 let now_ns = now_ns();
516 if at_ns <= now_ns {
517 deadline.min(std::time::Instant::now())
518 } else {
519 let wait_ns = at_ns - now_ns;
520 let due_instant =
521 std::time::Instant::now() + std::time::Duration::from_nanos(wait_ns);
522 deadline.min(due_instant)
523 }
524 }
525 None => deadline,
526 };
527 match registry.wait_until_async(&waiter, park_deadline).await {
532 crate::runtime::queue_wait_registry::WaitOutcome::Woken => continue,
533 crate::runtime::queue_wait_registry::WaitOutcome::Timeout => {
534 if std::time::Instant::now() >= deadline {
535 observe(crate::runtime::queue_telemetry::WaitOutcomeLabel::Timeout);
536 return Ok(RedwireWaitOutcome::TimedOut);
537 }
538 continue;
539 }
540 crate::runtime::queue_wait_registry::WaitOutcome::Cancelled => {
541 observe(crate::runtime::queue_telemetry::WaitOutcomeLabel::Cancelled);
542 return Ok(RedwireWaitOutcome::Cancelled);
543 }
544 }
545 }
546 }
547
548 pub(crate) fn redwire_queue_wait_cap_check(&self, wait_ms: u64) -> Result<(), String> {
557 let cap = self.config_u64(QUEUE_MAX_WAIT_MS_CONFIG_KEY, QUEUE_MAX_WAIT_MS_DEFAULT);
558 if wait_ms > cap {
559 Err(format!(
560 "queue-wait WAIT {wait_ms}ms exceeds server cap {QUEUE_MAX_WAIT_MS_CONFIG_KEY} = {cap}ms"
561 ))
562 } else {
563 Ok(())
564 }
565 }
566
567 pub(crate) fn enqueue_event_payload(
568 &self,
569 queue: &str,
570 payload: Value,
571 ) -> RedDBResult<EntityId> {
572 if super::delete_publication::event_writer(self).is_none() {
573 return super::vcs_queue::with_operation(self, &[queue], true, || {
574 self.enqueue_event_payload_inner(queue, payload)
575 });
576 }
577 self.enqueue_event_payload_inner(queue, payload)
578 }
579
580 fn enqueue_event_payload_inner(&self, queue: &str, payload: Value) -> RedDBResult<EntityId> {
581 let _event_snapshot = super::delete_publication::event_context(self)
582 .map(super::execution_context::CurrentSnapshotGuard::install);
583 let store = self.inner.db.store();
584 if store.get_collection(queue).is_none() {
586 crate::runtime::impl_ddl::ensure_event_target_queue_pub(self, queue)?;
587 }
588
589 let payload_bytes = estimate_payload_bytes(&payload);
591 let outbox_bytes = OUTBOX_APPROX_BYTES.fetch_add(payload_bytes, Ordering::Relaxed);
592
593 if outbox_bytes > OUTBOX_MAX_BYTES {
595 OUTBOX_APPROX_BYTES.fetch_sub(payload_bytes, Ordering::Relaxed);
596 EVENTS_DRAIN_RETRIES_TOTAL.fetch_add(1, Ordering::Relaxed);
597 return self.route_event_to_outbox_dlq(queue, payload, "outbox_max_bytes_exceeded");
598 }
599
600 if outbox_bytes > OUTBOX_WARN_BYTES && outbox_bytes - payload_bytes <= OUTBOX_WARN_BYTES {
602 tracing::warn!(
603 outbox_bytes,
604 warn_threshold = OUTBOX_WARN_BYTES,
605 "event outbox approaching capacity warning threshold"
606 );
607 crate::telemetry::operator_event::OperatorEvent::OutboxDlqActivated {
608 queue: queue.to_string(),
609 dlq: format!("{queue}_outbox_dlq"),
610 reason: "outbox_warn_bytes_exceeded".to_string(),
611 }
612 .emit_global();
613 }
614
615 let config = load_queue_config(store.as_ref(), queue);
616
617 if let Some(max_size) = config.max_size {
619 let current_len = load_queue_message_views(store.as_ref(), queue)
620 .unwrap_or_default()
621 .len();
622 if current_len >= max_size {
623 OUTBOX_APPROX_BYTES.fetch_sub(payload_bytes, Ordering::Relaxed);
624 EVENTS_DRAIN_RETRIES_TOTAL.fetch_add(1, Ordering::Relaxed);
625 return self.route_event_to_outbox_dlq(queue, payload, "queue_full");
626 }
627 if current_len * 10 >= max_size * 8 {
629 tracing::warn!(
630 queue = %queue,
631 size = current_len,
632 max = max_size,
633 "event target queue near capacity"
634 );
635 }
636 }
637
638 let id = self.enqueue_event_payload_raw(store.as_ref(), queue, &config, payload)?;
639 EVENTS_ENQUEUED_TOTAL.fetch_add(1, Ordering::Relaxed);
640 Ok(id)
641 }
642
643 fn route_event_to_outbox_dlq(
645 &self,
646 queue: &str,
647 payload: Value,
648 reason: &str,
649 ) -> RedDBResult<EntityId> {
650 let dlq_name = format!("{queue}_outbox_dlq");
651 EVENTS_DLQ_TOTAL.fetch_add(1, Ordering::Relaxed);
652
653 crate::telemetry::operator_event::OperatorEvent::OutboxDlqActivated {
654 queue: queue.to_string(),
655 dlq: dlq_name.clone(),
656 reason: reason.to_string(),
657 }
658 .emit_global();
659
660 let store = self.inner.db.store();
661 if store.get_collection(&dlq_name).is_none() {
662 crate::runtime::impl_ddl::ensure_event_target_queue_pub(self, &dlq_name)?;
663 }
664 let dlq_config = load_queue_config(store.as_ref(), &dlq_name);
665 let id = self.enqueue_event_payload_raw(store.as_ref(), &dlq_name, &dlq_config, payload)?;
666 EVENTS_ENQUEUED_TOTAL.fetch_add(1, Ordering::Relaxed);
667 Ok(id)
668 }
669
670 fn enqueue_event_payload_raw(
672 &self,
673 store: &UnifiedStore,
674 queue: &str,
675 config: &QueueRuntimeConfig,
676 payload: Value,
677 ) -> RedDBResult<EntityId> {
678 let event_xid = super::delete_publication::event_writer(self);
679 let position = next_queue_position(store, queue, QueueSide::Right)?;
680 let mut entity = UnifiedEntity::new(
681 EntityId::new(0),
682 EntityKind::QueueMessage {
683 queue: queue.to_string(),
684 position,
685 },
686 EntityData::QueueMessage(QueueMessageData {
687 payload,
688 priority: None,
689 enqueued_at_ns: now_ns(),
690 attempts: 0,
691 max_attempts: config.max_attempts,
692 acked: false,
693 }),
694 );
695 if let Some(xid) = self.current_xid().or(event_xid) {
696 entity.set_xmin(xid);
697 }
698 if let Some(epoch) = self.vcs_writer_epoch(queue)? {
699 entity.set_branch_origin(epoch);
700 }
701 super::vcs_queue::stamp_entity(store, queue, &mut entity)?;
702 let bytes = crate::storage::unified::memory_size::entity_bytes(&entity) as u64;
705 let _admission = self.admit_non_evictable_growth(
706 crate::storage::memory_pools::MemoryPool::SegmentArena,
707 "enqueue event",
708 bytes.saturating_mul(3),
709 )?;
710 let id = store
711 .insert_auto(queue, entity)
712 .map_err(|err| RedDBError::Internal(err.to_string()))?;
713 if let Some(ttl_ms) = config.ttl_ms {
714 store
715 .set_metadata(queue, id, queue_message_ttl_metadata(ttl_ms))
716 .map_err(|err| RedDBError::Internal(err.to_string()))?;
717 }
718 self.invalidate_result_cache_for_table(queue);
719 Ok(id)
720 }
721
722 pub fn execute_create_queue(
723 &self,
724 raw_query: &str,
725 query: &CreateQueueQuery,
726 ) -> RedDBResult<RuntimeQueryResult> {
727 self.check_write(crate::runtime::write_gate::WriteKind::Ddl)?;
728 if super::impl_ddl::is_system_schema_name(&query.name) {
729 return Err(RedDBError::Query("system schema is read-only".to_string()));
730 }
731 if query.dlq.as_deref() == Some(query.name.as_str()) {
732 return Err(RedDBError::Query(
733 "dead-letter queue must be different from the source queue".to_string(),
734 ));
735 }
736
737 let store = self.inner.db.store();
738 let exists = store.get_collection(&query.name).is_some();
739 if exists {
740 if query.if_not_exists {
741 return Ok(RuntimeQueryResult::ok_message(
742 raw_query.to_string(),
743 &format!("queue '{}' already exists", query.name),
744 "create",
745 ));
746 }
747 return Err(RedDBError::Query(format!(
748 "queue '{}' already exists",
749 query.name
750 )));
751 }
752
753 store
754 .create_collection(&query.name)
755 .map_err(|err| RedDBError::Internal(err.to_string()))?;
756 super::queue_storage_index::prepare_queue(self, &query.name)?;
757 if let Some(ttl_ms) = query.ttl_ms {
758 self.inner
759 .db
760 .set_collection_default_ttl_ms(&query.name, ttl_ms);
761 }
762 self.inner
763 .db
764 .save_collection_contract(queue_collection_contract(
765 &query.name,
766 query.priority,
767 query.ttl_ms,
768 ))
769 .map_err(|err| RedDBError::Internal(err.to_string()))?;
770 save_queue_config(
771 store.as_ref(),
772 &query.name,
773 &QueueRuntimeConfig {
774 mode: query.mode,
775 priority: query.priority,
776 max_size: query.max_size,
777 ttl_ms: query.ttl_ms,
778 dlq: query.dlq.clone(),
779 max_attempts: query.max_attempts,
780 lock_deadline_ms: query.lock_deadline_ms,
781 in_flight_cap_per_group: query.in_flight_cap_per_group,
782 retry_delay_ms: query.retry_delay_ms,
783 dedup_window_ms: query.dedup_window_ms,
784 },
785 )?;
786
787 if let Some(dlq) = &query.dlq {
788 if store.get_collection(dlq).is_none() {
789 store
790 .create_collection(dlq)
791 .map_err(|err| RedDBError::Internal(err.to_string()))?;
792 super::queue_storage_index::prepare_queue(self, dlq)?;
793 self.inner
794 .db
795 .save_collection_contract(queue_collection_contract(dlq, false, None))
796 .map_err(|err| RedDBError::Internal(err.to_string()))?;
797 }
798 }
799
800 self.invalidate_result_cache();
801 self.inner
802 .db
803 .persist_metadata()
804 .map_err(|err| RedDBError::Internal(err.to_string()))?;
805 let mut type_tags = Vec::new();
810 if let Some(dlq) = &query.dlq {
811 type_tags.push(format!("dlq:{}", dlq));
812 }
813 self.schema_vocabulary_apply(
814 crate::runtime::schema_vocabulary::DdlEvent::CreateCollection {
815 collection: query.name.clone(),
816 columns: vec!["payload".to_string()],
817 type_tags,
818 description: None,
819 },
820 );
821
822 let mut msg = format!("queue '{}' created", query.name);
823 msg.push_str(&format!(" (mode={})", query.mode.as_str()));
824 if query.priority {
825 msg.push_str(" (priority)");
826 }
827 if let Some(max_size) = query.max_size {
828 msg.push_str(&format!(" (max_size={max_size})"));
829 }
830 if let Some(ttl_ms) = query.ttl_ms {
831 msg.push_str(&format!(" (ttl={ttl_ms}ms)"));
832 }
833 if let Some(dlq) = &query.dlq {
834 msg.push_str(&format!(
835 " (dlq={dlq}, max_attempts={})",
836 query.max_attempts
837 ));
838 }
839
840 Ok(RuntimeQueryResult::ok_message(
841 raw_query.to_string(),
842 &msg,
843 "create",
844 ))
845 }
846
847 pub fn execute_alter_queue(
848 &self,
849 raw_query: &str,
850 query: &AlterQueueQuery,
851 ) -> RedDBResult<RuntimeQueryResult> {
852 self.check_write(crate::runtime::write_gate::WriteKind::Ddl)?;
853 let store = self.inner.db.store();
854 ensure_queue_exists(store.as_ref(), &query.name)?;
855
856 let mut config = load_queue_config(store.as_ref(), &query.name);
857 let mut summary: Vec<String> = Vec::new();
858
859 if let Some(new_mode) = query.mode {
860 let pending =
861 load_pending_entries(store.as_ref(), &query.name, None, None).unwrap_or_default();
862 if !pending.is_empty() {
863 tracing::warn!(
864 queue = %query.name,
865 pending_count = pending.len(),
866 new_mode = %new_mode.as_str(),
867 "ALTER QUEUE SET MODE: {} in-flight messages will drain with old mode; \
868 new reads use {}",
869 pending.len(),
870 new_mode.as_str(),
871 );
872 }
873 config.mode = new_mode;
874 summary.push(format!("mode={}", new_mode.as_str()));
875 }
876 if let Some(max_attempts) = query.max_attempts {
877 config.max_attempts = max_attempts;
878 summary.push(format!("max_attempts={max_attempts}"));
879 }
880 if let Some(lock_deadline_ms) = query.lock_deadline_ms {
881 config.lock_deadline_ms = lock_deadline_ms;
882 summary.push(format!("lock_deadline_ms={lock_deadline_ms}"));
883 }
884 if let Some(in_flight_cap) = query.in_flight_cap_per_group {
885 config.in_flight_cap_per_group = in_flight_cap;
886 summary.push(format!("in_flight_cap_per_group={in_flight_cap}"));
887 }
888 if let Some(dlq) = &query.dlq {
889 if dlq == &query.name {
890 return Err(RedDBError::Query(
891 "dead-letter queue must be different from the source queue".to_string(),
892 ));
893 }
894 config.dlq = Some(dlq.clone());
895 summary.push(format!("dlq={dlq}"));
896 }
897 if let Some(retry_delay_ms) = query.retry_delay_ms {
898 config.retry_delay_ms = if retry_delay_ms == 0 {
899 None
900 } else {
901 Some(retry_delay_ms)
902 };
903 summary.push(format!("retry_delay_ms={retry_delay_ms}"));
904 }
905 if let Some(dedup_window_ms) = query.dedup_window_ms {
906 config.dedup_window_ms = Some(dedup_window_ms);
907 summary.push(format!("dedup_window_ms={dedup_window_ms}"));
908 }
909
910 save_queue_config(store.as_ref(), &query.name, &config)?;
911
912 self.invalidate_result_cache();
913 self.inner
914 .db
915 .persist_metadata()
916 .map_err(|err| RedDBError::Internal(err.to_string()))?;
917
918 Ok(RuntimeQueryResult::ok_message(
919 raw_query.to_string(),
920 &format!("queue '{}' altered: {}", query.name, summary.join(", ")),
921 "alter",
922 ))
923 }
924
925 pub fn execute_drop_queue(
926 &self,
927 raw_query: &str,
928 query: &DropQueueQuery,
929 ) -> RedDBResult<RuntimeQueryResult> {
930 self.check_write(crate::runtime::write_gate::WriteKind::Ddl)?;
931 let store = self.inner.db.store();
932 let _publication = store.vault_publication_lock.lock();
933 self.vcs_reject_destructive_ddl(&query.name)?;
934 if super::impl_ddl::is_system_schema_name(&query.name) {
935 return Err(RedDBError::Query("system schema is read-only".to_string()));
936 }
937 if store.get_collection(&query.name).is_none() {
938 if query.if_exists {
939 return Ok(RuntimeQueryResult::ok_message(
940 raw_query.to_string(),
941 &format!("queue '{}' does not exist", query.name),
942 "drop",
943 ));
944 }
945 return Err(RedDBError::NotFound(format!(
946 "queue '{}' not found",
947 query.name
948 )));
949 }
950 let actual = crate::runtime::ddl::polymorphic_resolver::resolve(
951 &query.name,
952 &self.inner.db.catalog_model_snapshot(),
953 )?;
954 crate::runtime::ddl::polymorphic_resolver::ensure_model_match(
955 crate::catalog::CollectionModel::Queue,
956 actual,
957 )?;
958
959 store
960 .drop_collection(&query.name)
961 .map_err(|err| RedDBError::Internal(err.to_string()))?;
962 self.inner.db.clear_collection_default_ttl_ms(&query.name);
963 self.inner
964 .db
965 .remove_collection_contract(&query.name)
966 .map_err(|err| RedDBError::Internal(err.to_string()))?;
967 remove_queue_metadata(store.as_ref(), &query.name);
968 self.invalidate_result_cache();
969 self.inner
970 .db
971 .persist_metadata()
972 .map_err(|err| RedDBError::Internal(err.to_string()))?;
973 self.schema_vocabulary_apply(
975 crate::runtime::schema_vocabulary::DdlEvent::DropCollection {
976 collection: query.name.clone(),
977 },
978 );
979
980 Ok(RuntimeQueryResult::ok_message(
981 raw_query.to_string(),
982 &format!("queue '{}' dropped", query.name),
983 "drop",
984 ))
985 }
986
987 pub fn execute_queue_command(
988 &self,
989 raw_query: &str,
990 cmd: &QueueCommand,
991 ) -> RedDBResult<RuntimeQueryResult> {
992 let queues = match cmd {
993 QueueCommand::Move {
994 source,
995 destination,
996 ..
997 } => vec![source.as_str(), destination.as_str()],
998 QueueCommand::Push { queue, .. }
999 | QueueCommand::Pop { queue, .. }
1000 | QueueCommand::Peek { queue, .. }
1001 | QueueCommand::Len { queue }
1002 | QueueCommand::Purge { queue }
1003 | QueueCommand::GroupCreate { queue, .. }
1004 | QueueCommand::GroupRead { queue, .. }
1005 | QueueCommand::Pending { queue, .. }
1006 | QueueCommand::Claim { queue, .. }
1007 | QueueCommand::Ack { queue, .. }
1008 | QueueCommand::AckBatch { queue, .. }
1009 | QueueCommand::Nack { queue, .. } => vec![queue.as_str()],
1010 };
1011 if matches!(cmd, QueueCommand::GroupRead { .. }) {
1014 return self.execute_queue_command_inner(raw_query, cmd);
1015 }
1016 let write = !matches!(
1017 cmd,
1018 QueueCommand::Peek { .. } | QueueCommand::Len { .. } | QueueCommand::Pending { .. }
1019 );
1020 super::vcs_queue::with_operation(self, &queues, write, || {
1021 self.execute_queue_command_inner(raw_query, cmd)
1022 })
1023 }
1024
1025 fn execute_queue_command_inner(
1026 &self,
1027 raw_query: &str,
1028 cmd: &QueueCommand,
1029 ) -> RedDBResult<RuntimeQueryResult> {
1030 self.check_write(crate::runtime::write_gate::WriteKind::Dml)?;
1031 match cmd {
1032 QueueCommand::Push {
1033 queue,
1034 value,
1035 side,
1036 priority,
1037 key,
1038 available,
1039 dedup_key,
1040 } => {
1041 let store = self.inner.db.store();
1042 ensure_queue_exists(store.as_ref(), queue)?;
1043 let config = load_queue_config(store.as_ref(), queue);
1044 if key.is_some() && config.priority {
1045 return Err(RedDBError::Query(format!(
1046 "ordering key is not supported on priority queue '{}'",
1047 queue
1048 )));
1049 }
1050 if priority.is_some() && !config.priority {
1051 return Err(RedDBError::Query(format!(
1052 "queue '{}' is not a priority queue",
1053 queue
1054 )));
1055 }
1056 if let Some(dedup_key) = dedup_key.as_deref() {
1057 let now = now_ns();
1058 reclaim_expired_queue_dedup(store.as_ref(), queue, now)?;
1059 if let Some(existing_id) =
1060 find_queue_dedup(store.as_ref(), queue, dedup_key, now)?
1061 {
1062 let mut result = UnifiedResult::with_columns(vec![
1063 "message_id".into(),
1064 "side".into(),
1065 "queue".into(),
1066 ]);
1067 let mut record = UnifiedRecord::new();
1068 record.set("message_id", Value::text(message_id_string(existing_id)));
1069 record.set(
1070 "side",
1071 Value::text(match side {
1072 QueueSide::Left => "left".to_string(),
1073 QueueSide::Right => "right".to_string(),
1074 }),
1075 );
1076 record.set("queue", Value::text(queue.clone()));
1077 result.push(record);
1078
1079 return Ok(RuntimeQueryResult {
1080 query: raw_query.to_string(),
1081 mode: QueryMode::Sql,
1082 statement: "queue_push",
1083 engine: "runtime-queue",
1084 result,
1085 affected_rows: 1,
1086 statement_type: "insert",
1087 bookmark: None,
1088 notice: None,
1089 });
1090 }
1091 }
1092 if let Some(max_size) = config.max_size {
1093 let full = match super::queue_storage_index::capacity_reached(
1094 self, queue, max_size,
1095 )? {
1096 Some(full) => full,
1097 None => {
1098 queue_capacity_reached_by_scan(self, store.as_ref(), queue, max_size)?
1099 }
1100 };
1101 if full {
1102 return Err(RedDBError::Query(format!(
1103 "queue '{}' is full (max_size={max_size})",
1104 queue
1105 )));
1106 }
1107 }
1108
1109 let position = next_queue_position(store.as_ref(), queue, *side)?;
1110 let mut entity = UnifiedEntity::new(
1111 EntityId::new(0),
1112 EntityKind::QueueMessage {
1113 queue: queue.clone(),
1114 position,
1115 },
1116 EntityData::QueueMessage(QueueMessageData {
1117 payload: value.clone(),
1118 priority: if config.priority { *priority } else { None },
1119 enqueued_at_ns: now_ns(),
1120 attempts: 0,
1121 max_attempts: config.max_attempts,
1122 acked: false,
1123 }),
1124 );
1125 if let Some(xid) = self.current_xid() {
1128 entity.set_xmin(xid);
1129 }
1130 super::vcs_queue::stamp_entity(store.as_ref(), queue, &mut entity)?;
1131 let bytes = crate::storage::unified::memory_size::entity_bytes(&entity) as u64;
1134 let _admission = self.admit_non_evictable_growth(
1135 crate::storage::memory_pools::MemoryPool::SegmentArena,
1136 "queue push",
1137 bytes.saturating_mul(3),
1138 )?;
1139 let id = if self.current_xid().is_none()
1140 && !UnifiedStore::deferred_store_wal_capture_active()
1141 && dedup_key.is_none()
1142 && available.is_none()
1143 && key.is_none()
1144 && config.ttl_ms.is_none()
1145 && config.max_size.is_none()
1146 {
1147 entity.id = store.next_entity_id();
1148 let id = entity.id;
1149 store
1150 .publish_rows(crate::storage::unified::store::RowPublication {
1151 replacements: Vec::new(),
1152 inserts: vec![(queue.clone(), entity, None)],
1153 deletes: Vec::new(),
1154 })
1155 .map_err(|err| RedDBError::Internal(err.to_string()))?;
1156 id
1157 } else {
1158 store
1159 .insert_auto(queue, entity)
1160 .map_err(|err| RedDBError::Internal(err.to_string()))?
1161 };
1162 if let Some(dedup_key) = dedup_key.as_deref() {
1163 let window_ms = config
1164 .dedup_window_ms
1165 .unwrap_or(reddb_rql::ast::DEFAULT_QUEUE_DEDUP_WINDOW_MS);
1166 let expires_at_ns =
1167 now_ns().saturating_add(window_ms.saturating_mul(1_000_000));
1168 let metadata_id =
1169 record_queue_dedup(store.as_ref(), queue, dedup_key, id, expires_at_ns)?;
1170 self.stamp_xmin_if_in_txn(QUEUE_META_COLLECTION, metadata_id);
1171 if self.current_xid().is_some() {
1172 self.record_pending_queue_dedup(
1173 current_connection_id(),
1174 queue,
1175 dedup_key,
1176 metadata_id,
1177 );
1178 }
1179 }
1180 let available_at_ns = available.map(|a| match a {
1185 reddb_rql::ast::QueueAvailability::DelayMs(ms) => {
1186 now_ns().saturating_add(ms.saturating_mul(1_000_000))
1187 }
1188 reddb_rql::ast::QueueAvailability::AtUnixMs(ms) => ms.saturating_mul(1_000_000),
1189 });
1190 if config.ttl_ms.is_some() || available_at_ns.is_some() || key.is_some() {
1191 store
1192 .set_metadata(
1193 queue,
1194 id,
1195 queue_message_metadata(config.ttl_ms, available_at_ns, key.as_deref()),
1196 )
1197 .map_err(|err| RedDBError::Internal(err.to_string()))?;
1198 }
1199 self.record_queue_wake(&queue_wait_scope(), queue);
1204 self.invalidate_result_cache();
1205
1206 let mut result = UnifiedResult::with_columns(vec![
1207 "message_id".into(),
1208 "side".into(),
1209 "queue".into(),
1210 ]);
1211 let mut record = UnifiedRecord::new();
1212 record.set("message_id", Value::text(message_id_string(id)));
1213 record.set(
1214 "side",
1215 Value::text(match side {
1216 QueueSide::Left => "left".to_string(),
1217 QueueSide::Right => "right".to_string(),
1218 }),
1219 );
1220 record.set("queue", Value::text(queue.clone()));
1221 result.push(record);
1222
1223 Ok(RuntimeQueryResult {
1224 query: raw_query.to_string(),
1225 mode: QueryMode::Sql,
1226 statement: "queue_push",
1227 engine: "runtime-queue",
1228 result,
1229 affected_rows: 1,
1230 statement_type: "insert",
1231 bookmark: None,
1232 notice: None,
1233 })
1234 }
1235 QueueCommand::Pop { queue, side, count } => {
1236 let store = self.inner.db.store();
1237 ensure_queue_exists(store.as_ref(), queue)?;
1238 let (lifecycle, _ps, txn) = runtime_lifecycle(self, queue);
1239 let popped = lifecycle
1240 .pop(queue, ast_side_to_lc(*side), *count, &txn)
1241 .map_err(map_qse)?;
1242
1243 let mut result =
1244 UnifiedResult::with_columns(vec!["message_id".into(), "payload".into()]);
1245 for (message_id, payload) in &popped {
1246 let mut record = UnifiedRecord::new();
1247 record.set(
1248 "message_id",
1249 Value::text(message_id_string(EntityId::new(*message_id))),
1250 );
1251 record.set("payload", payload.clone());
1252 result.push(record);
1253 }
1254 let popped_count = popped.len() as u64;
1255 if popped_count > 0 {
1256 self.invalidate_result_cache();
1257 }
1258
1259 Ok(RuntimeQueryResult {
1260 query: raw_query.to_string(),
1261 mode: QueryMode::Sql,
1262 statement: "queue_pop",
1263 engine: "runtime-queue",
1264 result,
1265 affected_rows: popped_count,
1266 statement_type: "delete",
1267 bookmark: None,
1268 notice: None,
1269 })
1270 }
1271 QueueCommand::Peek { queue, count } => {
1272 let store = self.inner.db.store();
1273 ensure_queue_exists(store.as_ref(), queue)?;
1274 let (lifecycle, _ps, txn) = runtime_lifecycle(self, queue);
1275 let messages = lifecycle.peek(queue, *count, &txn);
1276
1277 let mut result =
1278 UnifiedResult::with_columns(vec!["message_id".into(), "payload".into()]);
1279 for message in messages {
1280 let mut record = UnifiedRecord::new();
1281 record.set(
1282 "message_id",
1283 Value::text(message_id_string(EntityId::new(message.message_id))),
1284 );
1285 record.set("payload", message.payload);
1286 result.push(record);
1287 }
1288
1289 Ok(RuntimeQueryResult {
1290 query: raw_query.to_string(),
1291 mode: QueryMode::Sql,
1292 statement: "queue_peek",
1293 engine: "runtime-queue",
1294 result,
1295 affected_rows: 0,
1296 statement_type: "select",
1297 bookmark: None,
1298 notice: None,
1299 })
1300 }
1301 QueueCommand::Len { queue } => {
1302 let store = self.inner.db.store();
1303 ensure_queue_exists(store.as_ref(), queue)?;
1304 let count =
1305 load_queue_message_views_with_runtime(Some(self), store.as_ref(), queue)?.len()
1306 as u64;
1307 let mut result = UnifiedResult::with_columns(vec!["len".into()]);
1308 let mut record = UnifiedRecord::new();
1309 record.set("len", Value::UnsignedInteger(count));
1310 result.push(record);
1311
1312 Ok(RuntimeQueryResult {
1313 query: raw_query.to_string(),
1314 mode: QueryMode::Sql,
1315 statement: "queue_len",
1316 engine: "runtime-queue",
1317 result,
1318 affected_rows: 0,
1319 statement_type: "select",
1320 bookmark: None,
1321 notice: None,
1322 })
1323 }
1324 QueueCommand::Purge { queue } => {
1325 let store = self.inner.db.store();
1326 ensure_queue_exists(store.as_ref(), queue)?;
1327 let (lifecycle, _ps, txn) = runtime_lifecycle(self, queue);
1328 let count = lifecycle.purge(queue, &txn).map_err(map_qse)?;
1329 if count > 0 {
1330 self.invalidate_result_cache();
1331 }
1332
1333 Ok(RuntimeQueryResult::ok_message(
1334 raw_query.to_string(),
1335 &format!("{count} messages purged from queue '{queue}'"),
1336 "delete",
1337 ))
1338 }
1339 QueueCommand::GroupCreate { queue, group } => {
1340 let store = self.inner.db.store();
1341 ensure_queue_exists(store.as_ref(), queue)?;
1342 if queue_group_exists(store.as_ref(), queue, group)? {
1343 return Ok(RuntimeQueryResult::ok_message(
1344 raw_query.to_string(),
1345 &format!(
1346 "consumer group '{}' already exists on queue '{}'",
1347 group, queue
1348 ),
1349 "create",
1350 ));
1351 }
1352 save_queue_group(store.as_ref(), queue, group)?;
1353 self.invalidate_result_cache();
1354
1355 Ok(RuntimeQueryResult::ok_message(
1356 raw_query.to_string(),
1357 &format!("consumer group '{}' created on queue '{}'", group, queue),
1358 "create",
1359 ))
1360 }
1361 QueueCommand::GroupRead {
1362 queue,
1363 group,
1364 consumer,
1365 count,
1366 wait_ms,
1367 } => {
1368 let store = self.inner.db.store();
1369 ensure_queue_exists(store.as_ref(), queue)?;
1370 if let Some(ms) = *wait_ms {
1376 if self.current_xid().is_some() {
1377 return Err(RedDBError::Query(
1378 "QUEUE READ … WAIT is autocommit-only: refusing to park inside an explicit transaction (BEGIN/COMMIT)"
1379 .to_string(),
1380 ));
1381 }
1382 let cap =
1383 self.config_u64(QUEUE_MAX_WAIT_MS_CONFIG_KEY, QUEUE_MAX_WAIT_MS_DEFAULT);
1384 if ms > cap {
1385 return Err(RedDBError::Query(format!(
1386 "QUEUE READ … WAIT {ms}ms exceeds server cap {QUEUE_MAX_WAIT_MS_CONFIG_KEY} = {cap}ms"
1387 )));
1388 }
1389 }
1390 let config = load_queue_config(store.as_ref(), queue);
1394 let group_owned =
1395 resolve_read_group(store.as_ref(), queue, group.as_deref(), consumer, &config)?;
1396 let group_ref = group_owned.as_str();
1397 let delivered = self
1398 .group_read_with_optional_wait(queue, group_ref, consumer, *count, *wait_ms)?;
1399
1400 {
1404 let lease_count = u32::try_from(delivered.len()).unwrap_or(u32::MAX);
1405 let now_ns = std::time::SystemTime::now()
1406 .duration_since(std::time::UNIX_EPOCH)
1407 .map(|d| d.as_nanos() as u64)
1408 .unwrap_or(0);
1409 self.queue_presence().heartbeat(
1410 queue,
1411 group_ref,
1412 consumer,
1413 lease_count,
1414 now_ns,
1415 );
1416 }
1417
1418 let mut result = UnifiedResult::with_columns(vec![
1419 "message_id".into(),
1420 "payload".into(),
1421 "consumer".into(),
1422 "delivery_count".into(),
1423 "attempts".into(),
1424 ]);
1425
1426 for message in delivered {
1427 let mut record = UnifiedRecord::new();
1428 record.set(
1429 "message_id",
1430 Value::text(message_id_string(EntityId::new(message.message_id))),
1431 );
1432 record.set("payload", message.payload);
1433 record.set("consumer", Value::text(message.consumer));
1434 record.set(
1435 "delivery_count",
1436 Value::UnsignedInteger(u64::from(message.delivery_count)),
1437 );
1438 record.set(
1439 "attempts",
1440 Value::UnsignedInteger(u64::from(message.delivery_count)),
1441 );
1442 result.push(record);
1443 }
1444 if !result.records.is_empty() {
1445 self.invalidate_result_cache();
1446 }
1447
1448 Ok(RuntimeQueryResult {
1449 query: raw_query.to_string(),
1450 mode: QueryMode::Sql,
1451 statement: "queue_group_read",
1452 engine: "runtime-queue",
1453 result,
1454 affected_rows: 0,
1455 statement_type: "select",
1456 bookmark: None,
1457 notice: None,
1458 })
1459 }
1460 QueueCommand::Pending { queue, group } => {
1461 let store = self.inner.db.store();
1462 ensure_queue_exists(store.as_ref(), queue)?;
1463 require_queue_group(store.as_ref(), queue, group)?;
1464 let mut pending = load_pending_entries(store.as_ref(), queue, Some(group), None)?;
1465 pending.sort_by_key(|entry| entry.delivered_at_ns);
1466 let current_time_ns = now_ns();
1467
1468 let mut result = UnifiedResult::with_columns(vec![
1469 "message_id".into(),
1470 "consumer".into(),
1471 "delivered_at_ns".into(),
1472 "delivery_count".into(),
1473 "idle_ms".into(),
1474 ]);
1475 for entry in pending {
1476 let mut record = UnifiedRecord::new();
1477 record.set(
1478 "message_id",
1479 Value::text(message_id_string(entry.message_id)),
1480 );
1481 record.set("consumer", Value::text(entry.consumer));
1482 record.set(
1483 "delivered_at_ns",
1484 Value::UnsignedInteger(entry.delivered_at_ns),
1485 );
1486 record.set(
1487 "delivery_count",
1488 Value::UnsignedInteger(u64::from(entry.delivery_count)),
1489 );
1490 record.set(
1491 "idle_ms",
1492 Value::UnsignedInteger(
1493 current_time_ns.saturating_sub(entry.delivered_at_ns) / 1_000_000,
1494 ),
1495 );
1496 result.push(record);
1497 }
1498
1499 Ok(RuntimeQueryResult {
1500 query: raw_query.to_string(),
1501 mode: QueryMode::Sql,
1502 statement: "queue_pending",
1503 engine: "runtime-queue",
1504 result,
1505 affected_rows: 0,
1506 statement_type: "select",
1507 bookmark: None,
1508 notice: None,
1509 })
1510 }
1511 QueueCommand::Claim {
1512 queue,
1513 group,
1514 consumer,
1515 min_idle_ms,
1516 } => {
1517 let store = self.inner.db.store();
1518 ensure_queue_exists(store.as_ref(), queue)?;
1519 require_queue_group(store.as_ref(), queue, group)?;
1520 let (lifecycle, _ps, txn) = runtime_lifecycle(self, queue);
1521 let delivered = lifecycle
1522 .claim_delivering(queue, consumer, *min_idle_ms, &txn)
1523 .map_err(map_qse)?;
1524
1525 let mut result = UnifiedResult::with_columns(vec![
1526 "message_id".into(),
1527 "delivery_id".into(),
1528 "payload".into(),
1529 "consumer".into(),
1530 "delivery_count".into(),
1531 ]);
1532
1533 for message in delivered {
1534 let mut record = UnifiedRecord::new();
1535 record.set(
1536 "message_id",
1537 Value::text(message_id_string(EntityId::new(message.message_id))),
1538 );
1539 record.set("delivery_id", Value::text(message.delivery_id));
1540 record.set("payload", message.payload);
1541 record.set("consumer", Value::text(message.consumer));
1542 record.set(
1543 "delivery_count",
1544 Value::UnsignedInteger(u64::from(message.delivery_count)),
1545 );
1546 result.push(record);
1547 }
1548 if !result.records.is_empty() {
1549 self.invalidate_result_cache();
1550 }
1551 let affected_rows = result.records.len() as u64;
1552
1553 Ok(RuntimeQueryResult {
1554 query: raw_query.to_string(),
1555 mode: QueryMode::Sql,
1556 statement: "queue_claim",
1557 engine: "runtime-queue",
1558 result,
1559 affected_rows,
1560 statement_type: "update",
1561 bookmark: None,
1562 notice: None,
1563 })
1564 }
1565 QueueCommand::Ack {
1566 queue,
1567 group,
1568 message_id,
1569 delivery_id,
1570 } => {
1571 let store = self.inner.db.store();
1572 ensure_queue_exists(store.as_ref(), queue)?;
1573 let (group_owned, message_entity) = resolve_ack_nack_handle(
1574 store.as_ref(),
1575 queue,
1576 group,
1577 message_id,
1578 delivery_id.as_deref(),
1579 )?;
1580 let group_ref = group_owned.as_str();
1581 require_queue_group(store.as_ref(), queue, group_ref)?;
1582 let (lifecycle, ps, txn) = runtime_lifecycle(self, queue);
1583 let did = match delivery_id.as_deref() {
1584 Some(d) => d.to_string(),
1585 None => ps
1586 .find_pending_by_key(queue, message_entity.raw(), group_ref)
1587 .ok_or_else(|| {
1588 RedDBError::NotFound(format!(
1589 "no pending delivery for message '{}' on queue '{}' (group '{}')",
1590 message_entity.raw(),
1591 queue,
1592 group_ref
1593 ))
1594 })?,
1595 };
1596 lifecycle.ack(&txn, &did).map_err(map_qse)?;
1597 self.invalidate_result_cache();
1598
1599 Ok(RuntimeQueryResult::ok_message(
1600 raw_query.to_string(),
1601 "message acknowledged",
1602 "update",
1603 ))
1604 }
1605 QueueCommand::AckBatch {
1606 queue,
1607 group,
1608 message_ids,
1609 } => {
1610 if message_ids.is_empty() || message_ids.len() > 256 {
1611 return Err(RedDBError::Query(
1612 "QUEUE ACK BATCH requires 1 to 256 messages".to_string(),
1613 ));
1614 }
1615 let store = self.inner.db.store();
1616 let _publication = store.vault_publication_lock.lock();
1617 ensure_queue_exists(store.as_ref(), queue)?;
1618 require_queue_group(store.as_ref(), queue, group)?;
1619 let ps = PrimaryQueueStore::new(self.clone());
1620 let txn = ps.new_txn();
1621 let mut deliveries = Vec::with_capacity(message_ids.len());
1622 for message_id in message_ids {
1623 let (_, message) =
1624 resolve_ack_nack_handle(store.as_ref(), queue, group, message_id, None)?;
1625 let delivery = ps
1626 .find_pending_by_key(queue, message.raw(), group)
1627 .ok_or_else(|| {
1628 RedDBError::NotFound(format!(
1629 "no pending delivery for message '{message_id}'"
1630 ))
1631 })?;
1632 deliveries.push(delivery);
1633 }
1634 let fanout =
1635 ps.lifecycle_config(queue).mode == crate::storage::queue::QueueMode::Fanout;
1636 drop(_publication);
1637 ps.ack_many(&txn, &deliveries, fanout).map_err(map_qse)?;
1638 self.invalidate_result_cache();
1639 Ok(RuntimeQueryResult::ok_message(
1640 raw_query.to_string(),
1641 "messages acknowledged",
1642 "update",
1643 ))
1644 }
1645 QueueCommand::Nack {
1646 queue,
1647 group,
1648 message_id,
1649 delivery_id,
1650 delay_ms,
1651 } => {
1652 let store = self.inner.db.store();
1653 ensure_queue_exists(store.as_ref(), queue)?;
1654 let config = load_queue_config(store.as_ref(), queue);
1655 if delay_ms.is_some() {
1661 if let Some((_, role)) = current_auth_identity() {
1662 if !role.can_write() {
1663 return Err(RedDBError::InvalidOperation(format!(
1664 "role '{role}' is not authorized to override NACK retry delay on queue '{queue}'"
1665 )));
1666 }
1667 }
1668 }
1669 let (group_owned, message_entity) = resolve_ack_nack_handle(
1670 store.as_ref(),
1671 queue,
1672 group,
1673 message_id,
1674 delivery_id.as_deref(),
1675 )?;
1676 let group_ref = group_owned.as_str();
1677 require_queue_group(store.as_ref(), queue, group_ref)?;
1678 let (lifecycle, ps, txn) = runtime_lifecycle(self, queue);
1679 let did = match delivery_id.as_deref() {
1680 Some(d) => d.to_string(),
1681 None => ps
1682 .find_pending_by_key(queue, message_entity.raw(), group_ref)
1683 .ok_or_else(|| {
1684 RedDBError::NotFound(format!(
1685 "no pending delivery for message '{}' on queue '{}' (group '{}')",
1686 message_entity.raw(),
1687 queue,
1688 group_ref
1689 ))
1690 })?,
1691 };
1692 let effective_delay_ms = delay_ms.or(config.retry_delay_ms).unwrap_or(0);
1696 let pending_attempt = ps.read_pending_attempt(&did).map_err(map_qse)?;
1697 let nack_attempts = pending_attempt.attempts.saturating_add(1);
1698 let retry_available_at_ns = if effective_delay_ms > 0 {
1699 Some(now_ns().saturating_add(effective_delay_ms.saturating_mul(1_000_000)))
1700 } else {
1701 None
1702 };
1703 let retry_deadline = if effective_delay_ms > 0 {
1704 Some(
1705 std::time::Instant::now()
1706 + std::time::Duration::from_millis(effective_delay_ms),
1707 )
1708 } else {
1709 None
1710 };
1711 let outcome = lifecycle
1712 .nack_with_retry_deadline(&txn, &did, retry_deadline)
1713 .map_err(map_qse)?;
1714 if matches!(outcome, RetirementOutcome::Requeued) && effective_delay_ms > 0 {
1718 set_message_available_at_ns(
1719 store.as_ref(),
1720 queue,
1721 message_entity,
1722 retry_available_at_ns,
1723 config.ttl_ms,
1724 )?;
1725 }
1726 self.maybe_emit_nack_audit(
1733 queue,
1734 group_ref,
1735 &did,
1736 *delay_ms,
1737 config.retry_delay_ms,
1738 &outcome,
1739 );
1740 let outcome_label = match &outcome {
1741 RetirementOutcome::Requeued => NackOutcomeLabel::Retry,
1742 RetirementOutcome::MovedToDlq(_) => NackOutcomeLabel::Dlq,
1743 RetirementOutcome::Dropped => NackOutcomeLabel::Drop,
1744 };
1745 self.queue_telemetry().record_nacked(
1746 queue,
1747 group_ref,
1748 config.mode.as_str(),
1749 outcome_label,
1750 );
1751 if let RetirementOutcome::MovedToDlq(dlq) = &outcome {
1752 OperatorEvent::QueueDlqPromoted {
1753 queue: queue.to_string(),
1754 group: group_ref.to_string(),
1755 dlq: dlq.clone(),
1756 message_id: pending_attempt.message_id,
1757 attempts: nack_attempts,
1758 reason: format!("lifecycle_nack:{did}"),
1759 }
1760 .emit(self.audit_log());
1761 }
1762 let message = match outcome {
1763 RetirementOutcome::Requeued => {
1764 if effective_delay_ms > 0 {
1765 format!("message requeued (delay={effective_delay_ms}ms)")
1766 } else {
1767 "message requeued".to_string()
1768 }
1769 }
1770 RetirementOutcome::MovedToDlq(dlq) => {
1771 format!("message moved to dead-letter queue '{}'", dlq)
1772 }
1773 RetirementOutcome::Dropped => "message dropped after max attempts".to_string(),
1774 };
1775 self.invalidate_result_cache();
1776
1777 Ok(RuntimeQueryResult::ok_message(
1778 raw_query.to_string(),
1779 &message,
1780 "update",
1781 ))
1782 }
1783 QueueCommand::Move {
1784 source,
1785 destination,
1786 filter,
1787 limit,
1788 } => self.execute_queue_move(raw_query, source, destination, filter.as_ref(), *limit),
1789 }
1790 }
1791
1792 pub fn execute_queue_select(
1793 &self,
1794 raw_query: &str,
1795 query: &QueueSelectQuery,
1796 ) -> RedDBResult<RuntimeQueryResult> {
1797 super::vcs_queue::with_operation(self, &[&query.queue], false, || {
1798 self.execute_queue_select_inner(raw_query, query)
1799 })
1800 }
1801
1802 fn execute_queue_select_inner(
1803 &self,
1804 raw_query: &str,
1805 query: &QueueSelectQuery,
1806 ) -> RedDBResult<RuntimeQueryResult> {
1807 let store = self.inner.db.store();
1808 ensure_queue_exists(store.as_ref(), &query.queue)?;
1809 let config = load_queue_config(store.as_ref(), &query.queue);
1810 let dlq = queue_is_dead_letter_target(store.as_ref(), &query.queue);
1811 let columns = if query.columns.is_empty() {
1812 queue_projection_default_columns()
1813 } else {
1814 query.columns.clone()
1815 };
1816
1817 let mut messages =
1818 load_queue_message_views_with_runtime(Some(self), store.as_ref(), &query.queue)?;
1819 sort_queue_messages(&mut messages, &config, QueueSide::Left);
1820
1821 let mut result = UnifiedResult::with_columns(columns.clone());
1822 for message in messages {
1823 if query
1824 .filter
1825 .as_ref()
1826 .is_some_and(|filter| !queue_message_matches_filter(&message, dlq, filter))
1827 {
1828 continue;
1829 }
1830 let record = queue_projection_record(&columns, &message, dlq)?;
1831 result.push(record);
1832 if query
1833 .limit
1834 .is_some_and(|limit| result.records.len() >= limit as usize)
1835 {
1836 break;
1837 }
1838 }
1839
1840 Ok(RuntimeQueryResult {
1841 query: raw_query.to_string(),
1842 mode: QueryMode::Sql,
1843 statement: "queue_select",
1844 engine: "runtime-queue",
1845 result,
1846 affected_rows: 0,
1847 statement_type: "select",
1848 bookmark: None,
1849 notice: None,
1850 })
1851 }
1852
1853 fn execute_queue_move(
1854 &self,
1855 raw_query: &str,
1856 source: &str,
1857 destination: &str,
1858 filter: Option<&Filter>,
1859 limit: usize,
1860 ) -> RedDBResult<RuntimeQueryResult> {
1861 if source == destination {
1862 return Err(RedDBError::Query(
1863 "QUEUE MOVE source and destination must be different".to_string(),
1864 ));
1865 }
1866 let store = self.inner.db.store();
1867 ensure_queue_exists(store.as_ref(), source)?;
1868 ensure_queue_exists(store.as_ref(), destination)?;
1869 let source_config = load_queue_config(store.as_ref(), source);
1870 let destination_config = load_queue_config(store.as_ref(), destination);
1871 let source_dlq = queue_is_dead_letter_target(store.as_ref(), source);
1872
1873 let mut messages =
1874 load_queue_message_views_with_runtime(Some(self), store.as_ref(), source)?;
1875 sort_queue_messages(&mut messages, &source_config, QueueSide::Left);
1876 let selected = messages
1877 .into_iter()
1878 .filter(|message| {
1879 filter
1880 .map(|f| queue_message_matches_filter(message, source_dlq, f))
1881 .unwrap_or(true)
1882 })
1883 .take(limit)
1884 .collect::<Vec<_>>();
1885
1886 if let Some(max_size) = destination_config.max_size {
1887 let current_len =
1888 load_queue_message_views_with_runtime(Some(self), store.as_ref(), destination)?
1889 .len();
1890 if current_len + selected.len() > max_size {
1891 return Err(RedDBError::Query(format!(
1892 "queue '{}' is full (max_size={max_size})",
1893 destination
1894 )));
1895 }
1896 }
1897
1898 for message in &selected {
1899 let lock = queue_message_lock_handle(self, source, message.id);
1900 let Some(_guard) = lock.try_lock() else {
1901 return Err(RedDBError::Query(format!(
1902 "message '{}' is locked on queue '{}'",
1903 message.id.raw(),
1904 source
1905 )));
1906 };
1907 if queue_message_view_by_id(store.as_ref(), source, message.id)?.is_none() {
1908 return Err(RedDBError::Query(format!(
1909 "message '{}' is no longer available on queue '{}'",
1910 message.id.raw(),
1911 source
1912 )));
1913 }
1914 }
1915
1916 let mut inserted = Vec::new();
1917 for message in &selected {
1918 match insert_moved_queue_message(
1919 store.as_ref(),
1920 destination,
1921 &destination_config,
1922 message,
1923 ) {
1924 Ok(id) => inserted.push(id),
1925 Err(err) => {
1926 for id in inserted {
1927 let _ = store.delete(destination, id);
1928 }
1929 return Err(err);
1930 }
1931 }
1932 }
1933
1934 let (move_lifecycle, _move_ps, move_txn) = runtime_lifecycle(self, source);
1935 for message in &selected {
1936 move_lifecycle
1937 .delete_with_state(source, message.id.raw(), &move_txn)
1938 .map_err(map_qse)?;
1939 }
1940 if !selected.is_empty() {
1941 self.invalidate_result_cache();
1942 }
1943
1944 let selected_count = selected.len() as u64;
1945 self.audit_log().record_event(
1946 AuditEvent::builder("queue/move")
1947 .source(AuditAuthSource::System)
1948 .outcome(Outcome::Success)
1949 .resource(format!("queue:{source}->{destination}"))
1950 .fields([
1951 AuditFieldEscaper::field("source", source),
1952 AuditFieldEscaper::field("destination", destination),
1953 AuditFieldEscaper::field("selected", selected_count),
1954 AuditFieldEscaper::field("committed", selected_count),
1955 ])
1956 .build(),
1957 );
1958
1959 let mut result = UnifiedResult::with_columns(vec![
1960 "source".into(),
1961 "destination".into(),
1962 "selected".into(),
1963 "committed".into(),
1964 ]);
1965 let mut record = UnifiedRecord::new();
1966 record.set("source", Value::text(source.to_string()));
1967 record.set("destination", Value::text(destination.to_string()));
1968 record.set("selected", Value::UnsignedInteger(selected_count));
1969 record.set("committed", Value::UnsignedInteger(selected_count));
1970 result.push(record);
1971
1972 Ok(RuntimeQueryResult {
1973 query: raw_query.to_string(),
1974 mode: QueryMode::Sql,
1975 statement: "queue_move",
1976 engine: "runtime-queue",
1977 result,
1978 affected_rows: selected_count,
1979 statement_type: "update",
1980 bookmark: None,
1981 notice: None,
1982 })
1983 }
1984
1985 fn maybe_emit_nack_audit(
2000 &self,
2001 queue: &str,
2002 group: &str,
2003 delivery_id: &str,
2004 override_ms: Option<u64>,
2005 default_ms: Option<u64>,
2006 outcome: &RetirementOutcome,
2007 ) {
2008 let Some(override_ms) = override_ms else {
2009 return;
2010 };
2011 let outcome_label = match outcome {
2012 RetirementOutcome::Requeued => "requeued",
2013 RetirementOutcome::MovedToDlq(_) => "dlq",
2014 RetirementOutcome::Dropped => "dropped",
2015 };
2016 const SIGNIFICANT_DELAY_MS: u64 = 60_000;
2017 let destination_changed = !matches!(outcome, RetirementOutcome::Requeued);
2018 if override_ms < SIGNIFICANT_DELAY_MS && !destination_changed {
2019 return;
2020 }
2021 self.audit_log().record_event(
2022 AuditEvent::builder("queue/nack/override")
2023 .source(AuditAuthSource::System)
2024 .outcome(Outcome::Success)
2025 .resource(format!("queue:{queue}"))
2026 .fields([
2027 AuditFieldEscaper::field("queue", queue),
2028 AuditFieldEscaper::field("group", group),
2029 AuditFieldEscaper::field("delivery_id", delivery_id),
2030 AuditFieldEscaper::field("override_delay_ms", override_ms),
2031 AuditFieldEscaper::field("default_delay_ms", default_ms.unwrap_or(0)),
2032 AuditFieldEscaper::field("outcome", outcome_label),
2033 ])
2034 .build(),
2035 );
2036 }
2037
2038 pub(super) fn is_queue_collection(&self, collection: &str) -> bool {
2042 self.db()
2043 .collection_contract_arc(collection)
2044 .map(|contract| contract.declared_model == crate::catalog::CollectionModel::Queue)
2045 .unwrap_or(false)
2046 }
2047
2048 pub(super) fn execute_queue_shaped_claim(
2062 &self,
2063 raw_query: &str,
2064 query: &UpdateQuery,
2065 ) -> RedDBResult<RuntimeQueryResult> {
2066 validate_queue_claim_shape(query)?;
2067
2068 let queue = query.table.as_str();
2069 let store = self.inner.db.store();
2070 ensure_queue_exists(store.as_ref(), queue)?;
2071
2072 let config = load_queue_config(store.as_ref(), queue);
2077 if config.mode != QueueMode::Work {
2078 return Err(RedDBError::InvalidOperation(format!(
2079 "CLAIM on queue '{queue}' cannot be expressed: FANOUT delivery \
2080 must be consumed through GROUP READ, not a queue-shaped CLAIM"
2081 )));
2082 }
2083
2084 let group = resolve_read_group(store.as_ref(), queue, None, "", &config)?;
2088 let count = query.claim_limit.unwrap_or(0) as usize;
2089 let (lifecycle, _ps, txn) = runtime_lifecycle(self, queue);
2090 let delivered = lifecycle
2091 .deliver(&txn, queue, &group, count)
2092 .map_err(map_qse)?;
2093
2094 let mut result = UnifiedResult::with_columns(vec!["delivery_id".into(), "payload".into()]);
2095 for message in &delivered {
2096 let mut record = UnifiedRecord::new();
2097 record.set("delivery_id", Value::text(message.delivery_id.clone()));
2098 record.set("payload", message.payload.clone());
2099 result.push(record);
2100 }
2101 let affected_rows = delivered.len() as u64;
2102 if affected_rows > 0 {
2103 self.invalidate_result_cache();
2104 }
2105
2106 Ok(RuntimeQueryResult {
2107 query: raw_query.to_string(),
2108 mode: QueryMode::Sql,
2109 statement: "queue_claim",
2110 engine: "runtime-queue",
2111 result,
2112 affected_rows,
2113 statement_type: "update",
2114 bookmark: None,
2115 notice: None,
2116 })
2117 }
2118}
2119
2120fn validate_queue_claim_shape(query: &UpdateQuery) -> RedDBResult<()> {
2131 if query.order_by.iter().any(|clause| !clause.ascending) {
2132 return Err(RedDBError::InvalidOperation(format!(
2133 "CLAIM on queue '{}' cannot be expressed: a descending ORDER BY \
2134 conflicts with FIFO queue delivery order",
2135 query.table
2136 )));
2137 }
2138 if query.claim_exact {
2139 return Err(RedDBError::InvalidOperation(format!(
2140 "CLAIM EXACT on queue '{}' cannot be expressed: queue delivery \
2141 acquires up to N available messages, not an exact-or-nothing batch",
2142 query.table
2143 )));
2144 }
2145 Ok(())
2146}
2147
2148fn ensure_queue_exists(store: &UnifiedStore, queue: &str) -> RedDBResult<()> {
2149 if store.get_collection(queue).is_some() {
2150 Ok(())
2151 } else {
2152 Err(RedDBError::NotFound(format!("queue '{}' not found", queue)))
2153 }
2154}
2155
2156pub(super) fn load_queue_config(store: &UnifiedStore, queue: &str) -> QueueRuntimeConfig {
2157 let default = QueueRuntimeConfig {
2158 mode: QueueMode::Work,
2159 priority: false,
2160 max_size: None,
2161 ttl_ms: None,
2162 dlq: None,
2163 max_attempts: reddb_rql::ast::DEFAULT_QUEUE_MAX_ATTEMPTS,
2164 lock_deadline_ms: reddb_rql::ast::DEFAULT_QUEUE_LOCK_DEADLINE_MS,
2165 in_flight_cap_per_group: reddb_rql::ast::DEFAULT_QUEUE_IN_FLIGHT_CAP_PER_GROUP,
2166 retry_delay_ms: None,
2167 dedup_window_ms: None,
2168 };
2169
2170 let Some(manager) = store.get_collection(QUEUE_META_COLLECTION) else {
2171 return default;
2172 };
2173 let rows = match super::queue_storage_index::subject_rows_store(
2174 store,
2175 "queue_config",
2176 queue,
2177 None,
2178 None,
2179 ) {
2180 Ok(Some(rows)) => rows,
2181 Ok(None) | Err(_) => manager
2182 .query_all(|entity| {
2183 entity.data.as_row().is_some_and(|row| {
2184 row_text(row, "kind").as_deref() == Some("queue_config")
2185 && row_text(row, "queue").as_deref() == Some(queue)
2186 })
2187 })
2188 .into_iter()
2189 .filter_map(|entity| match entity.data {
2190 EntityData::Row(row) => Some((entity.id, row)),
2191 _ => None,
2192 })
2193 .collect(),
2194 };
2195 rows.into_iter()
2196 .next()
2197 .map(|(_, row)| QueueRuntimeConfig {
2198 mode: row_text(&row, "mode")
2199 .as_deref()
2200 .and_then(QueueMode::parse)
2201 .unwrap_or_default(),
2202 priority: row_bool(&row, "priority").unwrap_or(false),
2203 max_size: row_u64(&row, "max_size").map(|value| value as usize),
2204 ttl_ms: row_u64(&row, "ttl_ms"),
2205 dlq: row_text(&row, "dlq"),
2206 max_attempts: row_u64(&row, "max_attempts")
2207 .map(|value| value as u32)
2208 .unwrap_or(reddb_rql::ast::DEFAULT_QUEUE_MAX_ATTEMPTS),
2209 lock_deadline_ms: row_u64(&row, "lock_deadline_ms")
2210 .unwrap_or(reddb_rql::ast::DEFAULT_QUEUE_LOCK_DEADLINE_MS),
2211 in_flight_cap_per_group: row_u64(&row, "in_flight_cap_per_group")
2212 .map(|value| value as u32)
2213 .unwrap_or(reddb_rql::ast::DEFAULT_QUEUE_IN_FLIGHT_CAP_PER_GROUP),
2214 retry_delay_ms: row_u64(&row, "retry_delay_ms").filter(|v| *v > 0),
2215 dedup_window_ms: row_u64(&row, "dedup_window_ms"),
2216 })
2217 .unwrap_or(default)
2218}
2219
2220pub(super) fn queue_mode_str(store: &UnifiedStore, queue: &str) -> &'static str {
2221 load_queue_config(store, queue).mode.as_str()
2222}
2223
2224fn save_queue_config(
2225 store: &UnifiedStore,
2226 queue: &str,
2227 config: &QueueRuntimeConfig,
2228) -> RedDBResult<()> {
2229 match super::queue_storage_index::subject_rows_store(store, "queue_config", queue, None, None)?
2230 {
2231 Some(rows) => {
2232 for (id, _) in rows {
2233 let _ = super::vcs_queue::delete_metadata(store, id);
2234 }
2235 }
2236 None => remove_meta_rows(store, |row| {
2237 row_text(row, "kind").as_deref() == Some("queue_config")
2238 && row_text(row, "queue").as_deref() == Some(queue)
2239 }),
2240 }
2241
2242 let mut fields = HashMap::new();
2243 fields.insert("kind".to_string(), Value::text("queue_config".to_string()));
2244 fields.insert("queue".to_string(), Value::text(queue.to_string()));
2245 fields.insert(
2246 "mode".to_string(),
2247 Value::text(config.mode.as_str().to_string()),
2248 );
2249 fields.insert("priority".to_string(), Value::Boolean(config.priority));
2250 fields.insert(
2251 "max_size".to_string(),
2252 config
2253 .max_size
2254 .map(|value| Value::UnsignedInteger(value as u64))
2255 .unwrap_or(Value::Null),
2256 );
2257 fields.insert(
2258 "ttl_ms".to_string(),
2259 config
2260 .ttl_ms
2261 .map(Value::UnsignedInteger)
2262 .unwrap_or(Value::Null),
2263 );
2264 fields.insert(
2265 "dlq".to_string(),
2266 config.dlq.clone().map(Value::text).unwrap_or(Value::Null),
2267 );
2268 fields.insert(
2269 "max_attempts".to_string(),
2270 Value::UnsignedInteger(u64::from(config.max_attempts)),
2271 );
2272 fields.insert(
2273 "lock_deadline_ms".to_string(),
2274 Value::UnsignedInteger(config.lock_deadline_ms),
2275 );
2276 fields.insert(
2277 "in_flight_cap_per_group".to_string(),
2278 Value::UnsignedInteger(u64::from(config.in_flight_cap_per_group)),
2279 );
2280 fields.insert(
2281 "retry_delay_ms".to_string(),
2282 config
2283 .retry_delay_ms
2284 .map(Value::UnsignedInteger)
2285 .unwrap_or(Value::Null),
2286 );
2287 fields.insert(
2288 "dedup_window_ms".to_string(),
2289 config
2290 .dedup_window_ms
2291 .map(Value::UnsignedInteger)
2292 .unwrap_or(Value::Null),
2293 );
2294 insert_meta_row(store, fields).map(|_| ())
2295}
2296
2297fn remove_queue_metadata(store: &UnifiedStore, queue: &str) {
2298 remove_meta_rows(store, |row| {
2299 row_text(row, "queue").as_deref() == Some(queue)
2300 });
2301}
2302
2303fn queue_group_exists(store: &UnifiedStore, queue: &str, group: &str) -> RedDBResult<bool> {
2304 Ok(load_queue_groups(store, queue)?
2305 .into_iter()
2306 .any(|entry| entry.group == group))
2307}
2308
2309pub(super) fn require_queue_group(
2310 store: &UnifiedStore,
2311 queue: &str,
2312 group: &str,
2313) -> RedDBResult<()> {
2314 if queue_group_exists(store, queue, group)? {
2315 Ok(())
2316 } else {
2317 Err(RedDBError::NotFound(format!(
2318 "consumer group '{}' not found on queue '{}'",
2319 group, queue
2320 )))
2321 }
2322}
2323
2324pub(super) fn resolve_read_group(
2325 store: &UnifiedStore,
2326 queue: &str,
2327 group: Option<&str>,
2328 consumer: &str,
2329 config: &QueueRuntimeConfig,
2330) -> RedDBResult<String> {
2331 if let Some(group) = group {
2332 require_queue_group(store, queue, group)?;
2333 return Ok(group.to_string());
2334 }
2335
2336 match config.mode {
2337 QueueMode::Work => {
2338 if !queue_group_exists(store, queue, WORK_DEFAULT_GROUP)? {
2339 save_queue_group(store, queue, WORK_DEFAULT_GROUP)?;
2340 }
2341 Ok(WORK_DEFAULT_GROUP.to_string())
2342 }
2343 QueueMode::Fanout => {
2344 let fanout_group = format!("{FANOUT_GROUP_PREFIX}{consumer}");
2345 if !queue_group_exists(store, queue, &fanout_group)? {
2346 save_queue_group(store, queue, &fanout_group)?;
2347 }
2348 Ok(fanout_group)
2349 }
2350 }
2351}
2352
2353fn load_queue_groups(store: &UnifiedStore, queue: &str) -> RedDBResult<Vec<QueueGroupEntry>> {
2354 let Some(manager) = store.get_collection(QUEUE_META_COLLECTION) else {
2355 return Ok(Vec::new());
2356 };
2357 if let Some(rows) =
2358 super::queue_storage_index::subject_rows_store(store, "queue_group", queue, None, None)?
2359 {
2360 return Ok(rows
2361 .into_iter()
2362 .filter_map(|(entity_id, row)| {
2363 Some(QueueGroupEntry {
2364 entity_id,
2365 group: row_text(&row, "group")?,
2366 })
2367 })
2368 .collect());
2369 }
2370 let _publication = manager
2371 .has_backing()
2372 .then(|| store.vault_publication_lock.lock());
2373 let mut groups = Vec::new();
2374 super::function_budget::try_scan(
2375 |visit| manager.try_for_each_entity(usize::MAX, visit),
2376 |entity| {
2377 let Some(row) = entity.data.as_row() else {
2378 return true;
2379 };
2380 if row_text(row, "kind").as_deref() == Some("queue_group")
2381 && row_text(row, "queue").as_deref() == Some(queue)
2382 {
2383 let Some(group) = row_text(row, "group") else {
2384 return true;
2385 };
2386 groups.push(QueueGroupEntry {
2387 entity_id: entity.id,
2388 group,
2389 });
2390 }
2391 true
2392 },
2393 )?;
2394 Ok(groups)
2395}
2396
2397fn save_queue_group(store: &UnifiedStore, queue: &str, group: &str) -> RedDBResult<()> {
2398 let mut fields = HashMap::new();
2399 fields.insert("kind".to_string(), Value::text("queue_group".to_string()));
2400 fields.insert("queue".to_string(), Value::text(queue.to_string()));
2401 fields.insert("group".to_string(), Value::text(group.to_string()));
2402 fields.insert(
2403 "created_at_ns".to_string(),
2404 Value::UnsignedInteger(now_ns()),
2405 );
2406 insert_meta_row(store, fields).map(|_| ())
2407}
2408
2409pub(super) fn load_pending_entries(
2410 store: &UnifiedStore,
2411 queue: &str,
2412 group: Option<&str>,
2413 message_id: Option<EntityId>,
2414) -> RedDBResult<Vec<QueuePendingEntry>> {
2415 let Some(manager) = store.get_collection(QUEUE_META_COLLECTION) else {
2416 return Ok(Vec::new());
2417 };
2418 let lock_deadline_ns = load_queue_config(store, queue)
2419 .lock_deadline_ms
2420 .saturating_mul(1_000_000);
2421 let indexed_attempts = super::queue_storage_index::subject_rows_store(
2422 store,
2423 "queue_attempts_lc",
2424 queue,
2425 group,
2426 message_id.map(|id| id.raw()),
2427 )?;
2428 let attempts = indexed_attempts.unwrap_or_else(|| {
2429 manager
2430 .query_all(|entity| {
2431 entity.data.as_row().is_some_and(|row| {
2432 row_text(row, "kind").as_deref() == Some("queue_attempts_lc")
2433 && row_text(row, "queue").as_deref() == Some(queue)
2434 })
2435 })
2436 .into_iter()
2437 .filter_map(|entity| match entity.data {
2438 EntityData::Row(row) => Some((entity.id, row)),
2439 _ => None,
2440 })
2441 .collect()
2442 });
2443 let attempts_by_key: HashMap<(String, String, u64), u64> = attempts
2444 .into_iter()
2445 .filter_map(|(_, row)| {
2446 Some((
2447 (
2448 row_text(&row, "queue")?,
2449 row_text(&row, "group")?,
2450 row_u64(&row, "message_id")?,
2451 ),
2452 row_u64(&row, "attempts").unwrap_or(1),
2453 ))
2454 })
2455 .collect();
2456 let mut pending = Vec::new();
2457 for kind in ["queue_pending", "queue_pending_lc"] {
2458 match super::queue_storage_index::subject_rows_store(
2459 store,
2460 kind,
2461 queue,
2462 group,
2463 message_id.map(|id| id.raw()),
2464 )? {
2465 Some(rows) => pending.extend(rows),
2466 None => pending.extend(
2467 manager
2468 .query_all(|entity| {
2469 entity.data.as_row().is_some_and(|row| {
2470 row_text(row, "kind").as_deref() == Some(kind)
2471 && row_text(row, "queue").as_deref() == Some(queue)
2472 && group
2473 .map(|group_name| {
2474 row_text(row, "group").as_deref() == Some(group_name)
2475 })
2476 .unwrap_or(true)
2477 && message_id
2478 .map(|candidate| {
2479 row_u64(row, "message_id") == Some(candidate.raw())
2480 })
2481 .unwrap_or(true)
2482 })
2483 })
2484 .into_iter()
2485 .filter_map(|entity| match entity.data {
2486 EntityData::Row(row) => Some((entity.id, row)),
2487 _ => None,
2488 }),
2489 ),
2490 }
2491 }
2492 pending.sort_unstable_by_key(|(id, _)| id.raw());
2493 let snapshot = super::execution_context::capture_current_snapshot();
2494 if snapshot
2495 .as_ref()
2496 .is_some_and(|snapshot| snapshot.branch.is_some())
2497 {
2498 let mut visible = HashSet::new();
2499 for (id, row) in &pending {
2500 if let Some(message) = row_u64(row, "message_id") {
2501 if store
2502 .try_get(queue, EntityId::new(message), usize::MAX)?
2503 .is_some_and(|entity| {
2504 super::execution_context::entity_visible_with_context(
2505 snapshot.as_ref(),
2506 &entity,
2507 )
2508 })
2509 {
2510 visible.insert(*id);
2511 }
2512 }
2513 }
2514 pending.retain(|(id, _)| visible.contains(id));
2515 }
2516 Ok(pending
2517 .into_iter()
2518 .filter_map(|(entity_id, row)| {
2519 let group = row_text(&row, "group")?;
2520 let message_id = row_u64(&row, "message_id")?;
2521 let kind = row_text(&row, "kind")?;
2522 let delivered_at_ns = if kind == "queue_pending_lc" {
2523 row_u64(&row, "lock_deadline_ns")
2524 .unwrap_or(0)
2525 .saturating_sub(lock_deadline_ns)
2526 } else {
2527 row_u64(&row, "delivered_at_ns")?
2528 };
2529 let delivery_count = if kind == "queue_pending_lc" {
2530 attempts_by_key
2531 .get(&(queue.to_string(), group.clone(), message_id))
2532 .copied()
2533 .unwrap_or(1)
2534 } else {
2535 row_u64(&row, "delivery_count").unwrap_or(1)
2536 };
2537 Some(QueuePendingEntry {
2538 entity_id,
2539 group,
2540 message_id: EntityId::new(message_id),
2541 consumer: row_text(&row, "consumer").unwrap_or_default(),
2542 delivered_at_ns,
2543 delivery_count: delivery_count as u32,
2544 })
2545 })
2546 .collect())
2547}
2548
2549pub(super) fn save_queue_pending(
2550 store: &UnifiedStore,
2551 queue: &str,
2552 group: &str,
2553 message_id: EntityId,
2554 consumer: &str,
2555 delivered_at_ns: u64,
2556 delivery_count: u32,
2557) -> RedDBResult<()> {
2558 match super::queue_storage_index::subject_rows_store(
2559 store,
2560 "queue_pending",
2561 queue,
2562 Some(group),
2563 Some(message_id.raw()),
2564 )? {
2565 Some(rows) => {
2566 for (id, _) in rows {
2567 let _ = super::vcs_queue::delete_metadata(store, id);
2568 }
2569 }
2570 None => remove_meta_rows(store, |row| {
2571 row_text(row, "kind").as_deref() == Some("queue_pending")
2572 && row_text(row, "queue").as_deref() == Some(queue)
2573 && row_text(row, "group").as_deref() == Some(group)
2574 && row_u64(row, "message_id") == Some(message_id.raw())
2575 }),
2576 }
2577
2578 let mut fields = HashMap::new();
2579 fields.insert("kind".to_string(), Value::text("queue_pending".to_string()));
2580 fields.insert("queue".to_string(), Value::text(queue.to_string()));
2581 fields.insert("group".to_string(), Value::text(group.to_string()));
2582 fields.insert(
2583 "message_id".to_string(),
2584 Value::UnsignedInteger(message_id.raw()),
2585 );
2586 fields.insert("consumer".to_string(), Value::text(consumer.to_string()));
2587 fields.insert(
2588 "delivered_at_ns".to_string(),
2589 Value::UnsignedInteger(delivered_at_ns),
2590 );
2591 fields.insert(
2592 "delivery_count".to_string(),
2593 Value::UnsignedInteger(u64::from(delivery_count)),
2594 );
2595 insert_meta_row(store, fields).map(|_| ())
2596}
2597
2598pub(super) fn require_pending_entry(
2599 store: &UnifiedStore,
2600 queue: &str,
2601 group: &str,
2602 message_id: EntityId,
2603) -> RedDBResult<QueuePendingEntry> {
2604 load_pending_entries(store, queue, Some(group), Some(message_id))?
2605 .into_iter()
2606 .next()
2607 .ok_or_else(|| {
2608 RedDBError::NotFound(format!(
2609 "message '{}' is not pending in group '{}' on queue '{}'",
2610 message_id.raw(),
2611 group,
2612 queue
2613 ))
2614 })
2615}
2616
2617pub(super) fn load_ack_entries(
2618 store: &UnifiedStore,
2619 queue: &str,
2620 group: Option<&str>,
2621 message_id: Option<EntityId>,
2622) -> RedDBResult<Vec<QueueAckEntry>> {
2623 let Some(manager) = store.get_collection(QUEUE_META_COLLECTION) else {
2624 return Ok(Vec::new());
2625 };
2626 let rows = match super::queue_storage_index::subject_rows_store(
2627 store,
2628 "queue_ack",
2629 queue,
2630 group,
2631 message_id.map(|id| id.raw()),
2632 )? {
2633 Some(rows) => rows,
2634 None => manager
2635 .query_all(|entity| {
2636 entity.data.as_row().is_some_and(|row| {
2637 row_text(row, "kind").as_deref() == Some("queue_ack")
2638 && row_text(row, "queue").as_deref() == Some(queue)
2639 && group
2640 .map(|group_name| row_text(row, "group").as_deref() == Some(group_name))
2641 .unwrap_or(true)
2642 && message_id
2643 .map(|candidate| row_u64(row, "message_id") == Some(candidate.raw()))
2644 .unwrap_or(true)
2645 })
2646 })
2647 .into_iter()
2648 .filter_map(|entity| match entity.data {
2649 EntityData::Row(row) => Some((entity.id, row)),
2650 _ => None,
2651 })
2652 .collect(),
2653 };
2654 Ok(rows
2655 .into_iter()
2656 .filter_map(|(entity_id, row)| {
2657 Some(QueueAckEntry {
2658 entity_id,
2659 group: row_text(&row, "group")?,
2660 message_id: EntityId::new(row_u64(&row, "message_id")?),
2661 })
2662 })
2663 .collect())
2664}
2665
2666pub(super) fn save_queue_ack(
2667 store: &UnifiedStore,
2668 queue: &str,
2669 group: &str,
2670 message_id: EntityId,
2671) -> RedDBResult<()> {
2672 let existing = load_ack_entries(store, queue, Some(group), Some(message_id))?;
2673 if !existing.is_empty() {
2674 return Ok(());
2675 }
2676
2677 let mut fields = HashMap::new();
2678 fields.insert("kind".to_string(), Value::text("queue_ack".to_string()));
2679 fields.insert("queue".to_string(), Value::text(queue.to_string()));
2680 fields.insert("group".to_string(), Value::text(group.to_string()));
2681 fields.insert(
2682 "message_id".to_string(),
2683 Value::UnsignedInteger(message_id.raw()),
2684 );
2685 fields.insert("acked_at_ns".to_string(), Value::UnsignedInteger(now_ns()));
2686 insert_meta_row(store, fields).map(|_| ())
2687}
2688
2689pub(super) fn queue_message_completed_for_all_groups(
2690 store: &UnifiedStore,
2691 queue: &str,
2692 message_id: EntityId,
2693) -> RedDBResult<bool> {
2694 let groups = load_queue_groups(store, queue)?;
2695 let pending = load_pending_entries(store, queue, None, Some(message_id))?;
2696 if !pending.is_empty() {
2697 return Ok(false);
2698 }
2699 if groups.is_empty() {
2700 return Ok(true);
2701 }
2702
2703 let acked_groups = load_ack_entries(store, queue, None, Some(message_id))?
2704 .into_iter()
2705 .map(|entry| entry.group)
2706 .collect::<HashSet<_>>();
2707 Ok(groups
2708 .into_iter()
2709 .all(|group| acked_groups.contains(&group.group)))
2710}
2711
2712fn load_queue_message_views(
2713 store: &UnifiedStore,
2714 queue: &str,
2715) -> RedDBResult<Vec<QueueMessageView>> {
2716 load_queue_message_views_with_runtime(None, store, queue)
2717}
2718
2719fn queue_capacity_reached_by_scan(
2721 runtime: &RedDBRuntime,
2722 store: &UnifiedStore,
2723 queue: &str,
2724 limit: usize,
2725) -> RedDBResult<bool> {
2726 let manager = store
2727 .get_collection(queue)
2728 .ok_or_else(|| RedDBError::NotFound(format!("queue '{queue}' not found")))?;
2729 let snapshot = super::impl_core::capture_current_snapshot();
2730 let policy = super::impl_core::rls_policy_filter_for_kind(
2731 runtime,
2732 queue,
2733 reddb_rql::ast::PolicyAction::Select,
2734 reddb_rql::ast::PolicyTargetKind::Messages,
2735 );
2736 if runtime.is_rls_enabled(queue) && policy.is_none() {
2737 return Ok(limit == 0);
2738 }
2739 let mut count = 0usize;
2740 manager.try_scan_for_each(
2741 snapshot.as_ref(),
2742 manager.backing_read_bytes_max().unwrap_or(usize::MAX),
2743 |entity| {
2744 if !matches!(entity.kind, EntityKind::QueueMessage { .. })
2745 || !matches!(entity.data, EntityData::QueueMessage(_))
2746 {
2747 return true;
2748 }
2749 if policy.as_ref().is_none_or(|filter| {
2750 super::query_exec::evaluate_entity_filter_with_db(
2751 Some(&runtime.inner.db),
2752 entity,
2753 filter,
2754 queue,
2755 queue,
2756 )
2757 }) {
2758 count += 1;
2759 }
2760 count < limit
2761 },
2762 )?;
2763 Ok(count >= limit)
2764}
2765
2766pub(super) fn load_queue_message_views_with_runtime(
2773 runtime: Option<&RedDBRuntime>,
2774 store: &UnifiedStore,
2775 queue: &str,
2776) -> RedDBResult<Vec<QueueMessageView>> {
2777 let manager = store
2778 .get_collection(queue)
2779 .ok_or_else(|| RedDBError::NotFound(format!("queue '{}' not found", queue)))?;
2780 let snap_ctx = crate::runtime::impl_core::capture_current_snapshot();
2784 let rls_filter = runtime.and_then(|rt| {
2785 crate::runtime::impl_core::rls_policy_filter_for_kind(
2786 rt,
2787 queue,
2788 reddb_rql::ast::PolicyAction::Select,
2789 reddb_rql::ast::PolicyTargetKind::Messages,
2790 )
2791 });
2792 let rls_enabled_but_denied = runtime.map(|rt| rt.is_rls_enabled(queue)).unwrap_or(false)
2793 && rls_filter.is_none()
2794 && runtime.is_some();
2795 if rls_enabled_but_denied {
2796 return Ok(Vec::new());
2798 }
2799 let mut views = Vec::new();
2800 manager.try_scan_for_each(
2801 snap_ctx.as_ref(),
2802 manager.backing_read_bytes_max().unwrap_or(usize::MAX),
2803 |entity| {
2804 if !matches!(entity.kind, EntityKind::QueueMessage { .. }) {
2805 return true;
2806 }
2807 if let (Some(filter), Some(rt)) = (rls_filter.as_ref(), runtime) {
2808 if !crate::runtime::query_exec::evaluate_entity_filter_with_db(
2809 Some(&rt.inner.db),
2810 entity,
2811 filter,
2812 queue,
2813 queue,
2814 ) {
2815 return true;
2816 }
2817 }
2818 if let Some(mut view) = queue_message_view_from_entity(entity.clone()) {
2819 view.available_at_ns = read_message_available_at_ns(store, queue, view.id);
2820 view.ordering_key = read_message_ordering_key(store, queue, view.id);
2821 views.push(view);
2822 }
2823 true
2824 },
2825 )?;
2826 Ok(views)
2827}
2828
2829fn queue_message_view_from_entity(entity: UnifiedEntity) -> Option<QueueMessageView> {
2830 let (position, _) = match &entity.kind {
2831 EntityKind::QueueMessage { position, queue } => (*position, queue),
2832 _ => return None,
2833 };
2834 let data = match entity.data {
2835 EntityData::QueueMessage(data) => data,
2836 _ => return None,
2837 };
2838 Some(QueueMessageView {
2839 id: entity.id,
2840 position,
2841 priority: data.priority.unwrap_or(0),
2842 payload: data.payload,
2843 attempts: data.attempts,
2844 max_attempts: data.max_attempts,
2845 enqueued_at_ns: data.enqueued_at_ns,
2846 available_at_ns: None,
2847 ordering_key: None,
2848 })
2849}
2850
2851pub(super) fn insert_moved_queue_message_payload(
2858 store: &UnifiedStore,
2859 queue: &str,
2860 payload: &Value,
2861) -> RedDBResult<EntityId> {
2862 let config = load_queue_config(store, queue);
2863 let position = next_queue_position(store, queue, QueueSide::Right)?;
2864 let enqueued_at_ns = std::time::SystemTime::now()
2865 .duration_since(std::time::UNIX_EPOCH)
2866 .map(|d| d.as_nanos() as u64)
2867 .unwrap_or(0);
2868 let mut entity = UnifiedEntity::new(
2869 EntityId::new(0),
2870 EntityKind::QueueMessage {
2871 queue: queue.to_string(),
2872 position,
2873 },
2874 EntityData::QueueMessage(QueueMessageData {
2875 payload: payload.clone(),
2876 priority: None,
2877 enqueued_at_ns,
2878 attempts: 0,
2879 max_attempts: config.max_attempts,
2880 acked: false,
2881 }),
2882 );
2883 super::vcs_queue::stamp_entity(store, queue, &mut entity)?;
2884 let id = store
2885 .insert_auto(queue, entity)
2886 .map_err(|err| RedDBError::Internal(err.to_string()))?;
2887 if let Some(ttl_ms) = config.ttl_ms {
2888 store
2889 .set_metadata(queue, id, queue_message_ttl_metadata(ttl_ms))
2890 .map_err(|err| RedDBError::Internal(err.to_string()))?;
2891 }
2892 Ok(id)
2893}
2894
2895fn insert_moved_queue_message(
2896 store: &UnifiedStore,
2897 queue: &str,
2898 config: &QueueRuntimeConfig,
2899 message: &QueueMessageView,
2900) -> RedDBResult<EntityId> {
2901 let position = next_queue_position(store, queue, QueueSide::Right)?;
2902 let mut entity = UnifiedEntity::new(
2903 EntityId::new(0),
2904 EntityKind::QueueMessage {
2905 queue: queue.to_string(),
2906 position,
2907 },
2908 EntityData::QueueMessage(QueueMessageData {
2909 payload: message.payload.clone(),
2910 priority: if config.priority {
2911 Some(message.priority)
2912 } else {
2913 None
2914 },
2915 enqueued_at_ns: message.enqueued_at_ns,
2916 attempts: 0,
2917 max_attempts: config.max_attempts,
2918 acked: false,
2919 }),
2920 );
2921 super::vcs_queue::stamp_entity(store, queue, &mut entity)?;
2922 let id = store
2923 .insert_auto(queue, entity)
2924 .map_err(|err| RedDBError::Internal(err.to_string()))?;
2925 if config.ttl_ms.is_some() || message.ordering_key.is_some() {
2926 store
2927 .set_metadata(
2928 queue,
2929 id,
2930 queue_message_metadata(config.ttl_ms, None, message.ordering_key.as_deref()),
2931 )
2932 .map_err(|err| RedDBError::Internal(err.to_string()))?;
2933 }
2934 Ok(id)
2935}
2936
2937fn queue_projection_default_columns() -> Vec<String> {
2938 [
2939 "id",
2940 "payload",
2941 "priority",
2942 "attempts",
2943 "last_error",
2944 "enqueued_at",
2945 "available_at",
2946 "key",
2947 "dlq",
2948 "tenant",
2949 ]
2950 .into_iter()
2951 .map(str::to_string)
2952 .collect()
2953}
2954
2955fn queue_projection_record(
2956 columns: &[String],
2957 message: &QueueMessageView,
2958 dlq: bool,
2959) -> RedDBResult<UnifiedRecord> {
2960 let mut record = UnifiedRecord::new();
2961 for column in columns {
2962 let value = queue_projection_value(message, dlq, column).ok_or_else(|| {
2963 RedDBError::Query(format!("unknown queue projection column '{}'", column))
2964 })?;
2965 record.set(column, value);
2966 }
2967 Ok(record)
2968}
2969
2970fn queue_projection_value(message: &QueueMessageView, dlq: bool, column: &str) -> Option<Value> {
2971 match column {
2972 "id" => Some(Value::text(message_id_string(message.id))),
2973 "payload" => Some(message.payload.clone()),
2974 "priority" => Some(Value::Integer(i64::from(message.priority))),
2975 "attempts" => Some(Value::UnsignedInteger(u64::from(message.attempts))),
2976 "last_error" => Some(Value::Null),
2977 "enqueued_at" => Some(Value::UnsignedInteger(message.enqueued_at_ns)),
2978 "available_at" => Some(Value::UnsignedInteger(
2979 message.available_at_ns.unwrap_or(message.enqueued_at_ns),
2980 )),
2981 "key" => Some(
2982 message
2983 .ordering_key
2984 .as_ref()
2985 .map(|key| Value::text(key.clone()))
2986 .unwrap_or(Value::Null),
2987 ),
2988 "dlq" => Some(Value::Boolean(dlq)),
2989 "tenant" => queue_message_tenant(&message.payload).or(Some(Value::Null)),
2990 _ => None,
2991 }
2992}
2993
2994fn queue_message_tenant(payload: &Value) -> Option<Value> {
2995 let Value::Json(bytes) = payload else {
2996 return None;
2997 };
2998 let json: crate::json::Value = crate::json::from_slice(bytes).ok()?;
2999 json.get("tenant")
3000 .and_then(crate::json::Value::as_str)
3001 .map(|tenant| Value::text(tenant.to_string()))
3002}
3003
3004fn queue_is_dead_letter_target(store: &UnifiedStore, queue: &str) -> bool {
3005 let Some(manager) = store.get_collection(QUEUE_META_COLLECTION) else {
3006 return false;
3007 };
3008 !manager
3009 .query_all(|entity| {
3010 entity.data.as_row().is_some_and(|row| {
3011 row_text(row, "kind").as_deref() == Some("queue_config")
3012 && row_text(row, "dlq").as_deref() == Some(queue)
3013 })
3014 })
3015 .is_empty()
3016}
3017
3018fn queue_message_matches_filter(message: &QueueMessageView, dlq: bool, filter: &Filter) -> bool {
3019 match filter {
3020 Filter::Compare { field, op, value } => queue_filter_field_value(message, dlq, field)
3021 .is_some_and(|candidate| queue_compare_values(&candidate, value, *op)),
3022 Filter::CompareFields { left, op, right } => {
3023 match (
3024 queue_filter_field_value(message, dlq, left),
3025 queue_filter_field_value(message, dlq, right),
3026 ) {
3027 (Some(left), Some(right)) => queue_compare_values(&left, &right, *op),
3028 _ => false,
3029 }
3030 }
3031 Filter::And(left, right) => {
3032 queue_message_matches_filter(message, dlq, left)
3033 && queue_message_matches_filter(message, dlq, right)
3034 }
3035 Filter::Or(left, right) => {
3036 queue_message_matches_filter(message, dlq, left)
3037 || queue_message_matches_filter(message, dlq, right)
3038 }
3039 Filter::Not(inner) => !queue_message_matches_filter(message, dlq, inner),
3040 Filter::IsNull(field) => queue_filter_field_value(message, dlq, field)
3041 .is_none_or(|value| matches!(value, Value::Null)),
3042 Filter::IsNotNull(field) => queue_filter_field_value(message, dlq, field)
3043 .is_some_and(|value| !matches!(value, Value::Null)),
3044 Filter::In { field, values } => {
3045 queue_filter_field_value(message, dlq, field).is_some_and(|candidate| {
3046 values
3047 .iter()
3048 .any(|value| queue_values_equal(&candidate, value))
3049 })
3050 }
3051 Filter::Between { field, low, high } => queue_filter_field_value(message, dlq, field)
3052 .is_some_and(|candidate| {
3053 queue_compare_values(&candidate, low, CompareOp::Ge)
3054 && queue_compare_values(&candidate, high, CompareOp::Le)
3055 }),
3056 Filter::Like { field, pattern } => queue_filter_text(message, dlq, field)
3057 .is_some_and(|value| queue_like_matches(&value, pattern)),
3058 Filter::StartsWith { field, prefix } => {
3059 queue_filter_text(message, dlq, field).is_some_and(|value| value.starts_with(prefix))
3060 }
3061 Filter::EndsWith { field, suffix } => {
3062 queue_filter_text(message, dlq, field).is_some_and(|value| value.ends_with(suffix))
3063 }
3064 Filter::Contains { field, substring } => {
3065 queue_filter_text(message, dlq, field).is_some_and(|value| value.contains(substring))
3066 }
3067 Filter::CompareExpr { .. } => false,
3068 }
3069}
3070
3071fn queue_filter_field_value(
3072 message: &QueueMessageView,
3073 dlq: bool,
3074 field: &FieldRef,
3075) -> Option<Value> {
3076 match field {
3077 FieldRef::TableColumn { table, column } if table.is_empty() => {
3078 queue_projection_value(message, dlq, column)
3079 .or_else(|| queue_payload_field_value(&message.payload, column))
3080 }
3081 FieldRef::TableColumn { column, .. } => queue_projection_value(message, dlq, column)
3082 .or_else(|| queue_payload_field_value(&message.payload, column)),
3083 _ => None,
3084 }
3085}
3086
3087fn queue_payload_field_value(payload: &Value, field: &str) -> Option<Value> {
3088 let Value::Json(bytes) = payload else {
3089 return None;
3090 };
3091 let json: crate::json::Value = crate::json::from_slice(bytes).ok()?;
3092 let value = json.get(field)?;
3093 json_value_to_schema_value(value)
3094}
3095
3096fn json_value_to_schema_value(value: &crate::json::Value) -> Option<Value> {
3097 if matches!(value, crate::json::Value::Null) {
3098 Some(Value::Null)
3099 } else if let Some(value) = value.as_bool() {
3100 Some(Value::Boolean(value))
3101 } else if let Some(value) = value.as_i64() {
3102 Some(Value::Integer(value))
3103 } else if let Some(value) = value.as_u64() {
3104 Some(Value::UnsignedInteger(value))
3105 } else if let Some(value) = value.as_f64() {
3106 Some(Value::Float(value))
3107 } else if let Some(value) = value.as_str() {
3108 Some(Value::text(value.to_string()))
3109 } else {
3110 Some(Value::Json(value.to_string_compact().into_bytes()))
3111 }
3112}
3113
3114fn queue_filter_text(message: &QueueMessageView, dlq: bool, field: &FieldRef) -> Option<String> {
3115 queue_filter_field_value(message, dlq, field).and_then(|value| match value {
3116 Value::Text(value) => Some(value.to_string()),
3117 Value::NodeRef(value) | Value::EdgeRef(value) | Value::TableRef(value) => Some(value),
3118 Value::Integer(value) => Some(value.to_string()),
3119 Value::UnsignedInteger(value) => Some(value.to_string()),
3120 Value::Float(value) => Some(value.to_string()),
3121 Value::Boolean(value) => Some(value.to_string()),
3122 _ => None,
3123 })
3124}
3125
3126fn queue_compare_values(left: &Value, right: &Value, op: CompareOp) -> bool {
3127 match op {
3128 CompareOp::Eq => queue_values_equal(left, right),
3129 CompareOp::Ne => !queue_values_equal(left, right),
3130 CompareOp::Lt => queue_partial_cmp(left, right).is_some_and(|ord| ord.is_lt()),
3131 CompareOp::Le => queue_partial_cmp(left, right).is_some_and(|ord| !ord.is_gt()),
3132 CompareOp::Gt => queue_partial_cmp(left, right).is_some_and(|ord| ord.is_gt()),
3133 CompareOp::Ge => queue_partial_cmp(left, right).is_some_and(|ord| !ord.is_lt()),
3134 }
3135}
3136
3137fn queue_values_equal(left: &Value, right: &Value) -> bool {
3138 if let (Some(left), Some(right)) = (queue_value_number(left), queue_value_number(right)) {
3139 return (left - right).abs() < f64::EPSILON;
3140 }
3141 match (left, right) {
3142 (Value::Text(left), Value::Text(right)) => left == right,
3143 (Value::Boolean(left), Value::Boolean(right)) => left == right,
3144 _ => left == right,
3145 }
3146}
3147
3148fn queue_partial_cmp(left: &Value, right: &Value) -> Option<std::cmp::Ordering> {
3149 if let (Some(left), Some(right)) = (queue_value_number(left), queue_value_number(right)) {
3150 return left.partial_cmp(&right);
3151 }
3152 match (left, right) {
3153 (Value::Text(left), Value::Text(right)) => Some(left.cmp(right)),
3154 _ => None,
3155 }
3156}
3157
3158fn queue_value_number(value: &Value) -> Option<f64> {
3159 match value {
3160 Value::Integer(value) => Some(*value as f64),
3161 Value::UnsignedInteger(value) => Some(*value as f64),
3162 Value::Float(value) => Some(*value),
3163 Value::Text(value) => value.parse().ok(),
3164 _ => None,
3165 }
3166}
3167
3168fn queue_like_matches(value: &str, pattern: &str) -> bool {
3169 if pattern == "%" {
3170 return true;
3171 }
3172 let starts_wild = pattern.starts_with('%');
3173 let ends_wild = pattern.ends_with('%');
3174 let needle = pattern.trim_matches('%');
3175 match (starts_wild, ends_wild) {
3176 (true, true) => value.contains(needle),
3177 (true, false) => value.ends_with(needle),
3178 (false, true) => value.starts_with(needle),
3179 (false, false) => value == needle,
3180 }
3181}
3182
3183pub(super) fn queue_message_view_by_id(
3184 store: &UnifiedStore,
3185 queue: &str,
3186 message_id: EntityId,
3187) -> RedDBResult<Option<QueueMessageView>> {
3188 let manager = queue_manager(store, queue)?;
3189 Ok(manager
3190 .get(message_id)
3191 .and_then(queue_message_view_from_entity)
3192 .map(|mut view| {
3193 view.available_at_ns = read_message_available_at_ns(store, queue, view.id);
3194 view
3195 }))
3196}
3197
3198pub(super) fn sort_queue_messages(
3199 messages: &mut [QueueMessageView],
3200 config: &QueueRuntimeConfig,
3201 side: QueueSide,
3202) {
3203 messages.sort_by(|left, right| {
3204 if config.priority {
3205 right
3206 .priority
3207 .cmp(&left.priority)
3208 .then_with(|| match side {
3209 QueueSide::Left => left.position.cmp(&right.position),
3210 QueueSide::Right => right.position.cmp(&left.position),
3211 })
3212 .then_with(|| left.id.raw().cmp(&right.id.raw()))
3213 } else {
3214 match side {
3215 QueueSide::Left => left.position.cmp(&right.position),
3216 QueueSide::Right => right.position.cmp(&left.position),
3217 }
3218 .then_with(|| left.id.raw().cmp(&right.id.raw()))
3219 }
3220 });
3221}
3222
3223pub(super) fn next_queue_position(
3224 store: &UnifiedStore,
3225 queue: &str,
3226 side: QueueSide,
3227) -> RedDBResult<u64> {
3228 if let Some(boundary) = super::queue_storage_index::position_boundary(
3229 store,
3230 queue,
3231 matches!(side, QueueSide::Right),
3232 )? {
3233 return Ok(
3234 boundary.map_or(QUEUE_POSITION_CENTER, |position| match side {
3235 QueueSide::Left => position.saturating_sub(1),
3236 QueueSide::Right => position.saturating_add(1),
3237 }),
3238 );
3239 }
3240 let manager = store
3241 .get_collection(queue)
3242 .ok_or_else(|| RedDBError::NotFound(format!("queue '{queue}' not found")))?;
3243 let snapshot = crate::runtime::impl_core::capture_current_snapshot();
3244 let mut boundary: Option<u64> = None;
3245 manager.try_scan_for_each(snapshot.as_ref(), usize::MAX, |entity| {
3248 let EntityKind::QueueMessage { position, .. } = &entity.kind else {
3249 return true;
3250 };
3251 if !matches!(entity.data, EntityData::QueueMessage(_)) {
3252 return true;
3253 }
3254 boundary = Some(boundary.map_or(*position, |current| match side {
3255 QueueSide::Left => current.min(*position),
3256 QueueSide::Right => current.max(*position),
3257 }));
3258 true
3259 })?;
3260 Ok(
3261 boundary.map_or(QUEUE_POSITION_CENTER, |position| match side {
3262 QueueSide::Left => position.saturating_sub(1),
3263 QueueSide::Right => position.saturating_add(1),
3264 }),
3265 )
3266}
3267
3268pub(super) fn increment_queue_attempts(
3269 store: &UnifiedStore,
3270 queue: &str,
3271 message_id: EntityId,
3272) -> RedDBResult<u32> {
3273 let manager = queue_manager(store, queue)?;
3274 let mut entity = manager
3275 .get(message_id)
3276 .ok_or_else(|| RedDBError::NotFound(format!("message '{}' not found", message_id.raw())))?;
3277 match &mut entity.data {
3278 EntityData::QueueMessage(message) => {
3279 message.attempts = message.attempts.saturating_add(1);
3280 let attempts = message.attempts;
3281 manager
3282 .update(entity)
3283 .map_err(|err| RedDBError::Internal(err.to_string()))?;
3284 Ok(attempts)
3285 }
3286 _ => Err(RedDBError::Query(format!(
3287 "entity '{}' is not a queue message",
3288 message_id.raw()
3289 ))),
3290 }
3291}
3292
3293pub(super) fn queue_message_attempts(
3294 store: &UnifiedStore,
3295 queue: &str,
3296 message_id: EntityId,
3297) -> RedDBResult<u32> {
3298 Ok(queue_message_data(store, queue, message_id)?.attempts)
3299}
3300
3301pub(super) fn queue_message_max_attempts(
3302 store: &UnifiedStore,
3303 queue: &str,
3304 message_id: EntityId,
3305) -> RedDBResult<u32> {
3306 Ok(queue_message_data(store, queue, message_id)?.max_attempts)
3307}
3308
3309pub(super) fn queue_message_payload(
3310 store: &UnifiedStore,
3311 queue: &str,
3312 message_id: EntityId,
3313) -> RedDBResult<Value> {
3314 Ok(queue_message_data(store, queue, message_id)?.payload)
3315}
3316
3317pub(super) fn queue_message_pending_any(
3318 store: &UnifiedStore,
3319 queue: &str,
3320 message_id: EntityId,
3321) -> RedDBResult<bool> {
3322 Ok(!load_pending_entries(store, queue, None, Some(message_id))?.is_empty())
3323}
3324
3325pub(super) fn queue_message_pending_for_group(
3326 store: &UnifiedStore,
3327 queue: &str,
3328 group: &str,
3329 message_id: EntityId,
3330) -> RedDBResult<bool> {
3331 Ok(!load_pending_entries(store, queue, Some(group), Some(message_id))?.is_empty())
3332}
3333
3334pub(super) fn queue_message_acked_for_group(
3335 store: &UnifiedStore,
3336 queue: &str,
3337 group: &str,
3338 message_id: EntityId,
3339) -> RedDBResult<bool> {
3340 Ok(!load_ack_entries(store, queue, Some(group), Some(message_id))?.is_empty())
3341}
3342
3343fn queue_manager(
3344 store: &UnifiedStore,
3345 queue: &str,
3346) -> RedDBResult<Arc<crate::storage::unified::SegmentManager>> {
3347 store
3348 .get_collection(queue)
3349 .ok_or_else(|| RedDBError::NotFound(format!("queue '{}' not found", queue)))
3350}
3351
3352pub(super) fn queue_message_data(
3353 store: &UnifiedStore,
3354 queue: &str,
3355 message_id: EntityId,
3356) -> RedDBResult<QueueMessageData> {
3357 let manager = queue_manager(store, queue)?;
3358 let entity = manager
3359 .get(message_id)
3360 .ok_or_else(|| RedDBError::NotFound(format!("message '{}' not found", message_id.raw())))?;
3361 match entity.data {
3362 EntityData::QueueMessage(message) => Ok(message),
3363 _ => Err(RedDBError::Query(format!(
3364 "entity '{}' is not a queue message",
3365 message_id.raw()
3366 ))),
3367 }
3368}
3369
3370pub(super) fn insert_meta_row(
3371 store: &UnifiedStore,
3372 fields: HashMap<String, Value>,
3373) -> RedDBResult<EntityId> {
3374 let created = store.get_collection(QUEUE_META_COLLECTION).is_none();
3375 let _ = store.get_or_create_collection(QUEUE_META_COLLECTION);
3376 if created {
3377 super::queue_storage_index::prepare_meta_store(store)?;
3378 }
3379 let mut entity = UnifiedEntity::new(
3380 EntityId::new(0),
3381 EntityKind::TableRow {
3382 table: Arc::from(QUEUE_META_COLLECTION),
3383 row_id: 0,
3384 },
3385 EntityData::Row(RowData {
3386 columns: Vec::new(),
3387 named: Some(fields),
3388 schema: None,
3389 }),
3390 );
3391 super::vcs_queue::stamp_entity(store, QUEUE_META_COLLECTION, &mut entity)?;
3392 Ok(store.insert_auto(QUEUE_META_COLLECTION, entity)?)
3393}
3394
3395pub(super) fn remove_meta_rows(store: &UnifiedStore, predicate: impl Fn(&RowData) -> bool + Sync) {
3396 let rows = match super::vcs_queue::metadata_rows(store, predicate) {
3397 Ok(rows) => rows,
3398 Err(error) => {
3399 super::vcs_queue::record_error(error);
3400 return;
3401 }
3402 };
3403 for (id, _) in rows {
3404 let _ = super::vcs_queue::delete_metadata(store, id);
3405 }
3406}
3407
3408pub(super) fn reclaim_expired_queue_dedup(
3409 store: &UnifiedStore,
3410 queue: &str,
3411 now_ns: u64,
3412) -> RedDBResult<usize> {
3413 let rows = super::queue_storage_index::dedup_rows(store, queue, None, now_ns, 64)?;
3415 let count = rows.len();
3416 for (id, _) in rows {
3417 super::vcs_queue::delete_metadata(store, id)?;
3418 }
3419 Ok(count)
3420}
3421
3422pub(super) fn find_queue_dedup(
3423 store: &UnifiedStore,
3424 queue: &str,
3425 dedup_key: &str,
3426 now_ns: u64,
3427) -> RedDBResult<Option<EntityId>> {
3428 Ok(
3429 super::queue_storage_index::dedup_rows(store, queue, Some(dedup_key), now_ns, 1)?
3430 .into_iter()
3431 .next()
3432 .and_then(|(_, message)| message)
3433 .map(EntityId::new),
3434 )
3435}
3436
3437pub(super) fn record_queue_dedup(
3438 store: &UnifiedStore,
3439 queue: &str,
3440 dedup_key: &str,
3441 message_id: EntityId,
3442 expires_at_ns: u64,
3443) -> RedDBResult<EntityId> {
3444 let mut fields = HashMap::new();
3445 fields.insert(
3446 "kind".to_string(),
3447 Value::text(KIND_QUEUE_PUSH_DEDUP.to_string()),
3448 );
3449 fields.insert("queue".to_string(), Value::text(queue.to_string()));
3450 fields.insert("dedup_key".to_string(), Value::text(dedup_key.to_string()));
3451 fields.insert(
3452 "message_id".to_string(),
3453 Value::UnsignedInteger(message_id.raw()),
3454 );
3455 fields.insert(
3456 "expires_at_ns".to_string(),
3457 Value::UnsignedInteger(expires_at_ns),
3458 );
3459 insert_meta_row(store, fields)
3460}
3461
3462pub(super) fn delete_meta_entity(store: &UnifiedStore, entity_id: EntityId) {
3463 let _ = super::vcs_queue::delete_metadata(store, entity_id);
3464}
3465
3466fn queue_message_lock_key(queue: &str, message_id: EntityId) -> String {
3467 format!("{queue}:{}", message_id.raw())
3468}
3469
3470pub(super) fn queue_message_lock_handle(
3471 runtime: &RedDBRuntime,
3472 queue: &str,
3473 message_id: EntityId,
3474) -> Arc<parking_lot::Mutex<()>> {
3475 let key = queue_message_lock_key(queue, message_id);
3476 if let Some(lock) = runtime.inner.queue_message_locks.read().get(&key).cloned() {
3477 return lock;
3478 }
3479
3480 let mut locks = runtime.inner.queue_message_locks.write();
3481 locks
3482 .entry(key)
3483 .or_insert_with(|| Arc::new(parking_lot::Mutex::new(())))
3484 .clone()
3485}
3486
3487pub(super) fn forget_queue_message_lock(runtime: &RedDBRuntime, queue: &str, message_id: EntityId) {
3488 runtime
3489 .inner
3490 .queue_message_locks
3491 .write()
3492 .remove(&queue_message_lock_key(queue, message_id));
3493}
3494
3495fn parse_message_id(value: &str) -> RedDBResult<EntityId> {
3496 let raw = value.strip_prefix('e').unwrap_or(value);
3497 raw.parse::<u64>()
3498 .map(EntityId::new)
3499 .map_err(|_| RedDBError::Query(format!("invalid message id '{}'", value)))
3500}
3501
3502pub(super) fn resolve_ack_nack_handle(
3508 store: &UnifiedStore,
3509 queue: &str,
3510 group_hint: &str,
3511 message_id_hint: &str,
3512 delivery_id: Option<&str>,
3513) -> RedDBResult<(String, EntityId)> {
3514 if let Some(did) = delivery_id {
3515 return resolve_delivery_id(store, queue, did);
3516 }
3517 if group_hint.is_empty() || message_id_hint.is_empty() {
3518 return Err(RedDBError::Query(
3519 "ACK/NACK requires either GROUP <group> '<message_id>' or WITH delivery_id = '<id>'"
3520 .to_string(),
3521 ));
3522 }
3523 log_tuple_deprecation(queue);
3524 let entity = parse_message_id(message_id_hint)?;
3525 Ok((group_hint.to_string(), entity))
3526}
3527
3528fn resolve_delivery_id(
3529 store: &UnifiedStore,
3530 queue: &str,
3531 delivery_id: &str,
3532) -> RedDBResult<(String, EntityId)> {
3533 let Some(manager) = store.get_collection(QUEUE_META_COLLECTION) else {
3534 return Err(RedDBError::Query(format!(
3535 "delivery_id '{}' does not resolve to a live pending delivery",
3536 delivery_id
3537 )));
3538 };
3539 let rows = match super::queue_storage_index::delivery_rows_store(store, delivery_id)? {
3540 Some(rows) => rows,
3541 None => manager
3542 .query_all(|entity| {
3543 entity.data.as_row().is_some_and(|row| {
3544 row_text(row, "kind").as_deref() == Some("queue_pending_lc")
3545 && row_text(row, "delivery_id").as_deref() == Some(delivery_id)
3546 })
3547 })
3548 .into_iter()
3549 .filter_map(|entity| match entity.data {
3550 EntityData::Row(row) => Some((entity.id, row)),
3551 _ => None,
3552 })
3553 .collect(),
3554 };
3555 for (_, row) in rows {
3556 if row_text(&row, "kind").as_deref() == Some("queue_pending_lc") {
3557 let row_queue = row_text(&row, "queue").unwrap_or_default();
3558 let row_group = row_text(&row, "group").unwrap_or_default();
3559 let row_message = row_u64(&row, "message_id").unwrap_or(0);
3560 if row_queue != queue {
3561 return Err(RedDBError::Query(format!(
3562 "delivery_id '{}' belongs to queue '{}', not '{}'",
3563 delivery_id, row_queue, queue
3564 )));
3565 }
3566 return Ok((row_group, EntityId::new(row_message)));
3567 }
3568 }
3569 Err(RedDBError::Query(format!(
3570 "delivery_id '{}' does not resolve to a live pending delivery",
3571 delivery_id
3572 )))
3573}
3574
3575fn log_tuple_deprecation(queue: &str) {
3578 use std::sync::atomic::Ordering;
3579 use std::sync::{Mutex, OnceLock};
3580 use std::time::Instant;
3581
3582 static LAST_EMIT: OnceLock<Mutex<HashMap<(u64, String), Instant>>> = OnceLock::new();
3583 const COOLDOWN: std::time::Duration = std::time::Duration::from_secs(60);
3584
3585 let map = LAST_EMIT.get_or_init(|| Mutex::new(HashMap::new()));
3586 let key = (super::impl_core::current_connection_id(), queue.to_string());
3587 let now = Instant::now();
3588 let mut guard = match map.lock() {
3589 Ok(g) => g,
3590 Err(_) => return,
3591 };
3592 let should_emit =
3593 !matches!(guard.get(&key), Some(prev) if now.duration_since(*prev) < COOLDOWN);
3594 if should_emit {
3595 guard.insert(key.clone(), now);
3596 drop(guard);
3597 TUPLE_DEPRECATION_EMITS.fetch_add(1, Ordering::Relaxed);
3598 tracing::warn!(
3599 target: "reddb::queue_lifecycle",
3600 queue = queue,
3601 connection_id = key.0,
3602 "ACK/NACK by (queue, group, message_id) tuple is deprecated; \
3603 switch to the server-issued delivery_id (ADR 0026). \
3604 The tuple path will be removed one minor release after introduction.",
3605 );
3606 }
3607}
3608
3609pub static TUPLE_DEPRECATION_EMITS: std::sync::atomic::AtomicU64 =
3615 std::sync::atomic::AtomicU64::new(0);
3616
3617fn message_id_string(message_id: EntityId) -> String {
3618 message_id.raw().to_string()
3619}
3620
3621fn delivered_message_json(
3627 message: crate::runtime::queue_lifecycle::DeliveredMessage,
3628) -> crate::serde_json::Value {
3629 use crate::serde_json::{Map, Value as JsonValue};
3630 let mut obj = Map::new();
3631 obj.insert(
3632 "message_id".to_string(),
3633 JsonValue::String(message_id_string(EntityId::new(message.message_id))),
3634 );
3635 obj.insert("payload".to_string(), message.payload.to_json());
3636 obj.insert("consumer".to_string(), JsonValue::String(message.consumer));
3637 obj.insert(
3638 "delivery_count".to_string(),
3639 JsonValue::Number(message.delivery_count as f64),
3640 );
3641 JsonValue::Object(obj)
3642}
3643
3644pub(crate) fn pending_counts_by_group(
3649 store: &UnifiedStore,
3650) -> std::collections::BTreeMap<(String, String), u64> {
3651 let mut counts: std::collections::BTreeMap<(String, String), u64> =
3652 std::collections::BTreeMap::new();
3653 let Some(manager) = store.get_collection(QUEUE_META_COLLECTION) else {
3654 return counts;
3655 };
3656 for entity in manager.query_all(|entity| {
3657 entity
3658 .data
3659 .as_row()
3660 .is_some_and(|row| row_text(row, "kind").as_deref() == Some("queue_pending"))
3661 }) {
3662 if let Some(row) = entity.data.as_row() {
3663 let queue = row_text(row, "queue");
3664 let group = row_text(row, "group");
3665 if let (Some(q), Some(g)) = (queue, group) {
3666 *counts.entry((q, g)).or_insert(0) += 1;
3667 }
3668 }
3669 }
3670 counts
3671}
3672
3673pub(super) fn row_text(row: &RowData, field: &str) -> Option<String> {
3674 match row.get_field(field)?.clone() {
3675 Value::Text(value) => Some(value.to_string()),
3676 Value::NodeRef(value) => Some(value),
3677 Value::EdgeRef(value) => Some(value),
3678 Value::TableRef(value) => Some(value),
3679 _ => None,
3680 }
3681}
3682
3683pub(super) fn row_u64(row: &RowData, field: &str) -> Option<u64> {
3684 match row.get_field(field)?.clone() {
3685 Value::UnsignedInteger(value) => Some(value),
3686 Value::Integer(value) if value >= 0 => Some(value as u64),
3687 Value::Float(value) if value >= 0.0 => Some(value as u64),
3688 Value::Text(value) => value.parse().ok(),
3689 _ => None,
3690 }
3691}
3692
3693fn row_bool(row: &RowData, field: &str) -> Option<bool> {
3694 match row.get_field(field)?.clone() {
3695 Value::Boolean(value) => Some(value),
3696 Value::Text(value) => match value.to_ascii_lowercase().as_str() {
3697 "true" => Some(true),
3698 "false" => Some(false),
3699 _ => None,
3700 },
3701 _ => None,
3702 }
3703}
3704
3705fn queue_collection_contract(
3706 name: &str,
3707 priority: bool,
3708 ttl_ms: Option<u64>,
3709) -> crate::physical::CollectionContract {
3710 let now = current_unix_ms();
3711 let mut context_index_fields = Vec::new();
3712 if priority {
3713 context_index_fields.push("priority".to_string());
3714 }
3715
3716 crate::physical::CollectionContract {
3717 graph_contract: None,
3718 name: name.to_string(),
3719 description: None,
3720 description_generated: false,
3721 declared_model: crate::catalog::CollectionModel::Queue,
3722 schema_mode: crate::catalog::SchemaMode::Dynamic,
3723 origin: crate::physical::ContractOrigin::Explicit,
3724 version: 1,
3725 created_at_unix_ms: now,
3726 updated_at_unix_ms: now,
3727 default_ttl_ms: ttl_ms,
3728 vector_dimension: None,
3729 vector_metric: None,
3730 context_index_fields,
3731 declared_columns: Vec::new(),
3732 table_def: None,
3733 timestamps_enabled: false,
3734 context_index_enabled: false,
3735 metrics_raw_retention_ms: None,
3736 metrics_rollup_policies: Vec::new(),
3737 metrics_tenant_identity: None,
3738 metrics_namespace: None,
3739 append_only: true,
3743 subscriptions: Vec::new(),
3744 analytics_config: Vec::new(),
3745 session_key: None,
3746 session_gap_ms: None,
3747 retention_duration_ms: None,
3748 analytical_storage: None,
3749
3750 ai_policy: None,
3751 }
3752}
3753
3754fn current_unix_ms() -> u128 {
3755 std::time::SystemTime::now()
3756 .duration_since(std::time::UNIX_EPOCH)
3757 .unwrap_or_default()
3758 .as_millis()
3759}
3760
3761pub(super) fn now_ns() -> u64 {
3762 std::time::SystemTime::now()
3763 .duration_since(std::time::UNIX_EPOCH)
3764 .unwrap_or_default()
3765 .as_nanos() as u64
3766}
3767
3768pub(super) fn queue_message_ttl_metadata(ttl_ms: u64) -> Metadata {
3769 queue_message_metadata(Some(ttl_ms), None, None)
3770}
3771
3772pub(super) fn queue_message_metadata(
3778 ttl_ms: Option<u64>,
3779 available_at_ns: Option<u64>,
3780 ordering_key: Option<&str>,
3781) -> Metadata {
3782 let mut fields = HashMap::new();
3783 if let Some(ttl_ms) = ttl_ms {
3784 fields.insert(
3785 "_ttl_ms".to_string(),
3786 if ttl_ms <= i64::MAX as u64 {
3787 MetadataValue::Int(ttl_ms as i64)
3788 } else {
3789 MetadataValue::Timestamp(ttl_ms)
3790 },
3791 );
3792 }
3793 if let Some(at_ns) = available_at_ns {
3794 fields.insert(
3795 "_available_at_ns".to_string(),
3796 MetadataValue::Timestamp(at_ns),
3797 );
3798 }
3799 if let Some(key) = ordering_key {
3800 fields.insert(
3801 "_ordering_key".to_string(),
3802 MetadataValue::String(key.to_string()),
3803 );
3804 }
3805 Metadata::with_fields(fields)
3806}
3807
3808pub(super) fn earliest_future_available_at(store: &UnifiedStore, queue: &str) -> Option<u64> {
3816 let now_ns = now_ns();
3817 let views = load_queue_message_views_with_runtime(None, store, queue).ok()?;
3818 views
3819 .iter()
3820 .filter_map(|v| v.available_at_ns)
3821 .filter(|at| *at > now_ns)
3822 .min()
3823}
3824
3825pub(super) fn set_message_available_at_ns(
3836 store: &UnifiedStore,
3837 queue: &str,
3838 message_id: EntityId,
3839 available_at_ns: Option<u64>,
3840 fallback_ttl_ms: Option<u64>,
3841) -> RedDBResult<()> {
3842 if let Some(runtime) = super::vcs_queue::runtime(store) {
3843 if runtime.vcs_is_versioned(queue)? {
3844 let previous = super::vcs_queue::metadata_rows(store, |row| {
3845 row_text(row, "kind").as_deref() == Some("queue_retry")
3846 && row_text(row, "queue").as_deref() == Some(queue)
3847 && row_u64(row, "message_id") == Some(message_id.raw())
3848 })?;
3849 for (id, _) in previous {
3850 super::vcs_queue::delete_metadata(store, id)?;
3851 }
3852 insert_meta_row(
3853 store,
3854 HashMap::from([
3855 ("kind".into(), Value::text("queue_retry")),
3856 ("queue".into(), Value::text(queue)),
3857 (
3858 "message_id".into(),
3859 Value::UnsignedInteger(message_id.raw()),
3860 ),
3861 (
3862 "available_at_ns".into(),
3863 Value::UnsignedInteger(available_at_ns.unwrap_or(0)),
3864 ),
3865 ]),
3866 )?;
3867 return Ok(());
3868 }
3869 }
3870 let existing_ttl_ms = store
3871 .get_metadata(queue, message_id)
3872 .and_then(|md| match md.get("_ttl_ms")? {
3873 MetadataValue::Int(i) if *i >= 0 => Some(*i as u64),
3874 MetadataValue::Timestamp(t) => Some(*t),
3875 _ => None,
3876 })
3877 .or(fallback_ttl_ms);
3878 let existing_ordering_key = read_message_ordering_key(store, queue, message_id);
3879 let metadata = queue_message_metadata(
3880 existing_ttl_ms,
3881 available_at_ns,
3882 existing_ordering_key.as_deref(),
3883 );
3884 match store.set_metadata(queue, message_id, metadata) {
3885 Ok(()) => Ok(()),
3886 Err(crate::storage::StoreError::CollectionNotFound(_)) => Ok(()),
3887 Err(err) => Err(RedDBError::Internal(err.to_string())),
3888 }
3889}
3890
3891pub(super) fn read_message_ordering_key(
3894 store: &UnifiedStore,
3895 queue: &str,
3896 message_id: EntityId,
3897) -> Option<String> {
3898 let md = store.get_metadata(queue, message_id)?;
3899 match md.get("_ordering_key")? {
3900 MetadataValue::String(value) => Some(value.clone()),
3901 _ => None,
3902 }
3903}
3904
3905pub(super) fn read_message_available_at_ns(
3909 store: &UnifiedStore,
3910 queue: &str,
3911 message_id: EntityId,
3912) -> Option<u64> {
3913 if super::execution_context::capture_current_snapshot()
3914 .is_some_and(|snapshot| snapshot.branch.is_some())
3915 {
3916 match super::vcs_queue::metadata_rows(store, |row| {
3917 row_text(row, "kind").as_deref() == Some("queue_retry")
3918 && row_text(row, "queue").as_deref() == Some(queue)
3919 && row_u64(row, "message_id") == Some(message_id.raw())
3920 }) {
3921 Ok(rows) => {
3922 if let Some((_, row)) = rows.last() {
3923 return row_u64(row, "available_at_ns");
3924 }
3925 }
3926 Err(error) => {
3927 super::vcs_queue::record_error(error);
3928 return None;
3929 }
3930 }
3931 }
3932 let md = store.get_metadata(queue, message_id)?;
3933 match md.get("_available_at_ns")? {
3934 MetadataValue::Timestamp(t) => Some(*t),
3935 MetadataValue::Int(i) if *i >= 0 => Some(*i as u64),
3936 _ => None,
3937 }
3938}
3939
3940fn estimate_payload_bytes(payload: &Value) -> u64 {
3942 match payload {
3943 Value::Json(v) => v.len() as u64,
3944 Value::Text(s) => s.len() as u64,
3945 _ => 64,
3946 }
3947}
3948
3949#[cfg(test)]
3950mod presence_integration_tests {
3951 use super::*;
3952 use crate::storage::queue::presence::{PresenceState, DEFAULT_PRESENCE_TTL_MS};
3953 use crate::{RedDBOptions, RedDBRuntime};
3954
3955 #[test]
3959 fn queue_read_emits_consumer_presence_heartbeat() {
3960 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3961 rt.execute_query("CREATE QUEUE tasks").unwrap();
3962 rt.execute_query("QUEUE GROUP CREATE tasks workers")
3963 .unwrap();
3964 rt.execute_query("QUEUE PUSH tasks {'job':'a'}").unwrap();
3965 rt.execute_query("QUEUE READ tasks GROUP workers CONSUMER w1")
3966 .unwrap();
3967
3968 let snap = rt.queue_consumer_presence_snapshot(DEFAULT_PRESENCE_TTL_MS);
3969 assert_eq!(snap.len(), 1, "exactly one heartbeat recorded");
3970 let row = &snap[0];
3971 assert_eq!(row.queue, "tasks");
3972 assert_eq!(row.group, "workers");
3973 assert_eq!(row.consumer, "w1");
3974 assert_eq!(row.state, PresenceState::Active);
3975
3976 let counts = rt.queue_active_consumer_counts(DEFAULT_PRESENCE_TTL_MS);
3977 assert_eq!(
3978 counts[&("tasks".to_string(), "workers".to_string())],
3979 1,
3980 "active count reflects the live consumer"
3981 );
3982 }
3983
3984 #[test]
3988 fn empty_queue_read_still_heartbeats() {
3989 let rt = RedDBRuntime::with_options(RedDBOptions::in_memory()).unwrap();
3990 rt.execute_query("CREATE QUEUE empty_q").unwrap();
3991 rt.execute_query("QUEUE GROUP CREATE empty_q workers")
3992 .unwrap();
3993 rt.execute_query("QUEUE READ empty_q GROUP workers CONSUMER w1")
3995 .unwrap();
3996
3997 let snap = rt.queue_consumer_presence_snapshot(DEFAULT_PRESENCE_TTL_MS);
3998 assert_eq!(
3999 snap.len(),
4000 1,
4001 "empty read still registers consumer presence"
4002 );
4003 assert_eq!(snap[0].state, PresenceState::Active);
4004 assert_eq!(
4005 snap[0].lease_count, 0,
4006 "no messages delivered → zero leases"
4007 );
4008 }
4009}