1#![allow(deprecated)] use std::collections::HashMap;
13use std::time::Duration;
14
15use reinhardt_core::macros::settings;
16use serde::{Deserialize, Serialize};
17
18use crate::webhook::{HttpWebhookSender, RetryConfig, WebhookConfig};
19use crate::worker::{Worker, WorkerConfig};
20
21fn default_queue_name() -> String {
24 "default".to_string()
25}
26fn default_max_retries() -> u32 {
27 3
28}
29fn default_worker_name() -> String {
30 "worker".to_string()
31}
32fn default_concurrency() -> usize {
33 4
34}
35fn default_poll_interval_ms() -> u64 {
36 1000
37}
38fn default_webhook_method() -> String {
39 "POST".to_string()
40}
41fn default_webhook_timeout_secs() -> u64 {
42 5
43}
44fn default_retry_max_retries() -> u32 {
45 3
46}
47fn default_retry_initial_backoff_ms() -> u64 {
48 100
49}
50fn default_retry_max_backoff_ms() -> u64 {
51 30_000
52}
53fn default_retry_backoff_multiplier() -> f64 {
54 2.0
55}
56
57#[settings(fragment = true, section = "tasks_queue")]
67#[non_exhaustive]
68#[derive(Clone, Debug, Serialize, Deserialize)]
69pub struct QueueSettings {
70 #[serde(default = "default_queue_name")]
72 pub name: String,
73 #[serde(default = "default_max_retries")]
75 pub max_retries: u32,
76}
77
78impl Default for QueueSettings {
79 fn default() -> Self {
80 Self {
81 name: default_queue_name(),
82 max_retries: default_max_retries(),
83 }
84 }
85}
86
87#[settings(fragment = true)]
94#[derive(Clone, Debug, Serialize, Deserialize)]
95pub struct WebhookRetrySettings {
96 #[serde(default = "default_retry_max_retries")]
98 pub max_retries: u32,
99 #[serde(default = "default_retry_initial_backoff_ms")]
101 pub initial_backoff_ms: u64,
102 #[serde(default = "default_retry_max_backoff_ms")]
104 pub max_backoff_ms: u64,
105 #[serde(default = "default_retry_backoff_multiplier")]
107 pub backoff_multiplier: f64,
108}
109
110impl Default for WebhookRetrySettings {
111 fn default() -> Self {
112 Self {
113 max_retries: default_retry_max_retries(),
114 initial_backoff_ms: default_retry_initial_backoff_ms(),
115 max_backoff_ms: default_retry_max_backoff_ms(),
116 backoff_multiplier: default_retry_backoff_multiplier(),
117 }
118 }
119}
120
121impl From<&WebhookRetrySettings> for RetryConfig {
122 fn from(settings: &WebhookRetrySettings) -> Self {
123 Self {
124 max_retries: settings.max_retries,
125 initial_backoff: Duration::from_millis(settings.initial_backoff_ms),
126 max_backoff: Duration::from_millis(settings.max_backoff_ms),
127 backoff_multiplier: settings.backoff_multiplier,
128 }
129 }
130}
131
132#[settings(fragment = true, section = "tasks_webhook")]
136#[non_exhaustive]
137#[derive(Clone, Debug, Serialize, Deserialize)]
138pub struct WebhookSettings {
139 #[serde(default)]
141 pub url: String,
142 #[serde(default = "default_webhook_method")]
144 pub method: String,
145 #[serde(default)]
147 pub headers: HashMap<String, String>,
148 #[serde(default = "default_webhook_timeout_secs")]
150 pub timeout_secs: u64,
151 #[setting(node)]
153 #[serde(default)]
154 pub retry: WebhookRetrySettings,
155}
156
157impl Default for WebhookSettings {
158 fn default() -> Self {
159 Self {
160 url: String::new(),
161 method: default_webhook_method(),
162 headers: HashMap::new(),
163 timeout_secs: default_webhook_timeout_secs(),
164 retry: WebhookRetrySettings::default(),
165 }
166 }
167}
168
169impl From<&WebhookSettings> for WebhookConfig {
170 fn from(settings: &WebhookSettings) -> Self {
171 Self {
172 url: settings.url.clone(),
173 method: settings.method.clone(),
174 headers: settings.headers.clone(),
175 timeout: Duration::from_secs(settings.timeout_secs),
176 retry_config: RetryConfig::from(&settings.retry),
177 }
178 }
179}
180
181pub fn create_webhook_sender_from_settings(settings: &WebhookSettings) -> HttpWebhookSender {
183 HttpWebhookSender::new(WebhookConfig::from(settings))
184}
185
186#[settings(fragment = true, section = "tasks_worker")]
192#[non_exhaustive]
193#[derive(Clone, Debug, Serialize, Deserialize)]
194pub struct WorkerSettings {
195 #[serde(default = "default_worker_name")]
197 pub name: String,
198 #[serde(default = "default_concurrency")]
200 pub concurrency: usize,
201 #[serde(default = "default_poll_interval_ms")]
203 pub poll_interval_ms: u64,
204 #[setting(node)]
206 #[serde(default)]
207 pub webhooks: Vec<WebhookSettings>,
208}
209
210impl Default for WorkerSettings {
211 fn default() -> Self {
212 Self {
213 name: default_worker_name(),
214 concurrency: default_concurrency(),
215 poll_interval_ms: default_poll_interval_ms(),
216 webhooks: Vec::new(),
217 }
218 }
219}
220
221impl From<&WorkerSettings> for WorkerConfig {
222 fn from(settings: &WorkerSettings) -> Self {
223 Self {
224 name: settings.name.clone(),
225 concurrency: settings.concurrency,
226 poll_interval: Duration::from_millis(settings.poll_interval_ms),
227 webhook_configs: settings.webhooks.iter().map(WebhookConfig::from).collect(),
228 }
229 }
230}
231
232pub fn create_worker_from_settings(settings: &WorkerSettings) -> Worker {
234 Worker::new(WorkerConfig::from(settings))
235}
236
237#[cfg(feature = "sqs-backend")]
240fn default_sqs_visibility_timeout() -> i32 {
241 30
242}
243#[cfg(feature = "sqs-backend")]
244fn default_sqs_max_messages() -> i32 {
245 1
246}
247#[cfg(feature = "sqs-backend")]
248fn default_sqs_wait_time_seconds() -> i32 {
249 0
250}
251
252#[cfg(feature = "sqs-backend")]
256#[settings(fragment = true, section = "tasks_sqs")]
257#[non_exhaustive]
258#[derive(Clone, Debug, Serialize, Deserialize)]
259pub struct SqsSettings {
260 #[serde(default)]
262 pub queue_url: String,
263 #[serde(default = "default_sqs_visibility_timeout")]
265 pub visibility_timeout: i32,
266 #[serde(default = "default_sqs_max_messages")]
268 pub max_messages: i32,
269 #[serde(default = "default_sqs_wait_time_seconds")]
271 pub wait_time_seconds: i32,
272}
273
274#[cfg(feature = "sqs-backend")]
275impl Default for SqsSettings {
276 fn default() -> Self {
277 Self {
278 queue_url: String::new(),
279 visibility_timeout: default_sqs_visibility_timeout(),
280 max_messages: default_sqs_max_messages(),
281 wait_time_seconds: default_sqs_wait_time_seconds(),
282 }
283 }
284}
285
286#[cfg(feature = "sqs-backend")]
287impl From<&SqsSettings> for crate::backends::sqs::SqsConfig {
288 fn from(settings: &SqsSettings) -> Self {
289 crate::backends::sqs::SqsConfig::new(settings.queue_url.clone())
291 .with_visibility_timeout(settings.visibility_timeout)
292 .with_max_messages(settings.max_messages)
293 .with_wait_time_seconds(settings.wait_time_seconds)
294 }
295}
296
297#[cfg(feature = "sqs-backend")]
300pub async fn create_sqs_backend_from_settings(
301 settings: &SqsSettings,
302) -> Result<crate::backends::sqs::SqsBackend, crate::TaskExecutionError> {
303 crate::backends::sqs::SqsBackend::new(crate::backends::sqs::SqsConfig::from(settings)).await
304}
305
306#[cfg(feature = "rabbitmq-backend")]
309fn default_rabbitmq_url() -> String {
310 "amqp://localhost:5672/%2f".to_string()
311}
312#[cfg(feature = "rabbitmq-backend")]
313fn default_rabbitmq_queue_name() -> String {
314 "reinhardt_tasks".to_string()
315}
316#[cfg(feature = "rabbitmq-backend")]
317fn default_rabbitmq_routing_key() -> String {
318 "reinhardt_tasks".to_string()
319}
320
321#[cfg(feature = "rabbitmq-backend")]
326#[settings(fragment = true, section = "tasks_rabbitmq")]
327#[non_exhaustive]
328#[derive(Clone, Debug, Serialize, Deserialize)]
329pub struct RabbitMQSettings {
330 #[serde(default = "default_rabbitmq_url")]
332 pub url: String,
333 #[serde(default = "default_rabbitmq_queue_name")]
335 pub queue_name: String,
336 #[serde(default)]
338 pub exchange_name: String,
339 #[serde(default = "default_rabbitmq_routing_key")]
341 pub routing_key: String,
342}
343
344#[cfg(feature = "rabbitmq-backend")]
345impl Default for RabbitMQSettings {
346 fn default() -> Self {
347 Self {
348 url: default_rabbitmq_url(),
349 queue_name: default_rabbitmq_queue_name(),
350 exchange_name: String::new(),
351 routing_key: default_rabbitmq_routing_key(),
352 }
353 }
354}
355
356#[cfg(feature = "rabbitmq-backend")]
357impl From<&RabbitMQSettings> for crate::backends::rabbitmq::RabbitMQConfig {
358 fn from(settings: &RabbitMQSettings) -> Self {
359 Self {
360 url: settings.url.clone(),
361 queue_name: settings.queue_name.clone(),
362 exchange_name: settings.exchange_name.clone(),
363 routing_key: settings.routing_key.clone(),
364 }
365 }
366}
367
368#[cfg(feature = "rabbitmq-backend")]
371pub async fn create_rabbitmq_backend_from_settings(
372 settings: &RabbitMQSettings,
373) -> Result<crate::backends::rabbitmq::RabbitMQBackend, lapin::Error> {
374 crate::backends::rabbitmq::RabbitMQBackend::new(
375 crate::backends::rabbitmq::RabbitMQConfig::from(settings),
376 )
377 .await
378}
379
380#[cfg(test)]
381mod tests {
382 use super::*;
383 use reinhardt_conf::settings::fragment::SettingsFragment;
384
385 #[rstest::rstest]
386 fn section_names_are_crate_prefixed() {
387 assert_eq!(QueueSettings::section(), "tasks_queue");
389 assert_eq!(WorkerSettings::section(), "tasks_worker");
390 assert_eq!(WebhookSettings::section(), "tasks_webhook");
391 }
392
393 #[rstest::rstest]
394 fn queue_settings_default_has_expected_values() {
395 let settings = QueueSettings::default();
397
398 assert_eq!(settings.name, "default");
400 assert_eq!(settings.max_retries, 3);
401 }
402
403 #[rstest::rstest]
404 fn worker_settings_convert_milliseconds_to_duration() {
405 let settings = WorkerSettings {
407 name: "ingest".to_string(),
408 concurrency: 8,
409 poll_interval_ms: 2500,
410 webhooks: Vec::new(),
411 };
412
413 let config = WorkerConfig::from(&settings);
415
416 assert_eq!(config.name, "ingest");
418 assert_eq!(config.concurrency, 8);
419 assert_eq!(config.poll_interval, Duration::from_millis(2500));
420 assert!(config.webhook_configs.is_empty());
421 }
422
423 #[rstest::rstest]
424 fn webhook_settings_convert_seconds_and_nested_retry() {
425 let settings = WebhookSettings::default();
427
428 let config = WebhookConfig::from(&settings);
430
431 assert_eq!(config.method, "POST");
433 assert_eq!(config.timeout, Duration::from_secs(5));
434 assert_eq!(config.retry_config.max_retries, 3);
435 assert_eq!(
436 config.retry_config.initial_backoff,
437 Duration::from_millis(100)
438 );
439 assert_eq!(config.retry_config.max_backoff, Duration::from_secs(30));
440 assert_eq!(config.retry_config.backoff_multiplier, 2.0);
441 }
442
443 #[rstest::rstest]
444 fn worker_settings_map_nested_webhooks() {
445 let settings = WorkerSettings {
447 name: "w".to_string(),
448 concurrency: 1,
449 poll_interval_ms: 100,
450 webhooks: vec![WebhookSettings {
451 url: "https://example.com/hook".to_string(),
452 ..WebhookSettings::default()
453 }],
454 };
455
456 let config = WorkerConfig::from(&settings);
458
459 assert_eq!(config.webhook_configs.len(), 1);
461 assert_eq!(config.webhook_configs[0].url, "https://example.com/hook");
462 }
463
464 #[rstest::rstest]
465 fn webhook_settings_deserialize_with_defaults() {
466 let json = r#"{ "url": "https://example.com/hook", "timeout_secs": 10 }"#;
468
469 let settings: WebhookSettings = serde_json::from_str(json).unwrap();
471 let config = WebhookConfig::from(&settings);
472
473 assert_eq!(config.url, "https://example.com/hook");
475 assert_eq!(config.method, "POST");
476 assert_eq!(config.timeout, Duration::from_secs(10));
477 assert_eq!(config.retry_config.max_retries, 3);
478 }
479
480 #[cfg(feature = "sqs-backend")]
481 #[rstest::rstest]
482 fn sqs_settings_default_converts_to_config() {
483 let settings = SqsSettings {
485 queue_url: "https://sqs.example.com/q".to_string(),
486 ..SqsSettings::default()
487 };
488
489 let config = crate::backends::sqs::SqsConfig::from(&settings);
491
492 assert!(format!("{config:?}").contains("https://sqs.example.com/q"));
494 }
495
496 #[cfg(feature = "rabbitmq-backend")]
497 #[rstest::rstest]
498 fn rabbitmq_settings_default_converts_to_config() {
499 let settings = RabbitMQSettings::default();
501
502 let config = crate::backends::rabbitmq::RabbitMQConfig::from(&settings);
504
505 assert_eq!(config.queue_name, "reinhardt_tasks");
507 assert_eq!(config.routing_key, "reinhardt_tasks");
508 }
509}