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