boson_core/router/
queue_router.rs1use 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#[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 #[must_use]
26 pub fn new() -> Self {
27 Self {
28 backends: RwLock::new(HashMap::new()),
29 }
30 }
31
32 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 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 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 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 pub fn set_global(router: Self) {
79 let _ = GLOBAL_ROUTER.set(Arc::new(router));
80 }
81
82 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 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}