use crate::types::{BudgetId, TimestampMs};
use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;
pub const CAPS_MAX_BYTES: usize = 4096;
pub const CAPS_MAX_TOKENS: usize = 256;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct RetryPolicy {
#[serde(default = "default_max_retries")]
pub max_retries: u32,
#[serde(default)]
pub backoff: BackoffStrategy,
#[serde(default)]
pub retryable_categories: Vec<String>,
}
fn default_max_retries() -> u32 {
3
}
impl Default for RetryPolicy {
fn default() -> Self {
Self {
max_retries: default_max_retries(),
backoff: BackoffStrategy::default(),
retryable_categories: Vec::new(),
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "type")]
pub enum BackoffStrategy {
Fixed { delay_ms: u64 },
Exponential {
initial_delay_ms: u64,
max_delay_ms: u64,
multiplier: f64,
#[serde(default)]
jitter: bool,
},
}
impl Default for BackoffStrategy {
fn default() -> Self {
Self::Exponential {
initial_delay_ms: 1000,
max_delay_ms: 60_000,
multiplier: 2.0,
jitter: false,
}
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct TimeoutPolicy {
#[serde(default)]
pub attempt_timeout_ms: Option<u64>,
#[serde(default)]
pub execution_deadline_ms: Option<u64>,
#[serde(default = "default_max_reclaim_count")]
pub max_reclaim_count: u32,
}
fn default_max_reclaim_count() -> u32 {
100
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct SuspensionPolicy {
#[serde(default)]
pub default_timeout_ms: Option<u64>,
#[serde(default = "default_timeout_behavior")]
pub timeout_behavior: String,
}
fn default_timeout_behavior() -> String {
"fail".to_owned()
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct FallbackPolicy {
pub tiers: Vec<FallbackTier>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct FallbackTier {
pub provider: String,
pub model: String,
#[serde(default)]
pub timeout_ms: Option<u64>,
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct RoutingRequirements {
#[serde(default)]
pub required_capabilities: BTreeSet<String>,
#[serde(default)]
pub preferred_locality: Option<String>,
#[serde(default)]
pub isolation_level: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct StreamPolicy {
#[serde(default = "default_durability_mode")]
pub durability_mode: String,
#[serde(default = "default_retention_maxlen")]
pub retention_maxlen: u64,
#[serde(default)]
pub retention_ttl_ms: Option<u64>,
}
fn default_durability_mode() -> String {
"buffered".to_owned()
}
fn default_retention_maxlen() -> u64 {
10_000
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ExecutionPolicy {
#[serde(default)]
pub priority: i32,
#[serde(default)]
pub delay_until: Option<TimestampMs>,
#[serde(default)]
pub retry_policy: Option<RetryPolicy>,
#[serde(default)]
pub timeout_policy: Option<TimeoutPolicy>,
#[serde(default = "default_max_reclaim_count")]
pub max_reclaim_count: u32,
#[serde(default)]
pub suspension_policy: Option<SuspensionPolicy>,
#[serde(default)]
pub fallback_policy: Option<FallbackPolicy>,
#[serde(default = "default_max_replay_count")]
pub max_replay_count: u32,
#[serde(default)]
pub budget_ids: Vec<BudgetId>,
#[serde(default)]
pub routing_requirements: Option<RoutingRequirements>,
#[serde(default)]
pub dedup_window_ms: Option<u64>,
#[serde(default)]
pub stream_policy: Option<StreamPolicy>,
#[serde(default = "default_max_signals")]
pub max_signals_per_execution: u32,
}
impl Default for ExecutionPolicy {
fn default() -> Self {
Self {
priority: 0,
delay_until: None,
retry_policy: None,
timeout_policy: None,
max_reclaim_count: default_max_reclaim_count(),
suspension_policy: None,
fallback_policy: None,
max_replay_count: default_max_replay_count(),
budget_ids: Vec::new(),
routing_requirements: None,
dedup_window_ms: None,
stream_policy: None,
max_signals_per_execution: default_max_signals(),
}
}
}
fn default_max_replay_count() -> u32 {
10
}
fn default_max_signals() -> u32 {
10_000
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn execution_policy_defaults() {
let policy = ExecutionPolicy::default();
assert_eq!(policy.priority, 0);
assert_eq!(policy.max_reclaim_count, 100);
assert_eq!(policy.max_replay_count, 10);
assert_eq!(policy.max_signals_per_execution, 10_000);
assert!(policy.retry_policy.is_none());
assert!(policy.timeout_policy.is_none());
}
#[test]
fn retry_policy_serde() {
let policy = RetryPolicy {
max_retries: 3,
backoff: BackoffStrategy::Exponential {
initial_delay_ms: 100,
max_delay_ms: 30_000,
multiplier: 2.0,
jitter: true,
},
retryable_categories: vec!["timeout".into(), "provider_error".into()],
};
let json = serde_json::to_string(&policy).unwrap();
let parsed: RetryPolicy = serde_json::from_str(&json).unwrap();
assert_eq!(policy, parsed);
}
#[test]
fn timeout_policy_defaults() {
let json = r#"{"attempt_timeout_ms": 30000}"#;
let policy: TimeoutPolicy = serde_json::from_str(json).unwrap();
assert_eq!(policy.attempt_timeout_ms, Some(30_000));
assert_eq!(policy.max_reclaim_count, 100);
}
#[test]
fn retry_policy_defaults() {
let policy = RetryPolicy::default();
assert_eq!(policy.max_retries, 3);
assert_eq!(
policy.backoff,
BackoffStrategy::Exponential {
initial_delay_ms: 1000,
max_delay_ms: 60_000,
multiplier: 2.0,
jitter: false,
}
);
assert!(policy.retryable_categories.is_empty());
}
#[test]
fn retry_policy_lua_compatible_json() {
let policy = RetryPolicy::default();
let json = serde_json::to_value(&policy).unwrap();
assert_eq!(json["max_retries"], 3);
let backoff = &json["backoff"];
assert_eq!(backoff["type"], "exponential");
assert_eq!(backoff["initial_delay_ms"], 1000);
assert_eq!(backoff["max_delay_ms"], 60_000);
assert_eq!(backoff["multiplier"], 2.0);
let fixed = RetryPolicy {
max_retries: 1,
backoff: BackoffStrategy::Fixed { delay_ms: 5000 },
retryable_categories: vec![],
};
let json = serde_json::to_value(&fixed).unwrap();
assert_eq!(json["backoff"]["type"], "fixed");
assert_eq!(json["backoff"]["delay_ms"], 5000);
}
#[test]
fn retry_policy_deserialize_minimal() {
let json = r#"{"max_retries": 5}"#;
let policy: RetryPolicy = serde_json::from_str(json).unwrap();
assert_eq!(policy.max_retries, 5);
assert_eq!(policy.backoff, BackoffStrategy::default());
}
#[test]
fn full_execution_policy_serde() {
let policy = ExecutionPolicy {
priority: 10,
retry_policy: Some(RetryPolicy {
max_retries: 5,
backoff: BackoffStrategy::Fixed { delay_ms: 1000 },
retryable_categories: vec![],
}),
..Default::default()
};
let json = serde_json::to_string(&policy).unwrap();
let parsed: ExecutionPolicy = serde_json::from_str(&json).unwrap();
assert_eq!(policy, parsed);
}
}