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            checkpoint: None,
150        }
151    }
152
153    async fn registered_output(registry: &JobRegistry) -> Value {
154        registry
155            .get(JobType::new("jobs.example"))
156            .expect("handler exists")
157            .execute(test_context(), json!({}))
158            .await
159            .expect("registered handler should execute")
160            .output()
161            .cloned()
162            .expect("handler should return output")
163    }
164
165    #[tokio::test]
166    async fn registered_handler_executes_successfully_via_trait_object() {
167        let mut registry = JobRegistry::new();
168        registry.register(ExampleHandler);
169        let handler = registry
170            .get(JobType::new("jobs.example"))
171            .expect("handler exists");
172
173        handler
174            .execute(test_context(), json!({}))
175            .await
176            .expect("registered handler should execute");
177    }
178
179    #[test]
180    fn registered_types_returns_sorted_job_types() {
181        let mut registry = JobRegistry::new();
182        registry.register(ExampleHandler);
183
184        assert_eq!(
185            registry.registered_types(),
186            vec![JobType::new("jobs.example")]
187        );
188    }
189
190    #[tokio::test]
191    async fn try_register_rejects_duplicate_job_type_and_keeps_first_handler() {
192        let mut registry = JobRegistry::new();
193        registry
194            .try_register(OutputHandler("first"))
195            .expect("first handler registration should succeed");
196
197        assert_eq!(
198            registry.try_register(OutputHandler("second")),
199            Err(JobRegistryError::DuplicateJobType {
200                job_type: JobType::new("jobs.example"),
201            })
202        );
203        assert_eq!(registered_output(&registry).await, json!("first"));
204    }
205
206    #[tokio::test]
207    async fn try_register_boxed_rejects_duplicate_job_type_and_keeps_first_handler() {
208        let mut registry = JobRegistry::new();
209        registry
210            .try_register_boxed(Arc::new(OutputHandler("first")))
211            .expect("first handler registration should succeed");
212
213        assert_eq!(
214            registry.try_register_boxed(Arc::new(OutputHandler("second"))),
215            Err(JobRegistryError::DuplicateJobType {
216                job_type: JobType::new("jobs.example"),
217            })
218        );
219        assert_eq!(registered_output(&registry).await, json!("first"));
220    }
221
222    #[tokio::test]
223    async fn register_overwrites_duplicate_job_type() {
224        let mut registry = JobRegistry::new();
225        registry.register(OutputHandler("first"));
226        registry.register(OutputHandler("second"));
227
228        assert_eq!(registered_output(&registry).await, json!("second"));
229    }
230
231    #[tokio::test]
232    async fn trait_register_boxed_overwrites_duplicate_job_type() {
233        let mut registry = JobRegistry::new();
234        JobHandlerRegistry::register_boxed(&mut registry, Arc::new(OutputHandler("first")));
235        JobHandlerRegistry::register_boxed(&mut registry, Arc::new(OutputHandler("second")));
236
237        assert_eq!(registered_output(&registry).await, json!("second"));
238    }
239
240    #[test]
241    fn retry_delay_override_matches_job_type_and_failure_code() {
242        let mut registry = JobRegistry::new();
243        registry.register_retry_delay_override(
244            JobType::new("jobs.example"),
245            "job.example.wait",
246            42,
247        );
248
249        assert_eq!(
250            registry.retry_delay_override(JobType::new("jobs.example"), "job.example.wait"),
251            Some(42)
252        );
253        assert_eq!(
254            registry.retry_delay_override(JobType::new("jobs.other"), "job.example.wait"),
255            None
256        );
257        assert_eq!(
258            registry.retry_delay_override(JobType::new("jobs.example"), "job.example.other"),
259            None
260        );
261    }
262
263    #[test]
264    #[should_panic(expected = "retry delay override must be positive")]
265    fn retry_delay_override_rejects_zero_delay() {
266        let mut registry = JobRegistry::new();
267        registry.register_retry_delay_override(JobType::new("jobs.example"), "job.example.wait", 0);
268    }
269}