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