Skip to main content

rivetkit_core/actor/
queue.rs

1use std::collections::BTreeSet;
2use std::fmt;
3use std::future::pending;
4use std::pin::Pin;
5use std::sync::Arc;
6use std::sync::atomic::Ordering;
7use std::time::Duration;
8
9use crate::time::{Instant, SystemTime, UNIX_EPOCH, sleep};
10
11use anyhow::{Context, Result};
12use rivet_error::RivetError;
13use rivetkit_actor_persist::{generated::v4 as persist_v4, versioned as persist_versioned};
14use serde::{Deserialize, Serialize};
15#[cfg(not(target_arch = "wasm32"))]
16use tokio::runtime::{Builder, Handle};
17use tokio::sync::{futures::Notified, oneshot};
18use tokio_util::sync::CancellationToken;
19
20use crate::actor::config::ActorConfig;
21use crate::actor::context::ActorContext;
22use crate::actor::internal_storage;
23use crate::actor::persist::{
24	decode_latest_with_embedded_version, encode_latest_with_embedded_version,
25};
26use crate::actor::task_types::UserTaskKind;
27#[cfg(target_arch = "wasm32")]
28use crate::error::ActorRuntime;
29use crate::telemetry::{self, ActorInvocationTelemetry, IncomingTraceContext};
30
31#[derive(Clone, Debug, Default)]
32pub struct QueueNextOpts {
33	pub names: Option<Vec<String>>,
34	pub timeout: Option<Duration>,
35	pub signal: Option<CancellationToken>,
36	pub completable: bool,
37}
38
39#[derive(Clone, Debug, Default)]
40pub struct QueueWaitOpts {
41	pub timeout: Option<Duration>,
42	pub signal: Option<CancellationToken>,
43	pub completable: bool,
44}
45
46#[derive(Clone, Debug, Default)]
47pub struct EnqueueAndWaitOpts {
48	pub timeout: Option<Duration>,
49	pub signal: Option<CancellationToken>,
50}
51
52#[derive(Clone, Debug)]
53pub struct QueueNextBatchOpts {
54	pub names: Option<Vec<String>>,
55	pub count: u32,
56	pub timeout: Option<Duration>,
57	pub signal: Option<CancellationToken>,
58	pub completable: bool,
59}
60
61impl Default for QueueNextBatchOpts {
62	fn default() -> Self {
63		Self {
64			names: None,
65			count: 1,
66			timeout: None,
67			signal: None,
68			completable: false,
69		}
70	}
71}
72
73#[derive(Clone, Debug, Default)]
74pub struct QueueTryNextOpts {
75	pub names: Option<Vec<String>>,
76	pub completable: bool,
77}
78
79#[derive(Clone, Debug)]
80pub struct QueueTryNextBatchOpts {
81	pub names: Option<Vec<String>>,
82	pub count: u32,
83	pub completable: bool,
84}
85
86impl Default for QueueTryNextBatchOpts {
87	fn default() -> Self {
88		Self {
89			names: None,
90			count: 1,
91			completable: false,
92		}
93	}
94}
95
96pub(super) type QueueWaitActivityCallback = Arc<dyn Fn() + Send + Sync>;
97pub(super) type QueueInspectorUpdateCallback = Arc<dyn Fn(u32) + Send + Sync>;
98
99#[derive(Clone, Debug)]
100pub struct QueueMessage {
101	pub id: u64,
102	pub name: String,
103	pub body: Vec<u8>,
104	pub created_at: i64,
105	/// Ray ID and span of the invocation that sent the message, which the span
106	/// covering its receipt links back to. Empty when the sender carried no
107	/// trace context.
108	pub trace_context: IncomingTraceContext,
109	completion: Option<CompletionHandle>,
110}
111
112#[derive(Clone, Debug)]
113pub struct CompletableQueueMessage {
114	pub id: u64,
115	pub name: String,
116	pub body: Vec<u8>,
117	pub created_at: i64,
118	pub trace_context: IncomingTraceContext,
119	completion: CompletionHandle,
120}
121
122#[derive(Clone)]
123struct CompletionHandle(Arc<CompletionHandleInner>);
124
125struct CompletionHandleInner {
126	ctx: ActorContext,
127	message_id: u64,
128	completed: std::sync::atomic::AtomicBool,
129}
130
131pub(crate) type QueueMetadata = persist_v4::QueueMetadata;
132pub(crate) type PersistedQueueMessage = persist_v4::QueueMessage;
133
134#[cfg(test)]
135pub(crate) fn encode_queue_metadata(metadata: &QueueMetadata) -> Result<Vec<u8>> {
136	encode_latest_with_embedded_version::<persist_versioned::QueueMetadata>(
137		metadata.clone(),
138		rivetkit_actor_persist::CURRENT_VERSION,
139		"queue metadata",
140	)
141}
142
143pub(crate) fn decode_queue_metadata(payload: &[u8]) -> Result<QueueMetadata> {
144	let metadata = decode_latest_with_embedded_version::<persist_versioned::QueueMetadata>(
145		payload,
146		"queue metadata",
147	)?;
148	Ok(metadata)
149}
150
151pub(crate) fn encode_queue_message(message: &PersistedQueueMessage) -> Result<Vec<u8>> {
152	encode_latest_with_embedded_version::<persist_versioned::QueueMessage>(
153		message.clone(),
154		rivetkit_actor_persist::CURRENT_VERSION,
155		"queue message",
156	)
157}
158
159pub(crate) fn decode_queue_message(payload: &[u8]) -> Result<PersistedQueueMessage> {
160	let message = decode_latest_with_embedded_version::<persist_versioned::QueueMessage>(
161		payload,
162		"queue message",
163	)?;
164	Ok(message)
165}
166
167#[derive(RivetError, Serialize, Deserialize)]
168#[error(
169	"queue",
170	"full",
171	"Queue is full",
172	"Queue is full. Limit is {limit} messages."
173)]
174struct QueueFull {
175	limit: u32,
176}
177
178#[derive(RivetError, Serialize, Deserialize)]
179#[error(
180	"queue",
181	"message_too_large",
182	"Queue message is too large",
183	"Queue message too large ({size} bytes). Limit is {limit} bytes."
184)]
185struct QueueMessageTooLarge {
186	size: usize,
187	limit: u32,
188}
189
190#[derive(RivetError)]
191#[error("queue", "already_completed", "Queue message was already completed")]
192struct QueueAlreadyCompleted;
193
194#[derive(RivetError, Serialize, Deserialize)]
195#[error(
196	"queue",
197	"complete_not_configured",
198	"Queue message does not support completion",
199	"Queue '{name}' does not support completion responses."
200)]
201struct QueueCompleteNotConfigured {
202	name: String,
203}
204#[derive(RivetError, Serialize, Deserialize)]
205#[error(
206	"queue",
207	"message_identity_mismatch",
208	"Queue message identity does not match",
209	"Queue message {message_id} is named '{actual_name}', not '{expected_name}'."
210)]
211struct QueueMessageIdentityMismatch {
212	message_id: u64,
213	expected_name: String,
214	actual_name: String,
215}
216
217#[derive(RivetError)]
218#[error("actor", "aborted", "Actor aborted")]
219struct QueueActorAborted;
220
221#[derive(RivetError, Serialize, Deserialize)]
222#[error(
223	"queue",
224	"timed_out",
225	"Queue wait timed out",
226	"Queue wait timed out after {timeout_ms} ms."
227)]
228struct QueueWaitTimedOut {
229	timeout_ms: u64,
230}
231
232#[derive(RivetError, Serialize, Deserialize)]
233#[error(
234	"queue",
235	"completion_waiter_conflict",
236	"Queue completion waiter conflict",
237	"Queue completion waiter is already registered for message {message_id}."
238)]
239struct QueueCompletionWaiterConflict {
240	message_id: u64,
241}
242
243#[derive(RivetError)]
244#[error(
245	"queue",
246	"completion_waiter_dropped",
247	"Queue completion waiter dropped before response"
248)]
249struct QueueCompletionWaiterDropped;
250
251impl ActorContext {
252	pub async fn send(&self, name: &str, body: &[u8]) -> Result<QueueMessage> {
253		self.enqueue_message(name, body, None).await
254	}
255
256	pub async fn enqueue_and_wait(
257		&self,
258		name: &str,
259		body: &[u8],
260		opts: EnqueueAndWaitOpts,
261	) -> Result<Option<Vec<u8>>> {
262		let (sender, receiver) = oneshot::channel();
263		let message = self.enqueue_message(name, body, Some(sender)).await?;
264		let result = self
265			.wait_for_completion_response(message.id, receiver, opts.timeout, opts.signal.as_ref())
266			.await;
267		self.remove_completion_waiter(message.id).await;
268		result
269	}
270
271	async fn enqueue_message(
272		&self,
273		name: &str,
274		body: &[u8],
275		completion_waiter: Option<oneshot::Sender<Option<Vec<u8>>>>,
276	) -> Result<QueueMessage> {
277		self.ensure_initialized().await?;
278
279		let created_at = current_timestamp_ms()?;
280		let persisted = PersistedQueueMessage {
281			name: name.to_owned(),
282			body: body.to_vec(),
283			created_at,
284			failure_count: None,
285			available_at: None,
286			in_flight: None,
287			in_flight_at: None,
288		};
289		let encoded_message = encode_queue_message(&persisted).context("encode queue message")?;
290		let trace_context = self
291			.invocation_telemetry()
292			.map(ActorInvocationTelemetry::incoming_trace_context)
293			.unwrap_or_default();
294		let config = self.config();
295		if encoded_message.len() > config.max_queue_message_size as usize {
296			return Err(QueueMessageTooLarge {
297				size: encoded_message.len(),
298				limit: config.max_queue_message_size,
299			}
300			.build());
301		}
302
303		let mut metadata = self.0.queue_metadata.lock().await;
304		if metadata.size >= config.max_queue_size {
305			return Err(QueueFull {
306				limit: config.max_queue_size,
307			}
308			.build());
309		}
310
311		let id = if metadata.next_id == 0 {
312			1
313		} else {
314			metadata.next_id
315		};
316		metadata.next_id = id.saturating_add(1);
317		metadata.size = metadata.size.saturating_add(1);
318		let registered_completion_waiter = if let Some(waiter) = completion_waiter {
319			if self
320				.0
321				.queue_completion_waiters
322				.insert_async(id, waiter)
323				.await
324				.is_err()
325			{
326				metadata.next_id = id;
327				metadata.size = metadata.size.saturating_sub(1);
328				return Err(QueueCompletionWaiterConflict { message_id: id }.build());
329			}
330			true
331		} else {
332			false
333		};
334
335		let persist_result = internal_storage::persist_queue_message(
336			self.sql(),
337			id,
338			metadata.next_id,
339			&persisted,
340			trace_context.clone(),
341		)
342		.await;
343
344		if let Err(error) = persist_result {
345			metadata.next_id = id;
346			metadata.size = metadata.size.saturating_sub(1);
347			if registered_completion_waiter {
348				self.remove_completion_waiter(id).await;
349			}
350			return Err(error).context("persist queue message");
351		}
352
353		let queue_size = metadata.size;
354		drop(metadata);
355		self.0.metrics.add_queue_messages_sent(1);
356		self.0
357			.metrics
358			.set_queue_depth(self.0.queue_metadata.lock().await.size);
359		self.notify_inspector_update(queue_size);
360		self.0.queue_notify.notify_waiters();
361
362		Ok(QueueMessage {
363			id,
364			name: name.to_owned(),
365			body: body.to_vec(),
366			created_at,
367			trace_context,
368			completion: None,
369		})
370	}
371
372	pub async fn next(&self, opts: QueueNextOpts) -> Result<Option<QueueMessage>> {
373		let mut messages = self
374			.next_batch(QueueNextBatchOpts {
375				names: opts.names,
376				count: 1,
377				timeout: opts.timeout,
378				signal: opts.signal,
379				completable: opts.completable,
380			})
381			.await?;
382		Ok(messages.pop())
383	}
384
385	pub async fn next_batch(&self, opts: QueueNextBatchOpts) -> Result<Vec<QueueMessage>> {
386		self.ensure_initialized().await?;
387
388		let count = opts.count.max(1);
389		let deadline = opts.timeout.map(|timeout| Instant::now() + timeout);
390		let names = normalize_names(opts.names);
391
392		loop {
393			let notified = self.0.queue_notify.notified();
394			tokio::pin!(notified);
395			notified.as_mut().enable();
396			let messages = self
397				.try_receive_batch(names.as_ref(), count, opts.completable)
398				.await?;
399			if !messages.is_empty() {
400				return Ok(messages);
401			}
402
403			let remaining_timeout =
404				deadline.map(|deadline| deadline.saturating_duration_since(Instant::now()));
405			if matches!(remaining_timeout, Some(timeout) if timeout.is_zero()) {
406				return Ok(Vec::new());
407			}
408
409			let wait_guard = ActiveQueueWaitGuard::new(self);
410			let result = self
411				.wait_for_message(notified.as_mut(), remaining_timeout, opts.signal.as_ref())
412				.await;
413			drop(wait_guard);
414
415			match result {
416				WaitOutcome::Notified => continue,
417				WaitOutcome::TimedOut => return Ok(Vec::new()),
418				WaitOutcome::Aborted => return Err(QueueActorAborted.build()),
419			}
420		}
421	}
422
423	pub async fn wait_for_names(
424		&self,
425		names: Vec<String>,
426		opts: QueueWaitOpts,
427	) -> Result<QueueMessage> {
428		self.ensure_initialized().await?;
429
430		let deadline = opts.timeout.map(|timeout| Instant::now() + timeout);
431		let names = normalize_names(Some(names));
432
433		loop {
434			let notified = self.0.queue_notify.notified();
435			tokio::pin!(notified);
436			notified.as_mut().enable();
437			if let Some(message) = self
438				.try_receive_batch(names.as_ref(), 1, opts.completable)
439				.await?
440				.into_iter()
441				.next()
442			{
443				return Ok(message);
444			}
445
446			let remaining_timeout =
447				deadline.map(|deadline| deadline.saturating_duration_since(Instant::now()));
448			if let Some(timeout) = remaining_timeout
449				&& timeout.is_zero()
450			{
451				return Err(QueueWaitTimedOut {
452					timeout_ms: opts.timeout.map(duration_ms).unwrap_or(0),
453				}
454				.build());
455			}
456
457			let wait_guard = ActiveQueueWaitGuard::new(self);
458			let result = self
459				.wait_for_message(notified.as_mut(), remaining_timeout, opts.signal.as_ref())
460				.await;
461			drop(wait_guard);
462
463			match result {
464				WaitOutcome::Notified => continue,
465				WaitOutcome::TimedOut => {
466					return Err(QueueWaitTimedOut {
467						timeout_ms: opts.timeout.map(duration_ms).unwrap_or(0),
468					}
469					.build());
470				}
471				WaitOutcome::Aborted => return Err(QueueActorAborted.build()),
472			}
473		}
474	}
475
476	pub async fn wait_for_names_available(
477		&self,
478		names: Vec<String>,
479		opts: QueueWaitOpts,
480	) -> Result<()> {
481		self.ensure_initialized().await?;
482
483		let deadline = opts.timeout.map(|timeout| Instant::now() + timeout);
484		let names = normalize_names(Some(names));
485
486		loop {
487			let notified = self.0.queue_notify.notified();
488			tokio::pin!(notified);
489			notified.as_mut().enable();
490			if internal_storage::has_queue_messages(self.sql(), names.as_ref())
491				.await
492				.context("check for matching sqlite queue messages")?
493			{
494				return Ok(());
495			}
496
497			let remaining_timeout =
498				deadline.map(|deadline| deadline.saturating_duration_since(Instant::now()));
499			if let Some(timeout) = remaining_timeout
500				&& timeout.is_zero()
501			{
502				return Err(QueueWaitTimedOut {
503					timeout_ms: opts.timeout.map(duration_ms).unwrap_or(0),
504				}
505				.build());
506			}
507
508			let wait_guard = ActiveQueueWaitGuard::new(self);
509			let result = self
510				.wait_for_message(notified.as_mut(), remaining_timeout, opts.signal.as_ref())
511				.await;
512			drop(wait_guard);
513
514			match result {
515				WaitOutcome::Notified => continue,
516				WaitOutcome::TimedOut => {
517					return Err(QueueWaitTimedOut {
518						timeout_ms: opts.timeout.map(duration_ms).unwrap_or(0),
519					}
520					.build());
521				}
522				WaitOutcome::Aborted => return Err(QueueActorAborted.build()),
523			}
524		}
525	}
526	/// Completes a persisted message by durable identity. Missing or already
527	/// completed IDs are idempotent; an expected-name mismatch leaves the row
528	/// untouched.
529	pub async fn complete_persisted_message(
530		&self,
531		message_id: u64,
532		expected_name: &str,
533		response: Option<Vec<u8>>,
534	) -> Result<bool> {
535		self.ensure_initialized().await?;
536		let _receive_guard = self.0.queue_receive_lock.lock().await;
537		let Some(_) = self
538			.verify_persisted_message_identity_unlocked(message_id, expected_name)
539			.await?
540		else {
541			return Ok(false);
542		};
543		self.complete_message_by_id_unlocked(message_id, response)
544			.await?;
545		Ok(true)
546	}
547
548	async fn verify_persisted_message_identity_unlocked(
549		&self,
550		message_id: u64,
551		expected_name: &str,
552	) -> Result<Option<String>> {
553		let Some(actual_name) =
554			internal_storage::load_queue_message_name(self.sql(), message_id).await?
555		else {
556			return Ok(None);
557		};
558		if actual_name != expected_name {
559			return Err(QueueMessageIdentityMismatch {
560				message_id,
561				expected_name: expected_name.to_owned(),
562				actual_name: actual_name.clone(),
563			}
564			.build());
565		}
566		Ok(Some(actual_name))
567	}
568
569	pub fn try_next(&self, opts: QueueTryNextOpts) -> Result<Option<QueueMessage>> {
570		let mut messages = self.try_next_batch(QueueTryNextBatchOpts {
571			names: opts.names,
572			count: 1,
573			completable: opts.completable,
574		})?;
575		Ok(messages.pop())
576	}
577
578	pub fn try_next_batch(&self, opts: QueueTryNextBatchOpts) -> Result<Vec<QueueMessage>> {
579		self.block_on(async {
580			self.ensure_initialized().await?;
581			self.try_receive_batch(
582				normalize_names(opts.names).as_ref(),
583				opts.count.max(1),
584				opts.completable,
585			)
586			.await
587		})
588	}
589
590	pub async fn inspect_messages(&self) -> Result<Vec<QueueMessage>> {
591		self.ensure_initialized().await?;
592		self.list_messages().await
593	}
594
595	pub fn max_size(&self) -> u32 {
596		self.config().max_queue_size
597	}
598
599	/// Removes all messages from the queue and resets the size counter.
600	pub async fn reset(&self) -> Result<()> {
601		self.ensure_initialized().await?;
602
603		// Serialize against receivers before touching metadata. Lock order matches
604		// try_receive_batch (receive lock then metadata) so there is no deadlock.
605		let _receive_guard = self.0.queue_receive_lock.lock().await;
606
607		let mut metadata = self.0.queue_metadata.lock().await;
608
609		internal_storage::reset_queue(self.sql())
610			.await
611			.context("delete all sqlite queue messages")?;
612
613		metadata.size = 0;
614
615		self.0.queue_completion_waiters.clear_async().await;
616
617		drop(metadata);
618
619		self.0.metrics.set_queue_depth(0);
620		self.notify_inspector_update(0);
621		self.0.queue_notify.notify_waiters();
622
623		Ok(())
624	}
625
626	pub(crate) fn configure_queue(&self, config: ActorConfig) {
627		*self.0.queue_config.lock() = config;
628	}
629
630	pub(crate) fn set_wait_activity_callback(&self, callback: Option<Arc<dyn Fn() + Send + Sync>>) {
631		*self.0.queue_wait_activity_callback.lock() = callback;
632	}
633
634	pub(crate) fn set_inspector_update_callback(
635		&self,
636		callback: Option<Arc<dyn Fn(u32) + Send + Sync>>,
637	) {
638		*self.0.queue_inspector_update_callback.lock() = callback;
639	}
640
641	async fn ensure_initialized(&self) -> Result<()> {
642		self.0
643			.queue_initialize
644			.get_or_try_init(|| async {
645				let metadata = internal_storage::load_queue_metadata(self.sql())
646					.await
647					.context("load queue metadata from sqlite")?;
648				let mut state = self.0.queue_metadata.lock().await;
649				*state = metadata;
650				self.0.metrics.set_queue_depth(state.size);
651				Ok(())
652			})
653			.await
654			.map(|_| ())
655	}
656
657	async fn try_receive_batch(
658		&self,
659		names: Option<&BTreeSet<String>>,
660		count: u32,
661		completable: bool,
662	) -> Result<Vec<QueueMessage>> {
663		let _receive_guard = self.0.queue_receive_lock.lock().await;
664
665		let selected = self.list_messages_matching(names, count).await?;
666
667		if selected.is_empty() {
668			return Ok(Vec::new());
669		}
670
671		let _receive_spans: Vec<tracing::Span> = selected
672			.iter()
673			.map(|message| telemetry::start_queue_receive(self, message))
674			.collect();
675
676		if completable {
677			let queue_size = self.0.queue_metadata.lock().await.size;
678			self.0
679				.metrics
680				.add_queue_messages_received(selected.len().try_into().unwrap_or(u64::MAX));
681			self.notify_inspector_update(queue_size);
682			return Ok(selected
683				.into_iter()
684				.map(|message| self.attach_completion(message))
685				.collect());
686		}
687
688		self.remove_messages(selected.iter().map(|message| message.id).collect())
689			.await?;
690		self.0
691			.metrics
692			.add_queue_messages_received(selected.len().try_into().unwrap_or(u64::MAX));
693
694		Ok(selected)
695	}
696
697	async fn list_messages(&self) -> Result<Vec<QueueMessage>> {
698		let messages: Vec<QueueMessage> = internal_storage::load_queue_messages(self.sql())
699			.await
700			.context("list sqlite queue messages")?
701			.into_iter()
702			.map(queue_message_from_row)
703			.collect();
704
705		let actual_size = messages.len().try_into().unwrap_or(u32::MAX);
706		let mut metadata = self.0.queue_metadata.lock().await;
707		if metadata.size != actual_size {
708			metadata.size = actual_size;
709		}
710		if metadata.next_id == 0 {
711			metadata.next_id = messages
712				.last()
713				.map(|message| message.id.saturating_add(1))
714				.unwrap_or(1);
715		}
716
717		Ok(messages)
718	}
719
720	async fn list_messages_matching(
721		&self,
722		names: Option<&BTreeSet<String>>,
723		limit: u32,
724	) -> Result<Vec<QueueMessage>> {
725		internal_storage::load_queue_messages_matching(self.sql(), names, limit)
726			.await
727			.context("list matching sqlite queue messages")
728			.map(|rows| rows.into_iter().map(queue_message_from_row).collect())
729	}
730
731	fn attach_completion(&self, mut message: QueueMessage) -> QueueMessage {
732		message.completion = Some(CompletionHandle::new(self.clone(), message.id));
733		message
734	}
735
736	async fn remove_messages(&self, message_ids: Vec<u64>) -> Result<()> {
737		if message_ids.is_empty() {
738			return Ok(());
739		}
740
741		let deleted_count = internal_storage::delete_queue_messages(self.sql(), &message_ids)
742			.await
743			.context("delete sqlite queue messages")?;
744
745		let queue_size = {
746			let mut metadata = self.0.queue_metadata.lock().await;
747			metadata.size = metadata.size.saturating_sub(deleted_count);
748			metadata.size
749		};
750		self.0
751			.metrics
752			.set_queue_depth(self.0.queue_metadata.lock().await.size);
753		self.notify_inspector_update(queue_size);
754		Ok(())
755	}
756
757	async fn complete_message_by_id(
758		&self,
759		message_id: u64,
760		response: Option<Vec<u8>>,
761	) -> Result<()> {
762		let _receive_guard = self.0.queue_receive_lock.lock().await;
763		self.complete_message_by_id_unlocked(message_id, response)
764			.await
765	}
766
767	async fn complete_message_by_id_unlocked(
768		&self,
769		message_id: u64,
770		response: Option<Vec<u8>>,
771	) -> Result<()> {
772		self.remove_messages(vec![message_id]).await?;
773		if let Some(waiter) = self.remove_completion_waiter(message_id).await {
774			let _ = waiter.send(response);
775		}
776		Ok(())
777	}
778
779	async fn remove_completion_waiter(
780		&self,
781		message_id: u64,
782	) -> Option<oneshot::Sender<Option<Vec<u8>>>> {
783		self.0
784			.queue_completion_waiters
785			.remove_async(&message_id)
786			.await
787			.map(|(_, waiter)| waiter)
788	}
789
790	async fn wait_for_message(
791		&self,
792		mut notified: Pin<&mut Notified<'_>>,
793		timeout: Option<Duration>,
794		signal: Option<&CancellationToken>,
795	) -> WaitOutcome {
796		let actor_abort_signal = self.0.queue_abort_signal.lock().clone();
797		if signal.is_some_and(CancellationToken::is_cancelled) {
798			return WaitOutcome::Aborted;
799		}
800		if actor_abort_signal.is_cancelled() {
801			return WaitOutcome::Aborted;
802		}
803
804		let actor_aborted = async {
805			actor_abort_signal.cancelled().await;
806		};
807		let external_aborted = async {
808			if let Some(signal) = signal {
809				signal.cancelled().await;
810			} else {
811				pending::<()>().await;
812			}
813		};
814
815		match timeout {
816			Some(timeout) => {
817				tokio::select! {
818					_ = &mut notified => WaitOutcome::Notified,
819					_ = actor_aborted => WaitOutcome::Aborted,
820					_ = external_aborted => WaitOutcome::Aborted,
821					_ = sleep(timeout) => WaitOutcome::TimedOut,
822				}
823			}
824			None => {
825				tokio::select! {
826					_ = &mut notified => WaitOutcome::Notified,
827					_ = actor_aborted => WaitOutcome::Aborted,
828					_ = external_aborted => WaitOutcome::Aborted,
829				}
830			}
831		}
832	}
833
834	/// TS parity: queue-manager.ts keeps `enqueueAndWait` completion waits
835	/// alive across actor aborts; the surrounding tracked user task owns
836	/// shutdown cancellation.
837	async fn wait_for_completion_response(
838		&self,
839		message_id: u64,
840		mut receiver: oneshot::Receiver<Option<Vec<u8>>>,
841		timeout: Option<Duration>,
842		signal: Option<&CancellationToken>,
843	) -> Result<Option<Vec<u8>>> {
844		if signal.is_some_and(CancellationToken::is_cancelled) {
845			return Err(QueueActorAborted.build());
846		}
847
848		let external_aborted = async {
849			if let Some(signal) = signal {
850				signal.cancelled().await;
851			} else {
852				pending::<()>().await;
853			}
854		};
855
856		let wait_result = match timeout {
857			Some(timeout) => {
858				tokio::select! {
859					response = &mut receiver => CompletionWaitOutcome::Response(response),
860					_ = external_aborted => CompletionWaitOutcome::Aborted,
861					_ = sleep(timeout) => CompletionWaitOutcome::TimedOut,
862				}
863			}
864			None => {
865				tokio::select! {
866					response = &mut receiver => CompletionWaitOutcome::Response(response),
867					_ = external_aborted => CompletionWaitOutcome::Aborted,
868				}
869			}
870		};
871
872		match wait_result {
873			CompletionWaitOutcome::Response(Ok(response)) => Ok(response),
874			CompletionWaitOutcome::Response(Err(_)) => Err(QueueCompletionWaiterDropped.build())
875				.context(format!("wait for queue completion on message {message_id}")),
876			CompletionWaitOutcome::TimedOut => Err(QueueWaitTimedOut {
877				timeout_ms: timeout.map(duration_ms).unwrap_or(0),
878			}
879			.build()),
880			CompletionWaitOutcome::Aborted => Err(QueueActorAborted.build()),
881		}
882	}
883
884	fn block_on<T>(&self, future: impl std::future::Future<Output = Result<T>>) -> Result<T> {
885		#[cfg(not(target_arch = "wasm32"))]
886		{
887			if let Ok(handle) = Handle::try_current() {
888				tokio::task::block_in_place(|| handle.block_on(future))
889			} else {
890				Builder::new_current_thread()
891					.enable_all()
892					.build()
893					.context("build temporary runtime for queue operation")?
894					.block_on(future)
895			}
896		}
897
898		#[cfg(target_arch = "wasm32")]
899		{
900			drop(future);
901			Err(ActorRuntime::InvalidOperation {
902				operation: "queue.try_next_batch".to_owned(),
903				reason: "synchronous queue receive requires native runtime support".to_owned(),
904			}
905			.build())
906		}
907	}
908
909	fn config(&self) -> ActorConfig {
910		self.0.queue_config.lock().clone()
911	}
912
913	#[cfg(test)]
914	pub(crate) fn queue_config_for_tests(&self) -> ActorConfig {
915		self.config()
916	}
917
918	fn notify_wait_activity(&self) {
919		if let Some(callback) = self.0.queue_wait_activity_callback.lock().clone() {
920			callback();
921		}
922	}
923
924	fn notify_inspector_update(&self, queue_size: u32) {
925		if let Some(callback) = self.0.queue_inspector_update_callback.lock().clone() {
926			callback(queue_size);
927		}
928	}
929}
930
931impl QueueMessage {
932	pub async fn complete(self, response: Option<Vec<u8>>) -> Result<()> {
933		let completable = self.into_completable()?;
934		completable.complete(response).await
935	}
936
937	pub fn into_completable(self) -> Result<CompletableQueueMessage> {
938		let completion = self.completion.clone().ok_or_else(|| {
939			QueueCompleteNotConfigured {
940				name: self.name.clone(),
941			}
942			.build()
943		})?;
944
945		Ok(CompletableQueueMessage {
946			id: self.id,
947			name: self.name,
948			body: self.body,
949			created_at: self.created_at,
950			trace_context: self.trace_context,
951			completion,
952		})
953	}
954
955	pub fn is_completable(&self) -> bool {
956		self.completion.is_some()
957	}
958}
959
960impl CompletableQueueMessage {
961	pub async fn complete(self, response: Option<Vec<u8>>) -> Result<()> {
962		self.completion.complete(response).await
963	}
964
965	pub fn into_message(self) -> QueueMessage {
966		QueueMessage {
967			id: self.id,
968			name: self.name,
969			body: self.body,
970			created_at: self.created_at,
971			trace_context: self.trace_context,
972			completion: Some(self.completion),
973		}
974	}
975}
976
977impl CompletionHandle {
978	fn new(ctx: ActorContext, message_id: u64) -> Self {
979		Self(Arc::new(CompletionHandleInner {
980			ctx,
981			message_id,
982			completed: std::sync::atomic::AtomicBool::new(false),
983		}))
984	}
985
986	async fn complete(&self, response: Option<Vec<u8>>) -> Result<()> {
987		if self.0.completed.swap(true, Ordering::SeqCst) {
988			return Err(QueueAlreadyCompleted.build());
989		}
990
991		if let Err(error) = self
992			.0
993			.ctx
994			.complete_message_by_id(self.0.message_id, response)
995			.await
996		{
997			self.0.completed.store(false, Ordering::SeqCst);
998			return Err(error);
999		}
1000
1001		Ok(())
1002	}
1003}
1004
1005impl fmt::Debug for CompletionHandle {
1006	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1007		f.debug_struct("CompletionHandle")
1008			.field("message_id", &self.0.message_id)
1009			.field("completed", &self.0.completed.load(Ordering::SeqCst))
1010			.finish()
1011	}
1012}
1013
1014struct ActiveQueueWaitGuard<'a> {
1015	ctx: &'a ActorContext,
1016	started_at: Instant,
1017}
1018
1019impl<'a> ActiveQueueWaitGuard<'a> {
1020	fn new(ctx: &'a ActorContext) -> Self {
1021		ctx.0.active_queue_wait_count.fetch_add(1, Ordering::SeqCst);
1022		ctx.0.metrics.begin_user_task(UserTaskKind::QueueWait);
1023		ctx.notify_wait_activity();
1024		Self {
1025			ctx,
1026			started_at: Instant::now(),
1027		}
1028	}
1029}
1030
1031impl Drop for ActiveQueueWaitGuard<'_> {
1032	fn drop(&mut self) {
1033		self.ctx
1034			.0
1035			.metrics
1036			.end_user_task(UserTaskKind::QueueWait, self.started_at.elapsed());
1037		let previous = self
1038			.ctx
1039			.0
1040			.active_queue_wait_count
1041			.fetch_sub(1, Ordering::SeqCst);
1042		if previous == 0 {
1043			self.ctx
1044				.0
1045				.active_queue_wait_count
1046				.store(0, Ordering::SeqCst);
1047		}
1048		self.ctx.notify_wait_activity();
1049	}
1050}
1051
1052enum WaitOutcome {
1053	Notified,
1054	TimedOut,
1055	Aborted,
1056}
1057
1058enum CompletionWaitOutcome {
1059	Response(Result<Option<Vec<u8>>, oneshot::error::RecvError>),
1060	TimedOut,
1061	Aborted,
1062}
1063
1064fn normalize_names(names: Option<Vec<String>>) -> Option<BTreeSet<String>> {
1065	names.and_then(|names| {
1066		let normalized = names.into_iter().collect::<BTreeSet<_>>();
1067		if normalized.is_empty() {
1068			None
1069		} else {
1070			Some(normalized)
1071		}
1072	})
1073}
1074
1075fn queue_message_from_row(row: internal_storage::QueueMessageRow) -> QueueMessage {
1076	QueueMessage {
1077		id: row.id,
1078		name: row.message.name,
1079		body: row.message.body,
1080		created_at: row.message.created_at,
1081		trace_context: row.trace_context,
1082		completion: None,
1083	}
1084}
1085
1086fn current_timestamp_ms() -> Result<i64> {
1087	let now = SystemTime::now()
1088		.duration_since(UNIX_EPOCH)
1089		.context("current time is before unix epoch")?;
1090	i64::try_from(now.as_millis()).context("queue timestamp exceeds i64")
1091}
1092
1093fn duration_ms(duration: Duration) -> u64 {
1094	duration.as_millis().try_into().unwrap_or(u64::MAX)
1095}
1096
1097// Test shim keeps moved tests in crate-root tests/ with private-module access.
1098#[cfg(test)]
1099#[path = "../../tests/queue.rs"]
1100mod tests;