use std::marker::PhantomData;
use std::time::Duration;
use serde::Serialize;
use super::error::{RetryPolicyError, WorkerConfigError};
use super::validate::{validate_kind, validate_version};
pub const DEFAULT_MAX_PAYLOAD_BYTES: usize = 65_536;
#[derive(Debug, Clone, Copy)]
pub struct JobModel<J> {
kind: &'static str,
version: i16,
max_attempts: u32,
payload_limit: usize,
_job: PhantomData<J>,
}
impl<J> JobModel<J> {
pub const fn new(kind: &'static str, version: i16, max_attempts: u32) -> Self {
Self {
kind,
version,
max_attempts: if max_attempts < 1 { 1 } else { max_attempts },
payload_limit: DEFAULT_MAX_PAYLOAD_BYTES,
_job: PhantomData,
}
}
pub fn kind(&self) -> &'static str {
self.kind
}
pub fn version(&self) -> i16 {
self.version
}
pub fn max_attempts(&self) -> u32 {
self.max_attempts
}
pub fn max_payload_bytes(&self) -> usize {
self.payload_limit
}
}
impl<J: Serialize + serde::de::DeserializeOwned> JobModel<J> {
pub fn with_max_payload_bytes(mut self, bytes: usize) -> Self {
self.payload_limit = bytes;
self
}
pub(crate) fn validate_identity(&self) -> Result<(), String> {
validate_kind(self.kind)?;
validate_version(self.version)?;
Ok(())
}
}
#[derive(Debug, Clone, Copy)]
pub struct RetryPolicy {
base: Duration,
multiplier: f64,
cap: Duration,
jitter: bool,
}
impl Default for RetryPolicy {
fn default() -> Self {
Self::exponential(Duration::from_secs(1), 2.0, Duration::from_secs(3600))
}
}
impl RetryPolicy {
pub const fn exponential(base: Duration, multiplier: f64, cap: Duration) -> Self {
Self {
base,
multiplier,
cap,
jitter: false,
}
}
pub const fn fixed(delay: Duration) -> Self {
Self {
base: delay,
multiplier: 1.0,
cap: delay,
jitter: false,
}
}
pub fn jitter(self, enabled: bool) -> Self {
Self {
jitter: enabled,
..self
}
}
pub fn validate(&self) -> Result<(), RetryPolicyError> {
if self.multiplier.is_nan() || self.multiplier.is_infinite() {
return Err(RetryPolicyError::MultiplierNotFinite {
multiplier: self.multiplier,
});
}
if self.multiplier < 0.0 {
return Err(RetryPolicyError::MultiplierNegative {
multiplier: self.multiplier,
});
}
Ok(())
}
pub fn validated(self) -> Result<Self, RetryPolicyError> {
self.validate()?;
Ok(self)
}
pub fn delay_for(&self, attempts: u32) -> Duration {
if attempts == 0 {
return self.base;
}
let cap_secs = self.cap.as_secs_f64();
let exp = if attempts >= 40 {
40
} else {
attempts - 1
};
let raw = if self.multiplier == 0.0 {
0.0
} else if self.multiplier == 1.0 {
self.base.as_secs_f64()
} else {
let factor = self.multiplier.powi(i32::try_from(exp).unwrap_or(i32::MAX));
self.base.as_secs_f64() * factor
};
let clamped = if raw.is_nan() || raw < 0.0 {
0.0
} else if raw > cap_secs {
cap_secs
} else {
raw
};
let mut delay = Duration::from_secs_f64(clamped);
if self.jitter && !delay.is_zero() {
let cap_nanos = delay.as_nanos();
if cap_nanos > 0 {
let rand = uuid::Uuid::new_v4().as_u128() % (cap_nanos + 1);
delay = Duration::from_nanos(u64::try_from(rand).unwrap_or(0));
}
}
delay
}
pub fn base(&self) -> Duration {
self.base
}
pub fn cap(&self) -> Duration {
self.cap
}
pub fn jitter_enabled(&self) -> bool {
self.jitter
}
}
#[derive(Debug, Clone, Copy)]
pub struct WorkerConfig {
concurrency: usize,
poll_interval: Duration,
lease: Duration,
poll_batch: i64,
sweep_interval: Duration,
sweep_batch: i64,
job_timeout: Duration,
heartbeat_interval: Option<Duration>,
}
impl Default for WorkerConfig {
fn default() -> Self {
Self {
concurrency: 8,
poll_interval: Duration::from_millis(200),
lease: Duration::from_secs(300),
poll_batch: 8,
sweep_interval: Duration::from_secs(30),
sweep_batch: 64,
job_timeout: Duration::from_secs(60),
heartbeat_interval: None, }
}
}
impl WorkerConfig {
pub fn concurrency(mut self, n: usize) -> Self {
self.concurrency = if n < 1 { 1 } else { n };
self
}
pub fn poll_interval(mut self, d: Duration) -> Self {
self.poll_interval = if d < Duration::from_millis(1) {
Duration::from_millis(1)
} else {
d
};
self
}
pub fn lease(mut self, d: Duration) -> Self {
self.lease = if d < Duration::from_secs(1) {
Duration::from_secs(1)
} else {
d
};
self
}
pub fn poll_batch(mut self, n: i64) -> Self {
self.poll_batch = if n < 1 { 1 } else { n };
self
}
pub fn sweep_interval(mut self, d: Duration) -> Self {
self.sweep_interval = if d < Duration::from_millis(1) {
Duration::from_millis(1)
} else {
d
};
self
}
pub fn sweep_batch(mut self, n: i64) -> Self {
self.sweep_batch = if n < 1 { 1 } else { n };
self
}
pub fn job_timeout(mut self, d: Duration) -> Self {
self.job_timeout = if d < Duration::from_millis(1) {
Duration::from_millis(1)
} else {
d
};
self
}
pub fn heartbeat_interval(mut self, d: Duration) -> Self {
self.heartbeat_interval = Some(if d < Duration::from_millis(1) {
Duration::from_millis(1)
} else {
d
});
self
}
pub fn validate(&self) -> Result<(), WorkerConfigError> {
if self.job_timeout > self.lease {
return Err(WorkerConfigError::JobTimeoutExceedsLease {
job_timeout: self.job_timeout,
lease: self.lease,
});
}
let hb = self.effective_heartbeat_interval();
if hb >= self.lease {
return Err(WorkerConfigError::HeartbeatIntervalNotBelowLease {
heartbeat_interval: hb,
lease: self.lease,
});
}
Ok(())
}
pub fn validated(self) -> Result<Self, WorkerConfigError> {
self.validate()?;
Ok(self)
}
pub fn get_concurrency(&self) -> usize {
self.concurrency
}
pub fn get_poll_interval(&self) -> Duration {
self.poll_interval
}
pub fn get_lease(&self) -> Duration {
self.lease
}
pub fn get_poll_batch(&self) -> i64 {
self.poll_batch
}
pub fn get_sweep_interval(&self) -> Duration {
self.sweep_interval
}
pub fn get_sweep_batch(&self) -> i64 {
self.sweep_batch
}
pub fn get_job_timeout(&self) -> Duration {
self.job_timeout
}
pub fn get_heartbeat_interval(&self) -> Duration {
self.effective_heartbeat_interval()
}
fn effective_heartbeat_interval(&self) -> Duration {
match self.heartbeat_interval {
Some(v) => v,
None => {
let third = self.lease / 3;
if third < Duration::from_millis(1) {
Duration::from_millis(1)
} else {
third
}
}
}
}
}