use async_broadcast::{InactiveReceiver, Sender as BroadcastSender}; use futures::channel::mpsc::{self, Receiver as MpscReceiver, Sender as MpscSender}; use futures::channel::oneshot::{self, Receiver, Sender}; use futures::future::{AbortHandle, Abortable}; use futures::StreamExt; use galaxyui::r#async::executor::Background; use instant::Instant; use std::collections::HashMap; use std::future::Future; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::{Arc, Mutex}; use std::time::Duration; use anyhow::Result; use galaxyui::{r#async::Timer, Entity, RetryOption, SingletonEntity}; const DEFAULT_BUFFER_SIZE: usize = 1024; const DEFAULT_SYNC_RETRY_STRATEGY: RetryOption = RetryOption::exponential( Duration::from_millis(500), /* initial interval */ 2.0, /* exponential factor */ 3, /* max retry count */ ) .with_jitter(0.2 /* max_jitter_percentage */); /// An opaque identifier for a task in the sync queue. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] struct TaskId(u64); impl TaskId { /// Constructs a new globally-unique task ID. #[allow(clippy::new_without_default)] fn new() -> TaskId { static NEXT_ID: AtomicU64 = AtomicU64::new(0); let raw = NEXT_ID.fetch_add(1, Ordering::Relaxed); TaskId(raw) } } /// Trait for errors that can be classified as transient. pub trait IsTransientError { fn is_transient(&self) -> bool; } /// Trait for any task that can be enqueued in the sync queue. pub trait SyncQueueTaskTrait: Send + 'static { /// Error type for the task (if it fails). It needs to derive IsTransientError /// to decide the retry logic. type Error: std::error::Error + Send + Sync + IsTransientError + 'static; /// Result type for the task (if it succeeds). type Result: Send + Sync; /// The future should return a result of Self::Result or Self::Error. Note that /// we can only implement Send on non-wasm platforms. #[cfg(not(target_arch = "wasm32"))] type Fut: Future> + Send; #[cfg(target_arch = "wasm32")] type Fut: Future>; /// Implementation for running the task. fn run(&mut self) -> Self::Fut; } /// The operational mode of a [`SyncQueue`], specified at construction time. enum SyncQueueMode { /// Each caller receives results via a per-task oneshot channel /// (returned by [`SyncQueue::enqueue_with_result`]). PerTask, /// Results are broadcast to all subscribers via an /// [`async_broadcast`] channel (obtained from [`SyncQueue::subscribe`]). Streaming { /// Keeps the broadcast channel alive even when no active receivers exist. /// Without this, dropping the initial receiver from `async_broadcast::broadcast()` /// would permanently close the channel. New subscribers are created via /// [`InactiveReceiver::activate_cloned`]. _keepalive: InactiveReceiver>, }, } /// The result type broadcast in streaming mode. pub type BroadcastResult = Result::Result>, Arc<::Error>>; /// Broadcast receiver for streaming mode results. pub type BroadcastReceiver = async_broadcast::Receiver>; /// A queued task, with metadata and retry options. struct QueuedTask { task: T, retry_options: RetryOption, result_sender: Option>>, /// Context of the task used in logging / telemetry. context: String, } /// Configuration for rate limiting in the sync queue. #[derive(Clone)] struct RateLimitConfig { max_requests_per_minute: u32, tokens: Arc>, last_refill: Arc>, } impl RateLimitConfig { fn new(max_requests_per_minute: u32) -> Self { Self { max_requests_per_minute, tokens: Arc::new(Mutex::new(max_requests_per_minute as f64)), last_refill: Arc::new(Mutex::new(Instant::now())), } } async fn wait_for_token(&self) { loop { { let now = Instant::now(); let mut tokens = self.tokens.lock().unwrap(); let mut last_refill = self.last_refill.lock().unwrap(); // Calculate tokens to add based on time elapsed let elapsed = now.duration_since(*last_refill); let tokens_to_add = (elapsed.as_secs_f64() / 60.0) * self.max_requests_per_minute as f64; // Refill tokens (capped at max_rpm) *tokens = (*tokens + tokens_to_add).min(self.max_requests_per_minute as f64); *last_refill = now; // Try to consume a token if *tokens >= 1.0 { *tokens -= 1.0; return; // Token consumed, can proceed } } // No tokens available, wait a bit before checking again // Wait time is calculated to ensure we don't busy-wait let wait_time = Duration::from_millis(60_000 / self.max_requests_per_minute as u64); Timer::after(wait_time).await; } } } /// The global sync queue singleton, generic over the task type. pub struct SyncQueue { sender: Arc>, task_map: Arc>>>, /// Abort handle for the currently executing task. Set by the background /// processor before running a task and cleared after it completes. active_task_handle: Arc>>, mode: SyncQueueMode, } impl Clone for SyncQueueMode { fn clone(&self) -> Self { match self { SyncQueueMode::PerTask => SyncQueueMode::PerTask, SyncQueueMode::Streaming { _keepalive } => SyncQueueMode::Streaming { _keepalive: _keepalive.clone(), }, } } } impl Clone for SyncQueue { fn clone(&self) -> Self { Self { sender: self.sender.clone(), task_map: self.task_map.clone(), active_task_handle: self.active_task_handle.clone(), mode: self.mode.clone(), } } } impl SyncQueue { pub fn new(executor: &Arc) -> Self { Self::new_with_rate_limit(executor, None) } pub fn new_with_rate_limit(executor: &Arc, max_rpm: Option) -> Self { Self::new_inner(executor, max_rpm, SyncQueueMode::PerTask, None) } pub fn new_streaming(executor: &Arc) -> Self { Self::new_streaming_with_rate_limit(executor, None) } pub fn new_streaming_with_rate_limit(executor: &Arc, max_rpm: Option) -> Self { let (broadcast_tx, broadcast_rx) = async_broadcast::broadcast(DEFAULT_BUFFER_SIZE); let keepalive = broadcast_rx.deactivate(); Self::new_inner( executor, max_rpm, SyncQueueMode::Streaming { _keepalive: keepalive, }, Some(broadcast_tx), ) } fn new_inner( executor: &Arc, max_rpm: Option, mode: SyncQueueMode, broadcast_sender: Option>>, ) -> Self { let (sender, receiver) = mpsc::channel(DEFAULT_BUFFER_SIZE); let rate_limit_config = max_rpm.map(RateLimitConfig::new); let task_map: Arc>>> = Arc::new(Mutex::new(HashMap::new())); let active_task_handle: Arc>> = Arc::new(Mutex::new(None)); executor.spawn(Self::process_queue( receiver, rate_limit_config, task_map.clone(), active_task_handle.clone(), broadcast_sender, )); Self { sender: Arc::new(sender), task_map, active_task_handle, mode, } } /// Returns a new broadcast receiver for task results. /// /// # Panics /// Panics if this is a per-task queue. pub fn subscribe(&self) -> BroadcastReceiver { match &self.mode { SyncQueueMode::Streaming { _keepalive } => _keepalive.activate_cloned(), SyncQueueMode::PerTask => panic!("subscribe() called on a per-task queue"), } } /// Enqueues a task without returning a per-task result receiver. /// Results are delivered through the broadcast channel. /// /// # Panics /// Panics if this is a per-task queue. pub fn enqueue(&self, task: T, retry_options: Option, context: impl Into) { assert!( matches!(self.mode, SyncQueueMode::Streaming { .. }), "enqueue() called on a per-task queue" ); let task_id = TaskId::new(); let queued_task = QueuedTask { task, retry_options: retry_options.unwrap_or(DEFAULT_SYNC_RETRY_STRATEGY), context: context.into(), result_sender: None, }; self.task_map.lock().unwrap().insert(task_id, queued_task); if let Err(e) = self.sender.as_ref().clone().try_send(task_id) { log::warn!("Failed to enqueue task because of receiver error {e}"); self.task_map.lock().unwrap().remove(&task_id); } } /// Enqueues a task and returns a oneshot receiver for that task's result. /// /// # Panics /// Panics if this is a streaming queue. pub async fn enqueue_with_result( &self, task: T, retry_options: Option, context: impl Into, ) -> Receiver> { assert!( matches!(self.mode, SyncQueueMode::PerTask), "enqueue_with_result() called on a streaming queue" ); let (tx, rx) = oneshot::channel(); let task_id = TaskId::new(); let queued_task = QueuedTask { task, retry_options: retry_options.unwrap_or(DEFAULT_SYNC_RETRY_STRATEGY), context: context.into(), result_sender: Some(tx), }; self.task_map.lock().unwrap().insert(task_id, queued_task); // Ignore send error if no receiver (e.g., queue processor dropped) if let Err(e) = self.sender.as_ref().clone().try_send(task_id) { log::warn!("Failed to enqueue task because of receiver error {e}"); // Clean up the task from the map since it will never be processed. self.task_map.lock().unwrap().remove(&task_id); } rx } /// Check if there is a queued task (not currently executing) that matches /// the given comparison function. pub fn has_queued_task(&self, comparison: impl Fn(&T) -> bool) -> bool { self.task_map .lock() .unwrap() .values() .any(|queued_task| comparison(&queued_task.task)) } /// Cancel all pending and in-flight tasks. /// /// Pending tasks that have not yet started will have their result senders dropped, /// causing receivers to resolve to `Err(Canceled)`. The currently executing task /// (if any) is aborted via its `AbortHandle`. pub fn cancel_all(&self) { // Abort the currently executing task, if any. if let Some(handle) = self.active_task_handle.lock().unwrap().take() { handle.abort(); } // Drain all pending tasks from the map. Dropping the QueuedTask entries // drops their oneshot senders, signaling cancellation to receivers. self.task_map.lock().unwrap().clear(); } async fn retry_with_backoff( mut fut: impl FnMut() -> Fut, mut retry_options: RetryOption, context: &str, ) -> Result where Fut: Future>, { let mut attempt = 0; let max_attempts = retry_options.remaining_retries(); loop { match fut().await { Ok(res) => return Ok(res), Err(e) => { attempt += 1; let is_transient = e.is_transient(); if !is_transient || attempt > max_attempts { log::warn!( "SyncQueue task failed after {attempt} attempts: {e}. Context: {context}" ); return Err(e); } let delay = retry_options.duration(); retry_options.advance(); log::debug!( "SyncQueue retryable error (attempt {attempt}/{max_attempts}), retrying after {delay:?}. Error: {e}. Context: {context}" ); Timer::after(delay).await; } } } } /// Process tasks from the mpsc receiver. Should be called from an async context. async fn process_queue( mut receiver: MpscReceiver, rate_limit_config: Option, task_map: Arc>>>, active_task_handle: Arc>>, broadcast_sender: Option>>, ) { while let Some(task_id) = receiver.next().await { // Remove the task from the map. If it's missing, it was cancelled. let Some(mut queued_task) = task_map.lock().unwrap().remove(&task_id) else { continue; }; let retry_options = queued_task.retry_options; let rate_limit_config = rate_limit_config.clone(); // Wrap the task in Abortable so cancel_all can abort it. // Rate limiting is inside the abortable so cancellation also // interrupts a task waiting for a rate-limit token. let (abort_handle, abort_registration) = AbortHandle::new_pair(); *active_task_handle.lock().unwrap() = Some(abort_handle); let abortable_result = Abortable::new( async { if let Some(ref rate_limiter) = rate_limit_config { rate_limiter.wait_for_token().await; } let fut = || queued_task.task.run(); Self::retry_with_backoff(fut, retry_options, queued_task.context.as_str()).await }, abort_registration, ) .await; // Clear the active handle now that the task has finished. *active_task_handle.lock().unwrap() = None; match abortable_result { Ok(result) => { if let Some(sender) = queued_task.result_sender { // Per-task mode: deliver via oneshot. let _ = sender.send(result); } else if let Some(ref broadcast_tx) = broadcast_sender { // Streaming mode: deliver via broadcast. let broadcast_result = match result { Ok(value) => Ok(Arc::new(value)), Err(error) => Err(Arc::new(error)), }; if let Err(e) = broadcast_tx.try_broadcast(broadcast_result) { log::warn!("Failed to broadcast task result: {e}"); } } } // Task was aborted by cancel_all — drop the sender to signal cancellation. Err(_aborted) => {} } } log::debug!("No more tasks in the queue. Receiver closed."); } } impl Entity for SyncQueue { type Event = (); } impl SingletonEntity for SyncQueue {} #[cfg(test)] #[path = "sync_queue_tests.rs"] mod tests;