use std::time::Duration;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub struct RuntimeConfig {
pub thread_count: Option<usize>,
pub jit_threshold: Option<u32>,
pub signal_delivery: SignalDeliveryConfig,
pub completion_retry: CompletionRetryConfig,
pub outbox_enabled: bool,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct CompletionRetryConfig {
initial_backoff: Duration,
max_backoff: Duration,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
pub enum InvalidCompletionRetryLadder {
#[error(
"initial_backoff must be non-zero: the backoff ladder doubles from it, so a zero start \
can never climb and the unbounded retry becomes a hot loop against a failing store"
)]
ZeroInitialBackoff,
#[error(
"max_backoff ({max_backoff:?}) must be at least initial_backoff ({initial_backoff:?}): \
the ladder clamps to the ceiling as soon as doubling passes it, so a ceiling below the \
floor makes the interval ratchet DOWN — and a zero ceiling drives it to zero on the \
first advance, which is the same hot loop against a failing store that a zero \
initial_backoff would cause"
)]
CeilingBelowFloor {
initial_backoff: Duration,
max_backoff: Duration,
},
}
impl Default for CompletionRetryConfig {
fn default() -> Self {
Self {
initial_backoff: Duration::from_millis(1),
max_backoff: Duration::from_secs(30),
}
}
}
impl CompletionRetryConfig {
pub fn try_new(
initial_backoff: Duration,
max_backoff: Duration,
) -> Result<Self, InvalidCompletionRetryLadder> {
if initial_backoff.is_zero() {
return Err(InvalidCompletionRetryLadder::ZeroInitialBackoff);
}
if max_backoff < initial_backoff {
return Err(InvalidCompletionRetryLadder::CeilingBelowFloor {
initial_backoff,
max_backoff,
});
}
Ok(Self {
initial_backoff,
max_backoff,
})
}
#[must_use]
pub const fn initial_backoff(self) -> Duration {
self.initial_backoff
}
#[must_use]
pub const fn max_backoff(self) -> Duration {
self.max_backoff
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct SignalDeliveryConfig {
pub ready_timeout: Duration,
pub max_enqueue_attempts: u32,
pub initial_backoff: Duration,
pub max_backoff: Duration,
}
impl Default for SignalDeliveryConfig {
fn default() -> Self {
Self::new(
Duration::from_millis(50),
8,
Duration::from_millis(1),
Duration::from_millis(8),
)
}
}
impl SignalDeliveryConfig {
#[must_use]
pub const fn new(
ready_timeout: Duration,
max_enqueue_attempts: u32,
initial_backoff: Duration,
max_backoff: Duration,
) -> Self {
Self {
ready_timeout,
max_enqueue_attempts,
initial_backoff,
max_backoff,
}
}
pub(crate) fn cleanup_shutdown_timeout(self) -> Duration {
self.ready_timeout
.max(Self::default().ready_timeout)
.saturating_mul(self.max_enqueue_attempts.max(1).saturating_add(1))
}
}
impl RuntimeConfig {
#[must_use]
pub fn new(thread_count: Option<usize>) -> Self {
Self {
thread_count,
jit_threshold: None,
signal_delivery: SignalDeliveryConfig::default(),
completion_retry: CompletionRetryConfig::default(),
outbox_enabled: false,
}
}
#[must_use]
pub const fn with_jit_threshold(mut self, jit_threshold: Option<u32>) -> Self {
self.jit_threshold = jit_threshold;
self
}
#[must_use]
pub const fn with_signal_delivery(mut self, signal_delivery: SignalDeliveryConfig) -> Self {
self.signal_delivery = signal_delivery;
self
}
#[must_use]
pub const fn with_completion_retry(mut self, completion_retry: CompletionRetryConfig) -> Self {
self.completion_retry = completion_retry;
self
}
#[must_use]
pub const fn with_outbox_enabled(mut self, enabled: bool) -> Self {
self.outbox_enabled = enabled;
self
}
}
#[cfg(test)]
mod completion_retry_ladder_tests {
use super::{CompletionRetryConfig, InvalidCompletionRetryLadder};
use std::time::Duration;
#[test]
fn the_default_ladder_is_one_the_constructor_would_accept() {
let shipped = CompletionRetryConfig::default();
let through_the_gate =
CompletionRetryConfig::try_new(shipped.initial_backoff(), shipped.max_backoff());
assert_eq!(
through_the_gate,
Ok(shipped),
"the shipped default must be a ladder the constructor would have accepted, or \
`Default` is a second door into the type with different rules"
);
}
#[test]
fn a_zero_initial_backoff_cannot_be_named() -> Result<(), Box<dyn std::error::Error>> {
assert_eq!(
CompletionRetryConfig::try_new(Duration::ZERO, Duration::from_millis(8)),
Err(InvalidCompletionRetryLadder::ZeroInitialBackoff),
"a zero initial backoff must be refused by the constructor"
);
let accepted =
CompletionRetryConfig::try_new(Duration::from_millis(1), Duration::from_millis(8))?;
assert_eq!(accepted.initial_backoff(), Duration::from_millis(1));
assert_eq!(accepted.max_backoff(), Duration::from_millis(8));
Ok(())
}
#[test]
fn a_ceiling_below_the_floor_cannot_be_named() {
let floor = Duration::from_millis(8);
assert_eq!(
CompletionRetryConfig::try_new(floor, Duration::ZERO),
Err(InvalidCompletionRetryLadder::CeilingBelowFloor {
initial_backoff: floor,
max_backoff: Duration::ZERO,
}),
"a zero max_backoff must be refused — it drives the interval to zero on the first \
advance"
);
assert_eq!(
CompletionRetryConfig::try_new(floor, Duration::from_millis(4)),
Err(InvalidCompletionRetryLadder::CeilingBelowFloor {
initial_backoff: floor,
max_backoff: Duration::from_millis(4),
}),
"a max_backoff below initial_backoff must be refused — the interval would ratchet \
DOWN as the outage lengthens"
);
assert!(
CompletionRetryConfig::try_new(floor, floor).is_ok(),
"a ceiling equal to the floor is a fixed interval and a legitimate ladder — if this \
is refused the two refusals above are not attributable to the ceiling being LOW"
);
}
}