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