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>) {
45 self.backends
46 .write()
47 .expect("queue router lock not poisoned")
48 .insert(name.to_string(), backend);
49 }
50
51 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 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 pub fn set_global(router: Self) {
81 let _ = GLOBAL_ROUTER.set(Arc::new(router));
82 }
83
84 pub fn global() -> Arc<Self> {
90 GLOBAL_ROUTER
91 .get()
92 .cloned()
93 .expect("QueueRouter::set_global was not called")
94 }
95
96 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}