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