use core::fmt;
use std::env::VarError;
use std::time::Duration;
use crate::ordering::Ordering;
use crate::retry::ExponentialBackoff;
use crate::worker::WorkerId;
#[derive(Clone, Debug, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(default, deny_unknown_fields))]
#[non_exhaustive]
pub struct OutboxSettings {
pub dispatcher: DispatcherSettings,
pub retention: RetentionSettings,
}
impl OutboxSettings {
#[must_use]
pub fn dispatcher(mut self, dispatcher: DispatcherSettings) -> Self {
self.dispatcher = dispatcher;
self
}
#[must_use]
pub fn retention(mut self, retention: RetentionSettings) -> Self {
self.retention = retention;
self
}
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(default, deny_unknown_fields))]
#[non_exhaustive]
pub struct DispatcherSettings {
pub batch_size: u32,
#[cfg_attr(
feature = "serde",
serde(rename = "lease_ms", with = "crate::duration_serde::millis")
)]
pub lease: Duration,
pub max_in_flight: usize,
#[cfg_attr(
feature = "serde",
serde(rename = "publish_timeout_ms", with = "crate::duration_serde::millis")
)]
pub publish_timeout: Duration,
#[cfg_attr(
feature = "serde",
serde(rename = "poll_interval_ms", with = "crate::duration_serde::millis")
)]
pub poll_interval: Duration,
#[cfg_attr(
feature = "serde",
serde(
rename = "idle_poll_interval_ms",
with = "crate::duration_serde::millis"
)
)]
pub idle_poll_interval: Duration,
#[cfg_attr(
feature = "serde",
serde(rename = "drain_timeout_ms", with = "crate::duration_serde::millis")
)]
pub drain_timeout: Duration,
#[cfg_attr(
feature = "serde",
serde(rename = "store_timeout_ms", with = "crate::duration_serde::millis")
)]
pub store_timeout: Duration,
#[cfg_attr(
feature = "serde",
serde(rename = "stats_interval_ms", with = "crate::duration_serde::millis")
)]
pub stats_interval: Duration,
pub ordering: Ordering,
pub retry: ExponentialBackoff,
pub worker_id: Option<WorkerId>,
}
impl Default for DispatcherSettings {
fn default() -> Self {
Self {
batch_size: 100,
lease: Duration::from_secs(30),
max_in_flight: 16,
publish_timeout: Duration::from_secs(10),
poll_interval: Duration::from_millis(500),
idle_poll_interval: Duration::from_secs(5),
drain_timeout: Duration::from_secs(30),
store_timeout: Duration::from_secs(10),
stats_interval: Duration::from_secs(15),
ordering: Ordering::default(),
retry: ExponentialBackoff::default(),
worker_id: None,
}
}
}
impl DispatcherSettings {
#[must_use]
pub const fn batch_size(mut self, batch_size: u32) -> Self {
self.batch_size = batch_size;
self
}
#[must_use]
pub const fn lease(mut self, lease: Duration) -> Self {
self.lease = lease;
self
}
#[must_use]
pub const fn max_in_flight(mut self, max_in_flight: usize) -> Self {
self.max_in_flight = max_in_flight;
self
}
#[must_use]
pub const fn publish_timeout(mut self, publish_timeout: Duration) -> Self {
self.publish_timeout = publish_timeout;
self
}
#[must_use]
pub const fn poll_interval(mut self, poll_interval: Duration) -> Self {
self.poll_interval = poll_interval;
self
}
#[must_use]
pub const fn idle_poll_interval(mut self, idle_poll_interval: Duration) -> Self {
self.idle_poll_interval = idle_poll_interval;
self
}
#[must_use]
pub const fn drain_timeout(mut self, drain_timeout: Duration) -> Self {
self.drain_timeout = drain_timeout;
self
}
#[must_use]
pub const fn store_timeout(mut self, store_timeout: Duration) -> Self {
self.store_timeout = store_timeout;
self
}
#[must_use]
pub const fn stats_interval(mut self, stats_interval: Duration) -> Self {
self.stats_interval = stats_interval;
self
}
#[must_use]
pub const fn ordering(mut self, ordering: Ordering) -> Self {
self.ordering = ordering;
self
}
#[must_use]
pub const fn retry(mut self, retry: ExponentialBackoff) -> Self {
self.retry = retry;
self
}
#[must_use]
pub fn worker_id(mut self, worker_id: WorkerId) -> Self {
self.worker_id = Some(worker_id);
self
}
}
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(default, deny_unknown_fields))]
#[non_exhaustive]
pub struct RetentionSettings {
#[cfg_attr(
feature = "serde",
serde(
rename = "published_retention_ms",
with = "crate::duration_serde::millis"
)
)]
pub published_retention: Duration,
#[cfg_attr(
feature = "serde",
serde(
rename = "dead_retention_ms",
with = "crate::duration_serde::optional_millis"
)
)]
pub dead_retention: Option<Duration>,
pub purge_batch_size: u32,
}
impl Default for RetentionSettings {
fn default() -> Self {
Self {
published_retention: Duration::from_secs(7 * 24 * 60 * 60),
dead_retention: None,
purge_batch_size: 1_000,
}
}
}
impl RetentionSettings {
#[must_use]
pub const fn published_retention(mut self, retention: Duration) -> Self {
self.published_retention = retention;
self
}
#[must_use]
pub const fn dead_retention(mut self, retention: Option<Duration>) -> Self {
self.dead_retention = retention;
self
}
#[must_use]
pub const fn purge_batch_size(mut self, purge_batch_size: u32) -> Self {
self.purge_batch_size = purge_batch_size;
self
}
}
impl OutboxSettings {
pub fn from_env(prefix: &str) -> Result<Self, SettingsError> {
let mut dispatcher = DispatcherSettings::default();
let mut retention = RetentionSettings::default();
if let Some(v) = env_u32(prefix, "BATCH_SIZE")? {
dispatcher.batch_size = v;
}
if let Some(v) = env_duration_ms(prefix, "LEASE_MS")? {
dispatcher.lease = v;
}
if let Some(v) = env_usize(prefix, "MAX_IN_FLIGHT")? {
dispatcher.max_in_flight = v;
}
if let Some(v) = env_duration_ms(prefix, "PUBLISH_TIMEOUT_MS")? {
dispatcher.publish_timeout = v;
}
if let Some(v) = env_duration_ms(prefix, "POLL_INTERVAL_MS")? {
dispatcher.poll_interval = v;
}
if let Some(v) = env_duration_ms(prefix, "IDLE_POLL_INTERVAL_MS")? {
dispatcher.idle_poll_interval = v;
}
if let Some(v) = env_duration_ms(prefix, "DRAIN_TIMEOUT_MS")? {
dispatcher.drain_timeout = v;
}
if let Some(v) = env_duration_ms(prefix, "STORE_TIMEOUT_MS")? {
dispatcher.store_timeout = v;
}
if let Some(v) = env_duration_ms(prefix, "STATS_INTERVAL_MS")? {
dispatcher.stats_interval = v;
}
if let Some(v) = env_ordering(prefix, "ORDERING")? {
dispatcher.ordering = v;
}
if let Some(v) = env_duration_ms(prefix, "RETRY_BASE_MS")? {
dispatcher.retry.base = v;
}
if let Some(v) = env_duration_ms(prefix, "RETRY_MAX_DELAY_MS")? {
dispatcher.retry.max_delay = v;
}
if let Some(v) = env_u32(prefix, "RETRY_MAX_ATTEMPTS")? {
dispatcher.retry.max_attempts = v;
}
if let Some(v) = env_jitter(prefix, "RETRY_JITTER")? {
dispatcher.retry.jitter = v;
}
if let Some(v) = env_worker_id(prefix, "WORKER_ID")? {
dispatcher.worker_id = Some(v);
}
if let Some(v) = env_duration_ms(prefix, "PUBLISHED_RETENTION_MS")? {
retention.published_retention = v;
}
if let Some(v) = env_duration_ms(prefix, "DEAD_RETENTION_MS")? {
retention.dead_retention = Some(v);
}
if let Some(v) = env_u32(prefix, "PURGE_BATCH_SIZE")? {
retention.purge_batch_size = v;
}
Ok(Self {
dispatcher,
retention,
})
}
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub enum SettingsError {
Parse {
key: String,
value_kind: &'static str,
},
OutOfRange {
key: String,
message: &'static str,
},
}
impl SettingsError {
#[must_use]
pub fn parse(key: impl Into<String>, value_kind: &'static str) -> Self {
Self::Parse {
key: key.into(),
value_kind,
}
}
#[must_use]
pub fn out_of_range(key: impl Into<String>, message: &'static str) -> Self {
Self::OutOfRange {
key: key.into(),
message,
}
}
#[must_use]
pub fn key(&self) -> &str {
match self {
Self::Parse { key, .. } | Self::OutOfRange { key, .. } => key,
}
}
}
impl fmt::Display for SettingsError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Parse { key, value_kind } => {
write!(f, "{key} could not be parsed as {value_kind}")
}
Self::OutOfRange { key, message } => {
write!(f, "{key} is out of range: {message}")
}
}
}
}
impl std::error::Error for SettingsError {}
fn env_raw(prefix: &str, suffix: &str) -> Result<Option<String>, SettingsError> {
let key = format!("{prefix}{suffix}");
match std::env::var(&key) {
Ok(value) => Ok(Some(value)),
Err(VarError::NotPresent) => Ok(None),
Err(VarError::NotUnicode(_)) => Err(SettingsError::parse(key, "a UTF-8 string")),
}
}
fn env_u32(prefix: &str, suffix: &str) -> Result<Option<u32>, SettingsError> {
let Some(raw) = env_raw(prefix, suffix)? else {
return Ok(None);
};
raw.trim()
.parse::<u32>()
.map(Some)
.map_err(|_| SettingsError::parse(format!("{prefix}{suffix}"), "u32"))
}
fn env_usize(prefix: &str, suffix: &str) -> Result<Option<usize>, SettingsError> {
let Some(raw) = env_raw(prefix, suffix)? else {
return Ok(None);
};
raw.trim()
.parse::<usize>()
.map(Some)
.map_err(|_| SettingsError::parse(format!("{prefix}{suffix}"), "usize"))
}
fn env_duration_ms(prefix: &str, suffix: &str) -> Result<Option<Duration>, SettingsError> {
let Some(raw) = env_raw(prefix, suffix)? else {
return Ok(None);
};
let ms = raw
.trim()
.parse::<u64>()
.map_err(|_| SettingsError::parse(format!("{prefix}{suffix}"), "milliseconds"))?;
Ok(Some(Duration::from_millis(ms)))
}
fn env_jitter(prefix: &str, suffix: &str) -> Result<Option<f64>, SettingsError> {
let Some(raw) = env_raw(prefix, suffix)? else {
return Ok(None);
};
let key = format!("{prefix}{suffix}");
let value = raw
.trim()
.parse::<f64>()
.map_err(|_| SettingsError::parse(key.clone(), "f64"))?;
if !(0.0..1.0).contains(&value) {
return Err(SettingsError::out_of_range(
key,
"jitter must be in the range [0.0, 1.0)",
));
}
Ok(Some(value))
}
fn env_ordering(prefix: &str, suffix: &str) -> Result<Option<Ordering>, SettingsError> {
let Some(raw) = env_raw(prefix, suffix)? else {
return Ok(None);
};
match raw.trim().to_ascii_lowercase().as_str() {
"unordered" => Ok(Some(Ordering::Unordered)),
"per_key" | "perkey" | "per-key" => Ok(Some(Ordering::PerKey)),
_ => Err(SettingsError::parse(
format!("{prefix}{suffix}"),
"ordering (\"unordered\" or \"per_key\")",
)),
}
}
fn env_worker_id(prefix: &str, suffix: &str) -> Result<Option<WorkerId>, SettingsError> {
let Some(raw) = env_raw(prefix, suffix)? else {
return Ok(None);
};
let key = format!("{prefix}{suffix}");
WorkerId::parse(raw).map(Some).map_err(|err| match err {
reliar_core::IdError::TooLong { .. } => {
SettingsError::out_of_range(key, "worker id exceeds the maximum length")
}
_ => SettingsError::parse(key, "worker id"),
})
}