use crate::Operation;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum ExecutionStatus {
Pending,
Running,
Succeeded,
Failed,
RetryScheduled,
DeadLettered,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Execution {
pub execution_id: String,
pub trigger_id: String,
pub source_operation: Operation,
pub capability: String,
pub attempt: u32,
pub status: ExecutionStatus,
pub result: Option<Value>,
pub error: Option<String>,
pub created_ms: u128,
pub started_ms: Option<u128>,
pub completed_ms: Option<u128>,
pub claimed_by: Option<String>,
pub retry_policy: RetryPolicy,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RetryPolicy {
pub max_attempts: u32,
pub initial_backoff_ms: u64,
pub max_backoff_ms: u64,
pub backoff_multiplier: f64,
pub timeout_ms: u64,
}
impl Default for RetryPolicy {
fn default() -> Self {
Self {
max_attempts: 3,
initial_backoff_ms: 100,
max_backoff_ms: 30000,
backoff_multiplier: 2.0,
timeout_ms: 30000,
}
}
}
impl Execution {
pub fn new(
execution_id: String,
trigger_id: String,
source_operation: Operation,
capability: String,
) -> Self {
let now_ms = crate::now_ms();
Self {
execution_id,
trigger_id,
source_operation,
capability,
attempt: 1,
status: ExecutionStatus::Pending,
result: None,
error: None,
created_ms: now_ms,
started_ms: None,
completed_ms: None,
claimed_by: None,
retry_policy: RetryPolicy::default(),
}
}
pub fn mark_running(&mut self, claimed_by: String) {
self.status = ExecutionStatus::Running;
self.started_ms = Some(crate::now_ms());
self.claimed_by = Some(claimed_by);
}
pub fn mark_succeeded(&mut self, result: Value) {
self.status = ExecutionStatus::Succeeded;
self.result = Some(result);
self.completed_ms = Some(crate::now_ms());
}
pub fn mark_failed(&mut self, error: String) {
self.status = ExecutionStatus::Failed;
self.error = Some(error);
self.completed_ms = Some(crate::now_ms());
}
pub fn mark_retry_scheduled(&mut self) {
if self.attempt < self.retry_policy.max_attempts {
self.attempt += 1;
self.status = ExecutionStatus::RetryScheduled;
self.claimed_by = None;
} else {
self.status = ExecutionStatus::DeadLettered;
}
}
pub fn get_backoff_ms(&self) -> u64 {
let backoff = (self.retry_policy.initial_backoff_ms as f64
* self
.retry_policy
.backoff_multiplier
.powi((self.attempt - 1) as i32)) as u64;
backoff.min(self.retry_policy.max_backoff_ms)
}
pub fn can_retry(&self) -> bool {
matches!(
self.status,
ExecutionStatus::Failed | ExecutionStatus::RetryScheduled
) && self.attempt < self.retry_policy.max_attempts
}
pub fn is_terminal(&self) -> bool {
matches!(
self.status,
ExecutionStatus::Succeeded | ExecutionStatus::DeadLettered
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExecutionQueue {
#[serde(default)]
executions: HashMap<String, Execution>,
#[serde(default)]
pending_queue: Vec<String>,
#[serde(default, rename = "idempotency_map")]
idempotency_map_vec: Vec<((String, u64), String)>,
#[serde(skip)]
idempotency_map: HashMap<(String, u64), String>,
}
impl ExecutionQueue {
pub fn new() -> Self {
Self {
executions: HashMap::new(),
pending_queue: Vec::new(),
idempotency_map_vec: Vec::new(),
idempotency_map: HashMap::new(),
}
}
}
impl Default for ExecutionQueue {
fn default() -> Self {
Self::new()
}
}
impl ExecutionQueue {
fn sync_map_from_vec(&mut self) {
self.idempotency_map.clear();
for ((instance_id, sequence), exec_id) in &self.idempotency_map_vec {
self.idempotency_map
.insert((instance_id.clone(), *sequence), exec_id.clone());
}
}
fn sync_vec_from_map(&mut self) {
self.idempotency_map_vec = self
.idempotency_map
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
}
pub fn add_execution(&mut self, execution: Execution) -> Result<String, String> {
self.sync_map_from_vec();
let execution_id = execution.execution_id.clone();
let op_key = (
execution.source_operation.instance_id.clone(),
execution.source_operation.sequence,
);
if let Some(existing_id) = self.idempotency_map.get(&op_key) {
return Ok(existing_id.clone());
}
self.idempotency_map.insert(op_key, execution_id.clone());
self.executions.insert(execution_id.clone(), execution);
self.pending_queue.push(execution_id.clone());
self.sync_vec_from_map();
Ok(execution_id)
}
pub fn get_execution(&self, execution_id: &str) -> Option<Execution> {
self.executions.get(execution_id).cloned()
}
pub fn update_execution(
&mut self,
execution_id: &str,
execution: Execution,
) -> Result<(), String> {
if self.executions.contains_key(execution_id) {
self.executions.insert(execution_id.to_string(), execution);
Ok(())
} else {
Err(format!("Execution not found: {}", execution_id))
}
}
pub fn get_pending(&self) -> Vec<Execution> {
self.pending_queue
.iter()
.filter_map(|id| self.executions.get(id).cloned())
.filter(|e| matches!(e.status, ExecutionStatus::Pending))
.collect()
}
pub fn all(&self) -> Vec<Execution> {
self.executions.values().cloned().collect()
}
pub fn has_execution_for_operation(&self, instance_id: &str, sequence: u64) -> bool {
self.idempotency_map
.contains_key(&(instance_id.to_string(), sequence))
}
pub fn get_execution_for_operation(&self, instance_id: &str, sequence: u64) -> Option<String> {
self.idempotency_map
.get(&(instance_id.to_string(), sequence))
.cloned()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_execution_lifecycle() {
let mut exec = Execution::new(
"exec-1".to_string(),
"trigger-1".to_string(),
Operation::insert(
1,
"inst-a".to_string(),
1,
"orders:123".to_string(),
serde_json::json!({"total": 100}),
"Order".to_string(),
"orders".to_string(),
),
"process_order".to_string(),
);
assert_eq!(exec.status, ExecutionStatus::Pending);
assert_eq!(exec.attempt, 1);
exec.mark_running("inst-a".to_string());
assert_eq!(exec.status, ExecutionStatus::Running);
exec.mark_succeeded(serde_json::json!({"processed": true}));
assert_eq!(exec.status, ExecutionStatus::Succeeded);
assert!(exec.result.is_some());
assert!(exec.is_terminal());
}
#[test]
fn test_execution_idempotency() {
let mut queue = ExecutionQueue::new();
let op = Operation::insert(
1,
"inst-a".to_string(),
1,
"orders:123".to_string(),
serde_json::json!({"total": 100}),
"Order".to_string(),
"orders".to_string(),
);
let exec1 = Execution::new(
"exec-1".to_string(),
"trigger-1".to_string(),
op.clone(),
"process_order".to_string(),
);
let id1 = queue.add_execution(exec1).unwrap();
let exec2 = Execution::new(
"exec-2".to_string(),
"trigger-1".to_string(),
op.clone(),
"process_order".to_string(),
);
let id2 = queue.add_execution(exec2).unwrap();
assert_eq!(id1, id2);
assert_eq!(queue.all().len(), 1);
}
#[test]
fn test_retry_backoff() {
let mut exec = Execution::new(
"exec-1".to_string(),
"trigger-1".to_string(),
Operation::insert(
1,
"inst-a".to_string(),
1,
"orders:123".to_string(),
serde_json::json!({"total": 100}),
"Order".to_string(),
"orders".to_string(),
),
"process_order".to_string(),
);
assert_eq!(exec.get_backoff_ms(), 100);
exec.mark_failed("Test error".to_string());
exec.mark_retry_scheduled();
assert_eq!(exec.attempt, 2);
assert_eq!(exec.get_backoff_ms(), 200);
exec.mark_retry_scheduled();
assert_eq!(exec.attempt, 3);
assert_eq!(exec.get_backoff_ms(), 400);
}
}