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