use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum IdempotencyMode {
#[default]
Lwt,
None,
}
impl IdempotencyMode {
#[must_use]
pub fn parse(s: &str) -> Option<Self> {
match s.trim().to_ascii_lowercase().as_str() {
"lwt" => Some(Self::Lwt),
"none" => Some(Self::None),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
pub struct RetryPolicy {
pub max_attempts: u32,
pub base_delay_ms: u64,
pub backoff_multiplier: f64,
pub max_delay_ms: u64,
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
pub struct RateLimitPolicy {
pub max_in_flight: u32,
pub max_enqueue_per_second: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskConfig {
pub task_name: String,
pub priority: i32,
pub pool: String,
pub retry_policy: RetryPolicy,
pub rate_limit_policy: RateLimitPolicy,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub idempotency_mode: Option<IdempotencyMode>,
pub updated_at: DateTime<Utc>,
}
impl TaskConfig {
#[must_use]
pub fn default_for(task_name: &str) -> Self {
Self {
task_name: task_name.to_string(),
priority: 1,
pool: "global".to_string(),
retry_policy: RetryPolicy::default(),
rate_limit_policy: RateLimitPolicy::default(),
idempotency_mode: None,
updated_at: Utc::now(),
}
}
#[must_use]
pub fn resolved_idempotency_mode(&self, runtime_default: IdempotencyMode) -> IdempotencyMode {
self.idempotency_mode.unwrap_or(runtime_default)
}
#[must_use]
pub fn from_policy_defaults(
task_name: &str,
priority: i32,
pool: impl Into<String>,
retry_policy: RetryPolicy,
rate_limit_policy: RateLimitPolicy,
idempotency_mode: Option<IdempotencyMode>,
) -> Self {
Self {
task_name: task_name.to_string(),
priority,
pool: pool.into(),
retry_policy,
rate_limit_policy,
idempotency_mode,
updated_at: Utc::now(),
}
}
#[must_use]
pub const fn with_runtime_idempotency_fallback(
mut self,
runtime_default: IdempotencyMode,
) -> Self {
if self.idempotency_mode.is_none() {
self.idempotency_mode = Some(runtime_default);
}
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_policy_defaults_sets_fields() {
let config = TaskConfig::from_policy_defaults(
"notify",
2,
"urgent",
RetryPolicy {
max_attempts: 5,
base_delay_ms: 100,
backoff_multiplier: 2.0,
max_delay_ms: 1000,
},
RateLimitPolicy {
max_in_flight: 10,
max_enqueue_per_second: 3,
},
Some(IdempotencyMode::None),
);
assert_eq!(config.task_name, "notify");
assert_eq!(config.priority, 2);
assert_eq!(config.pool, "urgent");
assert_eq!(config.retry_policy.max_attempts, 5);
assert_eq!(config.rate_limit_policy.max_in_flight, 10);
assert_eq!(config.idempotency_mode, Some(IdempotencyMode::None));
}
#[test]
fn runtime_idempotency_fallback_fills_none_only() {
let filled = TaskConfig::default_for("t").with_runtime_idempotency_fallback(IdempotencyMode::Lwt);
assert_eq!(filled.idempotency_mode, Some(IdempotencyMode::Lwt));
let kept = TaskConfig::default_for("t");
let mut kept = kept;
kept.idempotency_mode = Some(IdempotencyMode::None);
let kept = kept.with_runtime_idempotency_fallback(IdempotencyMode::Lwt);
assert_eq!(kept.idempotency_mode, Some(IdempotencyMode::None));
}
}