Skip to main content

boson_core/router/
queue_router.rs

1//! Maps logical backend names to concrete [`QueueBackend`](crate::QueueBackend) implementations.
2
3use std::collections::HashMap;
4use std::sync::{Arc, OnceLock, RwLock};
5
6use crate::backend::{QueueBackend, DEFAULT_BACKEND_NAME};
7use crate::error::{BosonError, Result};
8
9static GLOBAL_ROUTER: OnceLock<Arc<QueueRouter>> = OnceLock::new();
10
11/// Registry for named queue backends (multi-backend hosts).
12#[derive(Debug)]
13pub struct QueueRouter {
14    backends: RwLock<HashMap<String, Arc<dyn QueueBackend>>>,
15}
16
17impl Default for QueueRouter {
18    fn default() -> Self {
19        Self::new()
20    }
21}
22
23impl QueueRouter {
24    /// Empty registry.
25    #[must_use]
26    pub fn new() -> Self {
27        Self {
28            backends: RwLock::new(HashMap::new()),
29        }
30    }
31
32    /// Register a single default backend under [`DEFAULT_BACKEND_NAME`](crate::backend::DEFAULT_BACKEND_NAME).
33    pub fn with_default(backend: Arc<dyn QueueBackend>) -> Self {
34        let mut router = Self::new();
35        router.register(DEFAULT_BACKEND_NAME, backend);
36        router
37    }
38
39    /// Register during initial setup (mutable bootstrap).
40    ///
41    /// Recovers from a poisoned lock by taking the inner value.
42    pub fn register(&mut self, name: &str, backend: Arc<dyn QueueBackend>) {
43        self.backends
44            .write()
45            .unwrap_or_else(std::sync::PoisonError::into_inner)
46            .insert(name.to_string(), backend);
47    }
48
49    /// Register after [`Self::set_global`] (runtime registration).
50    ///
51    /// # Errors
52    ///
53    /// Returns [`BosonError::Internal`](crate::BosonError::Internal) if the lock is poisoned.
54    pub fn register_runtime(&self, name: &str, backend: Arc<dyn QueueBackend>) -> Result<()> {
55        self.backends
56            .write()
57            .map_err(|_| BosonError::internal("queue router lock poisoned"))?
58            .insert(name.to_string(), backend);
59        Ok(())
60    }
61
62    /// Resolve a backend by logical name.
63    ///
64    /// # Errors
65    ///
66    /// Returns [`BosonError::Internal`](crate::BosonError::Internal) if the lock is poisoned,
67    /// or [`BosonError::UnknownBackend`](crate::BosonError::UnknownBackend) when `name` is not registered.
68    pub fn resolve(&self, name: &str) -> Result<Arc<dyn QueueBackend>> {
69        self.backends
70            .read()
71            .map_err(|_| BosonError::internal("queue router lock poisoned"))?
72            .get(name)
73            .cloned()
74            .ok_or_else(|| BosonError::UnknownBackend(name.to_string()))
75    }
76
77    /// Install the process-global router (call once at host boot).
78    pub fn set_global(router: Self) {
79        let _ = GLOBAL_ROUTER.set(Arc::new(router));
80    }
81
82    /// Global router installed via [`Self::set_global`].
83    ///
84    /// # Errors
85    ///
86    /// Returns [`BosonError::Internal`] when [`Self::set_global`] was not called.
87    pub fn global() -> Result<Arc<Self>> {
88        GLOBAL_ROUTER
89            .get()
90            .cloned()
91            .ok_or_else(|| BosonError::internal("QueueRouter::set_global was not called"))
92    }
93
94    /// Optional global router.
95    pub fn try_global() -> Option<Arc<Self>> {
96        GLOBAL_ROUTER.get().cloned()
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103    use crate::backend::JobEnqueueDisposition;
104    use crate::models::{Job, TaskConfig};
105    use async_trait::async_trait;
106    use chrono::{DateTime, Utc};
107
108    struct StubBackend;
109
110    impl std::fmt::Debug for StubBackend {
111        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112            f.write_str("StubBackend")
113        }
114    }
115
116    #[async_trait]
117    impl QueueBackend for StubBackend {
118        async fn upsert_job(&self, _job: &Job) -> Result<()> {
119            Ok(())
120        }
121
122        async fn enqueue_with_policies(
123            &self,
124            job: Job,
125            _task_config: &TaskConfig,
126        ) -> Result<(String, JobEnqueueDisposition)> {
127            Ok((job.job_id, JobEnqueueDisposition::InsertedNew))
128        }
129
130        async fn get_job(&self, _job_id: &str) -> Result<Option<Job>> {
131            Ok(None)
132        }
133
134        async fn list_jobs(
135            &self,
136            _status_filter: Option<crate::models::JobStatus>,
137            _offset: usize,
138            _limit: usize,
139        ) -> Result<Vec<Job>> {
140            Ok(vec![])
141        }
142
143        async fn cancel_job_if_active(&self, _job_id: &str) -> Result<()> {
144            Ok(())
145        }
146
147        async fn try_claim_job(&self, _job_id: &str) -> Result<Option<Job>> {
148            Ok(None)
149        }
150
151        async fn revert_job_to_queued(&self, _job_id: &str) -> Result<()> {
152            Ok(())
153        }
154
155        async fn distinct_pools_queued(&self) -> Result<Vec<String>> {
156            Ok(vec![])
157        }
158
159        async fn list_queued_for_pool_sorted(
160            &self,
161            _pool: &str,
162            _limit: usize,
163        ) -> Result<Vec<Job>> {
164            Ok(vec![])
165        }
166
167        async fn count_jobs(
168            &self,
169            _status_filter: Option<crate::models::JobStatus>,
170        ) -> Result<u64> {
171            Ok(0)
172        }
173
174        async fn count_jobs_for_task(
175            &self,
176            _task_name: &str,
177            _status: Option<crate::models::JobStatus>,
178        ) -> Result<u64> {
179            Ok(0)
180        }
181
182        async fn count_active_jobs_for_task(&self, _task_name: &str) -> Result<u32> {
183            Ok(0)
184        }
185
186        async fn find_nonterminal_by_idempotency_key(&self, _key: &str) -> Result<Option<String>> {
187            Ok(None)
188        }
189
190        async fn upsert_run(&self, _run: &crate::models::Run) -> Result<()> {
191            Ok(())
192        }
193
194        async fn get_run(&self, _run_id: &str) -> Result<Option<crate::models::Run>> {
195            Ok(None)
196        }
197
198        async fn list_runs(
199            &self,
200            _job_id_filter: Option<&str>,
201            _offset: usize,
202            _limit: usize,
203        ) -> Result<Vec<crate::models::Run>> {
204            Ok(vec![])
205        }
206
207        async fn finish_run(
208            &self,
209            _run_id: &str,
210            _status: crate::models::RunStatus,
211            _duration_ms: Option<i64>,
212            _error_message: Option<String>,
213        ) -> Result<()> {
214            Ok(())
215        }
216
217        async fn count_runs(&self, _job_id_filter: Option<&str>) -> Result<u64> {
218            Ok(0)
219        }
220
221        async fn count_runs_since(&self, _since: DateTime<Utc>) -> Result<u64> {
222            Ok(0)
223        }
224
225        async fn task_run_stats(&self, _task_name: &str) -> Result<crate::models::TaskRunStats> {
226            Ok(crate::models::TaskRunStats {
227                runs_total: 0,
228                success_count: 0,
229            })
230        }
231
232        async fn get_task_config(&self, _task_name: &str) -> Result<Option<TaskConfig>> {
233            Ok(None)
234        }
235
236        async fn upsert_task_config(&self, _config: &TaskConfig) -> Result<()> {
237            Ok(())
238        }
239
240        async fn try_claim_run_lease(
241            &self,
242            _job_id: &str,
243            _worker_id: &str,
244            _ttl_secs: i64,
245        ) -> Result<Option<String>> {
246            Ok(None)
247        }
248
249        async fn extend_lease(&self, _lease_id: &str, _ttl_secs: i64) -> Result<()> {
250            Ok(())
251        }
252
253        async fn release_lease(&self, _lease_id: &str) -> Result<()> {
254            Ok(())
255        }
256
257        async fn expired_lease_job_pairs(&self) -> Result<Vec<(String, String)>> {
258            Ok(vec![])
259        }
260    }
261
262    #[test]
263    fn register_and_resolve() {
264        let mut router = QueueRouter::new();
265        let backend: Arc<dyn QueueBackend> = Arc::new(StubBackend);
266        router.register("test", Arc::clone(&backend));
267        let resolved = router.resolve("test").expect("resolve");
268        assert!(Arc::ptr_eq(&resolved, &backend));
269    }
270
271    #[test]
272    fn unknown_backend_errors() {
273        let router = QueueRouter::new();
274        let err = router.resolve("missing").unwrap_err();
275        assert!(matches!(err, BosonError::UnknownBackend(_)));
276    }
277}