runledger_runtime/
registry.rs1use 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(®istry).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(®istry).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(®istry).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(®istry).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}