Skip to main content

runledger_runtime/
registry.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3
4pub use runledger_core::jobs::JobHandler;
5use runledger_core::jobs::{JobHandlerRegistry, JobType};
6use thiserror::Error;
7
8#[derive(Debug, Clone, Error, Eq, PartialEq)]
9#[non_exhaustive]
10pub enum JobRegistryError {
11    #[error("job handler already registered for {job_type}")]
12    DuplicateJobType { job_type: JobType<'static> },
13}
14
15#[derive(Clone, Default)]
16pub struct JobRegistry {
17    handlers: HashMap<JobType<'static>, Arc<dyn JobHandler>>,
18    retry_delay_overrides: HashMap<JobType<'static>, HashMap<&'static str, i32>>,
19}
20
21impl JobRegistry {
22    #[must_use]
23    pub fn new() -> Self {
24        Self::default()
25    }
26
27    pub fn register<H>(&mut self, handler: H)
28    where
29        H: JobHandler + 'static,
30    {
31        let handler: Arc<dyn JobHandler> = Arc::new(handler);
32        self.handlers.insert(handler.job_type(), handler);
33    }
34
35    pub fn try_register<H>(&mut self, handler: H) -> Result<(), JobRegistryError>
36    where
37        H: JobHandler + 'static,
38    {
39        self.try_register_boxed(Arc::new(handler))
40    }
41
42    pub fn try_register_boxed(
43        &mut self,
44        handler: Arc<dyn JobHandler>,
45    ) -> Result<(), JobRegistryError> {
46        let job_type = handler.job_type();
47        if self.handlers.contains_key(job_type.as_str()) {
48            return Err(JobRegistryError::DuplicateJobType { job_type });
49        }
50
51        self.handlers.insert(job_type, handler);
52        Ok(())
53    }
54
55    pub fn register_retry_delay_override(
56        &mut self,
57        job_type: JobType<'static>,
58        failure_code: &'static str,
59        retry_delay_ms: i32,
60    ) {
61        assert!(retry_delay_ms > 0, "retry delay override must be positive");
62
63        self.retry_delay_overrides
64            .entry(job_type)
65            .or_default()
66            .insert(failure_code, retry_delay_ms);
67    }
68
69    #[must_use]
70    pub fn get(&self, job_type: JobType<'_>) -> Option<Arc<dyn JobHandler>> {
71        self.handlers.get(job_type.as_str()).cloned()
72    }
73
74    #[must_use]
75    pub fn retry_delay_override(&self, job_type: JobType<'_>, failure_code: &str) -> Option<i32> {
76        self.retry_delay_overrides
77            .get(job_type.as_str())
78            .and_then(|overrides| overrides.get(failure_code).copied())
79    }
80
81    #[must_use]
82    pub fn registered_types(&self) -> Vec<JobType<'_>> {
83        let mut keys: Vec<JobType<'_>> = self.handlers.keys().copied().collect();
84        keys.sort_unstable();
85        keys
86    }
87}
88
89impl JobHandlerRegistry for JobRegistry {
90    fn register_boxed(&mut self, handler: Arc<dyn JobHandler>) {
91        self.handlers.insert(handler.job_type(), handler);
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use std::sync::Arc;
98
99    use async_trait::async_trait;
100    use runledger_core::jobs::{
101        JobCompletion, JobContext, JobFailure, JobHandlerRegistry, JobType,
102    };
103    use serde_json::{Value, json};
104    use uuid::Uuid;
105
106    use super::{JobHandler, JobRegistry, JobRegistryError};
107
108    struct ExampleHandler;
109
110    #[async_trait]
111    impl JobHandler for ExampleHandler {
112        fn job_type(&self) -> JobType<'static> {
113            JobType::new("jobs.example")
114        }
115
116        async fn execute(
117            &self,
118            _context: JobContext,
119            _payload: Value,
120        ) -> Result<JobCompletion, JobFailure> {
121            Ok(JobCompletion::success())
122        }
123    }
124
125    struct OutputHandler(&'static str);
126
127    #[async_trait]
128    impl JobHandler for OutputHandler {
129        fn job_type(&self) -> JobType<'static> {
130            JobType::new("jobs.example")
131        }
132
133        async fn execute(
134            &self,
135            _context: JobContext,
136            _payload: Value,
137        ) -> Result<JobCompletion, JobFailure> {
138            Ok(JobCompletion::with_output(json!(self.0)))
139        }
140    }
141
142    fn test_context() -> JobContext {
143        JobContext {
144            job_id: Uuid::now_v7(),
145            run_number: 1,
146            attempt: 1,
147            organization_id: None,
148            worker_id: "registry-test-worker".to_string(),
149        }
150    }
151
152    async fn registered_output(registry: &JobRegistry) -> Value {
153        registry
154            .get(JobType::new("jobs.example"))
155            .expect("handler exists")
156            .execute(test_context(), json!({}))
157            .await
158            .expect("registered handler should execute")
159            .output
160            .expect("handler should return output")
161    }
162
163    #[tokio::test]
164    async fn registered_handler_executes_successfully_via_trait_object() {
165        let mut registry = JobRegistry::new();
166        registry.register(ExampleHandler);
167        let handler = registry
168            .get(JobType::new("jobs.example"))
169            .expect("handler exists");
170
171        handler
172            .execute(test_context(), json!({}))
173            .await
174            .expect("registered handler should execute");
175    }
176
177    #[test]
178    fn registered_types_returns_sorted_job_types() {
179        let mut registry = JobRegistry::new();
180        registry.register(ExampleHandler);
181
182        assert_eq!(
183            registry.registered_types(),
184            vec![JobType::new("jobs.example")]
185        );
186    }
187
188    #[tokio::test]
189    async fn try_register_rejects_duplicate_job_type_and_keeps_first_handler() {
190        let mut registry = JobRegistry::new();
191        registry
192            .try_register(OutputHandler("first"))
193            .expect("first handler registration should succeed");
194
195        assert_eq!(
196            registry.try_register(OutputHandler("second")),
197            Err(JobRegistryError::DuplicateJobType {
198                job_type: JobType::new("jobs.example"),
199            })
200        );
201        assert_eq!(registered_output(&registry).await, json!("first"));
202    }
203
204    #[tokio::test]
205    async fn try_register_boxed_rejects_duplicate_job_type_and_keeps_first_handler() {
206        let mut registry = JobRegistry::new();
207        registry
208            .try_register_boxed(Arc::new(OutputHandler("first")))
209            .expect("first handler registration should succeed");
210
211        assert_eq!(
212            registry.try_register_boxed(Arc::new(OutputHandler("second"))),
213            Err(JobRegistryError::DuplicateJobType {
214                job_type: JobType::new("jobs.example"),
215            })
216        );
217        assert_eq!(registered_output(&registry).await, json!("first"));
218    }
219
220    #[tokio::test]
221    async fn register_overwrites_duplicate_job_type() {
222        let mut registry = JobRegistry::new();
223        registry.register(OutputHandler("first"));
224        registry.register(OutputHandler("second"));
225
226        assert_eq!(registered_output(&registry).await, json!("second"));
227    }
228
229    #[tokio::test]
230    async fn trait_register_boxed_overwrites_duplicate_job_type() {
231        let mut registry = JobRegistry::new();
232        JobHandlerRegistry::register_boxed(&mut registry, Arc::new(OutputHandler("first")));
233        JobHandlerRegistry::register_boxed(&mut registry, Arc::new(OutputHandler("second")));
234
235        assert_eq!(registered_output(&registry).await, json!("second"));
236    }
237
238    #[test]
239    fn retry_delay_override_matches_job_type_and_failure_code() {
240        let mut registry = JobRegistry::new();
241        registry.register_retry_delay_override(
242            JobType::new("jobs.example"),
243            "job.example.wait",
244            42,
245        );
246
247        assert_eq!(
248            registry.retry_delay_override(JobType::new("jobs.example"), "job.example.wait"),
249            Some(42)
250        );
251        assert_eq!(
252            registry.retry_delay_override(JobType::new("jobs.other"), "job.example.wait"),
253            None
254        );
255        assert_eq!(
256            registry.retry_delay_override(JobType::new("jobs.example"), "job.example.other"),
257            None
258        );
259    }
260
261    #[test]
262    #[should_panic(expected = "retry delay override must be positive")]
263    fn retry_delay_override_rejects_zero_delay() {
264        let mut registry = JobRegistry::new();
265        registry.register_retry_delay_override(JobType::new("jobs.example"), "job.example.wait", 0);
266    }
267}