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