1use core::fmt;
7use std::env::VarError;
8use std::time::Duration;
9
10use crate::ordering::Ordering;
11use crate::retry::ExponentialBackoff;
12use crate::worker::WorkerId;
13
14#[derive(Clone, Debug, Default)]
17#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
18#[cfg_attr(feature = "serde", serde(default, deny_unknown_fields))]
19#[non_exhaustive]
20pub struct OutboxSettings {
21 pub dispatcher: DispatcherSettings,
23 pub retention: RetentionSettings,
25}
26
27impl OutboxSettings {
28 #[must_use]
30 pub fn dispatcher(mut self, dispatcher: DispatcherSettings) -> Self {
31 self.dispatcher = dispatcher;
32 self
33 }
34
35 #[must_use]
37 pub fn retention(mut self, retention: RetentionSettings) -> Self {
38 self.retention = retention;
39 self
40 }
41}
42
43#[derive(Clone, Debug)]
46#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
47#[cfg_attr(feature = "serde", serde(default, deny_unknown_fields))]
48#[non_exhaustive]
49pub struct DispatcherSettings {
50 pub batch_size: u32,
60 #[cfg_attr(
62 feature = "serde",
63 serde(rename = "lease_ms", with = "crate::duration_serde::millis")
64 )]
65 pub lease: Duration,
66 pub max_in_flight: usize,
68 #[cfg_attr(
71 feature = "serde",
72 serde(rename = "publish_timeout_ms", with = "crate::duration_serde::millis")
73 )]
74 pub publish_timeout: Duration,
75 #[cfg_attr(
81 feature = "serde",
82 serde(rename = "poll_interval_ms", with = "crate::duration_serde::millis")
83 )]
84 pub poll_interval: Duration,
85 #[cfg_attr(
90 feature = "serde",
91 serde(
92 rename = "idle_poll_interval_ms",
93 with = "crate::duration_serde::millis"
94 )
95 )]
96 pub idle_poll_interval: Duration,
97 #[cfg_attr(
106 feature = "serde",
107 serde(rename = "drain_timeout_ms", with = "crate::duration_serde::millis")
108 )]
109 pub drain_timeout: Duration,
110 #[cfg_attr(
122 feature = "serde",
123 serde(rename = "store_timeout_ms", with = "crate::duration_serde::millis")
124 )]
125 pub store_timeout: Duration,
126 #[cfg_attr(
129 feature = "serde",
130 serde(rename = "stats_interval_ms", with = "crate::duration_serde::millis")
131 )]
132 pub stats_interval: Duration,
133 pub ordering: Ordering,
135 pub retry: ExponentialBackoff,
138 pub worker_id: Option<WorkerId>,
140}
141
142impl Default for DispatcherSettings {
143 fn default() -> Self {
144 Self {
145 batch_size: 100,
146 lease: Duration::from_secs(30),
147 max_in_flight: 16,
148 publish_timeout: Duration::from_secs(10),
149 poll_interval: Duration::from_millis(500),
150 idle_poll_interval: Duration::from_secs(5),
151 drain_timeout: Duration::from_secs(30),
152 store_timeout: Duration::from_secs(10),
153 stats_interval: Duration::from_secs(15),
154 ordering: Ordering::default(),
155 retry: ExponentialBackoff::default(),
156 worker_id: None,
157 }
158 }
159}
160
161impl DispatcherSettings {
162 #[must_use]
164 pub const fn batch_size(mut self, batch_size: u32) -> Self {
165 self.batch_size = batch_size;
166 self
167 }
168
169 #[must_use]
171 pub const fn lease(mut self, lease: Duration) -> Self {
172 self.lease = lease;
173 self
174 }
175
176 #[must_use]
178 pub const fn max_in_flight(mut self, max_in_flight: usize) -> Self {
179 self.max_in_flight = max_in_flight;
180 self
181 }
182
183 #[must_use]
185 pub const fn publish_timeout(mut self, publish_timeout: Duration) -> Self {
186 self.publish_timeout = publish_timeout;
187 self
188 }
189
190 #[must_use]
192 pub const fn poll_interval(mut self, poll_interval: Duration) -> Self {
193 self.poll_interval = poll_interval;
194 self
195 }
196
197 #[must_use]
199 pub const fn idle_poll_interval(mut self, idle_poll_interval: Duration) -> Self {
200 self.idle_poll_interval = idle_poll_interval;
201 self
202 }
203
204 #[must_use]
206 pub const fn drain_timeout(mut self, drain_timeout: Duration) -> Self {
207 self.drain_timeout = drain_timeout;
208 self
209 }
210
211 #[must_use]
213 pub const fn store_timeout(mut self, store_timeout: Duration) -> Self {
214 self.store_timeout = store_timeout;
215 self
216 }
217
218 #[must_use]
220 pub const fn stats_interval(mut self, stats_interval: Duration) -> Self {
221 self.stats_interval = stats_interval;
222 self
223 }
224
225 #[must_use]
227 pub const fn ordering(mut self, ordering: Ordering) -> Self {
228 self.ordering = ordering;
229 self
230 }
231
232 #[must_use]
234 pub const fn retry(mut self, retry: ExponentialBackoff) -> Self {
235 self.retry = retry;
236 self
237 }
238
239 #[must_use]
241 pub fn worker_id(mut self, worker_id: WorkerId) -> Self {
242 self.worker_id = Some(worker_id);
243 self
244 }
245}
246
247#[derive(Clone, Debug)]
249#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
250#[cfg_attr(feature = "serde", serde(default, deny_unknown_fields))]
251#[non_exhaustive]
252pub struct RetentionSettings {
253 #[cfg_attr(
256 feature = "serde",
257 serde(
258 rename = "published_retention_ms",
259 with = "crate::duration_serde::millis"
260 )
261 )]
262 pub published_retention: Duration,
263 #[cfg_attr(
266 feature = "serde",
267 serde(
268 rename = "dead_retention_ms",
269 with = "crate::duration_serde::optional_millis"
270 )
271 )]
272 pub dead_retention: Option<Duration>,
273 pub purge_batch_size: u32,
276}
277
278impl Default for RetentionSettings {
279 fn default() -> Self {
280 Self {
281 published_retention: Duration::from_secs(7 * 24 * 60 * 60),
282 dead_retention: None,
283 purge_batch_size: 1_000,
284 }
285 }
286}
287
288impl RetentionSettings {
289 #[must_use]
291 pub const fn published_retention(mut self, retention: Duration) -> Self {
292 self.published_retention = retention;
293 self
294 }
295
296 #[must_use]
298 pub const fn dead_retention(mut self, retention: Option<Duration>) -> Self {
299 self.dead_retention = retention;
300 self
301 }
302
303 #[must_use]
305 pub const fn purge_batch_size(mut self, purge_batch_size: u32) -> Self {
306 self.purge_batch_size = purge_batch_size;
307 self
308 }
309}
310
311impl OutboxSettings {
312 pub fn from_env(prefix: &str) -> Result<Self, SettingsError> {
324 let mut dispatcher = DispatcherSettings::default();
325 let mut retention = RetentionSettings::default();
326
327 if let Some(v) = env_u32(prefix, "BATCH_SIZE")? {
328 dispatcher.batch_size = v;
329 }
330 if let Some(v) = env_duration_ms(prefix, "LEASE_MS")? {
331 dispatcher.lease = v;
332 }
333 if let Some(v) = env_usize(prefix, "MAX_IN_FLIGHT")? {
334 dispatcher.max_in_flight = v;
335 }
336 if let Some(v) = env_duration_ms(prefix, "PUBLISH_TIMEOUT_MS")? {
337 dispatcher.publish_timeout = v;
338 }
339 if let Some(v) = env_duration_ms(prefix, "POLL_INTERVAL_MS")? {
340 dispatcher.poll_interval = v;
341 }
342 if let Some(v) = env_duration_ms(prefix, "IDLE_POLL_INTERVAL_MS")? {
343 dispatcher.idle_poll_interval = v;
344 }
345 if let Some(v) = env_duration_ms(prefix, "DRAIN_TIMEOUT_MS")? {
346 dispatcher.drain_timeout = v;
347 }
348 if let Some(v) = env_duration_ms(prefix, "STORE_TIMEOUT_MS")? {
349 dispatcher.store_timeout = v;
350 }
351 if let Some(v) = env_duration_ms(prefix, "STATS_INTERVAL_MS")? {
352 dispatcher.stats_interval = v;
353 }
354 if let Some(v) = env_ordering(prefix, "ORDERING")? {
355 dispatcher.ordering = v;
356 }
357 if let Some(v) = env_duration_ms(prefix, "RETRY_BASE_MS")? {
358 dispatcher.retry.base = v;
359 }
360 if let Some(v) = env_duration_ms(prefix, "RETRY_MAX_DELAY_MS")? {
361 dispatcher.retry.max_delay = v;
362 }
363 if let Some(v) = env_u32(prefix, "RETRY_MAX_ATTEMPTS")? {
364 dispatcher.retry.max_attempts = v;
365 }
366 if let Some(v) = env_jitter(prefix, "RETRY_JITTER")? {
367 dispatcher.retry.jitter = v;
368 }
369 if let Some(v) = env_worker_id(prefix, "WORKER_ID")? {
370 dispatcher.worker_id = Some(v);
371 }
372
373 if let Some(v) = env_duration_ms(prefix, "PUBLISHED_RETENTION_MS")? {
374 retention.published_retention = v;
375 }
376 if let Some(v) = env_duration_ms(prefix, "DEAD_RETENTION_MS")? {
377 retention.dead_retention = Some(v);
378 }
379 if let Some(v) = env_u32(prefix, "PURGE_BATCH_SIZE")? {
380 retention.purge_batch_size = v;
381 }
382
383 Ok(Self {
384 dispatcher,
385 retention,
386 })
387 }
388}
389
390#[derive(Clone, Debug, PartialEq)]
392#[non_exhaustive]
393pub enum SettingsError {
394 Parse {
397 key: String,
399 value_kind: &'static str,
401 },
402 OutOfRange {
404 key: String,
406 message: &'static str,
408 },
409}
410
411impl SettingsError {
417 #[must_use]
420 pub fn parse(key: impl Into<String>, value_kind: &'static str) -> Self {
421 Self::Parse {
422 key: key.into(),
423 value_kind,
424 }
425 }
426
427 #[must_use]
429 pub fn out_of_range(key: impl Into<String>, message: &'static str) -> Self {
430 Self::OutOfRange {
431 key: key.into(),
432 message,
433 }
434 }
435
436 #[must_use]
438 pub fn key(&self) -> &str {
439 match self {
440 Self::Parse { key, .. } | Self::OutOfRange { key, .. } => key,
441 }
442 }
443}
444
445impl fmt::Display for SettingsError {
446 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
447 match self {
448 Self::Parse { key, value_kind } => {
449 write!(f, "{key} could not be parsed as {value_kind}")
450 }
451 Self::OutOfRange { key, message } => {
452 write!(f, "{key} is out of range: {message}")
453 }
454 }
455 }
456}
457
458impl std::error::Error for SettingsError {}
459
460fn env_raw(prefix: &str, suffix: &str) -> Result<Option<String>, SettingsError> {
463 let key = format!("{prefix}{suffix}");
464 match std::env::var(&key) {
465 Ok(value) => Ok(Some(value)),
466 Err(VarError::NotPresent) => Ok(None),
467 Err(VarError::NotUnicode(_)) => Err(SettingsError::parse(key, "a UTF-8 string")),
468 }
469}
470
471fn env_u32(prefix: &str, suffix: &str) -> Result<Option<u32>, SettingsError> {
472 let Some(raw) = env_raw(prefix, suffix)? else {
473 return Ok(None);
474 };
475 raw.trim()
476 .parse::<u32>()
477 .map(Some)
478 .map_err(|_| SettingsError::parse(format!("{prefix}{suffix}"), "u32"))
479}
480
481fn env_usize(prefix: &str, suffix: &str) -> Result<Option<usize>, SettingsError> {
482 let Some(raw) = env_raw(prefix, suffix)? else {
483 return Ok(None);
484 };
485 raw.trim()
486 .parse::<usize>()
487 .map(Some)
488 .map_err(|_| SettingsError::parse(format!("{prefix}{suffix}"), "usize"))
489}
490
491fn env_duration_ms(prefix: &str, suffix: &str) -> Result<Option<Duration>, SettingsError> {
492 let Some(raw) = env_raw(prefix, suffix)? else {
493 return Ok(None);
494 };
495 let ms = raw
496 .trim()
497 .parse::<u64>()
498 .map_err(|_| SettingsError::parse(format!("{prefix}{suffix}"), "milliseconds"))?;
499 Ok(Some(Duration::from_millis(ms)))
500}
501
502fn env_jitter(prefix: &str, suffix: &str) -> Result<Option<f64>, SettingsError> {
503 let Some(raw) = env_raw(prefix, suffix)? else {
504 return Ok(None);
505 };
506 let key = format!("{prefix}{suffix}");
507 let value = raw
508 .trim()
509 .parse::<f64>()
510 .map_err(|_| SettingsError::parse(key.clone(), "f64"))?;
511 if !(0.0..1.0).contains(&value) {
512 return Err(SettingsError::out_of_range(
513 key,
514 "jitter must be in the range [0.0, 1.0)",
515 ));
516 }
517 Ok(Some(value))
518}
519
520fn env_ordering(prefix: &str, suffix: &str) -> Result<Option<Ordering>, SettingsError> {
521 let Some(raw) = env_raw(prefix, suffix)? else {
522 return Ok(None);
523 };
524 match raw.trim().to_ascii_lowercase().as_str() {
525 "unordered" => Ok(Some(Ordering::Unordered)),
526 "per_key" | "perkey" | "per-key" => Ok(Some(Ordering::PerKey)),
527 _ => Err(SettingsError::parse(
528 format!("{prefix}{suffix}"),
529 "ordering (\"unordered\" or \"per_key\")",
530 )),
531 }
532}
533
534fn env_worker_id(prefix: &str, suffix: &str) -> Result<Option<WorkerId>, SettingsError> {
535 let Some(raw) = env_raw(prefix, suffix)? else {
536 return Ok(None);
537 };
538 let key = format!("{prefix}{suffix}");
539 WorkerId::parse(raw).map(Some).map_err(|err| match err {
540 reliar_core::IdError::TooLong { .. } => {
541 SettingsError::out_of_range(key, "worker id exceeds the maximum length")
542 }
543 _ => SettingsError::parse(key, "worker id"),
544 })
545}