#![allow(dead_code)]
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum CheckpointPolicy {
Never,
AfterEveryTask,
Interval,
Explicit,
}
impl CheckpointPolicy {
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Self::Never => "Never",
Self::AfterEveryTask => "After Every Task",
Self::Interval => "Interval",
Self::Explicit => "Explicit",
}
}
#[must_use]
pub const fn all() -> &'static [CheckpointPolicy] {
&[
Self::Never,
Self::AfterEveryTask,
Self::Interval,
Self::Explicit,
]
}
}
impl std::fmt::Display for CheckpointPolicy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.label())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Checkpoint {
pub id: u64,
pub workflow_id: String,
pub timestamp_secs: u64,
pub completed_tasks: Vec<String>,
pub running_tasks: Vec<String>,
pub state_data: HashMap<String, String>,
pub label: Option<String>,
}
impl Checkpoint {
#[must_use]
pub fn new(id: u64, workflow_id: impl Into<String>) -> Self {
let ts = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or(Duration::ZERO)
.as_secs();
Self {
id,
workflow_id: workflow_id.into(),
timestamp_secs: ts,
completed_tasks: Vec::new(),
running_tasks: Vec::new(),
state_data: HashMap::new(),
label: None,
}
}
pub fn set_state(&mut self, key: impl Into<String>, value: impl Into<String>) {
self.state_data.insert(key.into(), value.into());
}
#[must_use]
pub fn get_state(&self, key: &str) -> Option<&String> {
self.state_data.get(key)
}
#[must_use]
pub fn is_task_completed(&self, task_id: &str) -> bool {
self.completed_tasks.iter().any(|t| t == task_id)
}
#[must_use]
pub fn task_count(&self) -> usize {
self.completed_tasks.len() + self.running_tasks.len()
}
}
#[derive(Debug, Clone)]
pub struct CheckpointManager {
policy: CheckpointPolicy,
interval_secs: u64,
checkpoints: Vec<Checkpoint>,
next_id: u64,
max_retained: usize,
}
impl Default for CheckpointManager {
fn default() -> Self {
Self {
policy: CheckpointPolicy::AfterEveryTask,
interval_secs: 60,
checkpoints: Vec::new(),
next_id: 1,
max_retained: 0,
}
}
}
impl CheckpointManager {
#[must_use]
pub fn new(policy: CheckpointPolicy) -> Self {
Self {
policy,
..Default::default()
}
}
#[must_use]
pub fn with_interval(interval_secs: u64) -> Self {
Self {
policy: CheckpointPolicy::Interval,
interval_secs,
..Default::default()
}
}
pub fn set_max_retained(&mut self, max: usize) {
self.max_retained = max;
}
#[must_use]
pub fn policy(&self) -> CheckpointPolicy {
self.policy
}
#[must_use]
pub fn count(&self) -> usize {
self.checkpoints.len()
}
pub fn create_checkpoint(&mut self, workflow_id: &str) -> &Checkpoint {
let id = self.next_id;
self.next_id += 1;
let cp = Checkpoint::new(id, workflow_id);
self.checkpoints.push(cp);
self.enforce_retention();
self.checkpoints
.last()
.expect("invariant: checkpoint just pushed above")
}
#[must_use]
pub fn latest(&self) -> Option<&Checkpoint> {
self.checkpoints.last()
}
#[must_use]
pub fn checkpoints_for(&self, workflow_id: &str) -> Vec<&Checkpoint> {
self.checkpoints
.iter()
.filter(|c| c.workflow_id == workflow_id)
.collect()
}
pub fn clear(&mut self) {
self.checkpoints.clear();
}
#[must_use]
pub fn interval_secs(&self) -> u64 {
self.interval_secs
}
fn enforce_retention(&mut self) {
if self.max_retained > 0 && self.checkpoints.len() > self.max_retained {
let excess = self.checkpoints.len() - self.max_retained;
self.checkpoints.drain(0..excess);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_policy_label() {
assert_eq!(CheckpointPolicy::Never.label(), "Never");
assert_eq!(CheckpointPolicy::AfterEveryTask.label(), "After Every Task");
}
#[test]
fn test_policy_display() {
assert_eq!(format!("{}", CheckpointPolicy::Interval), "Interval");
}
#[test]
fn test_policy_all() {
assert_eq!(CheckpointPolicy::all().len(), 4);
}
#[test]
fn test_checkpoint_new() {
let cp = Checkpoint::new(1, "wf-001");
assert_eq!(cp.id, 1);
assert_eq!(cp.workflow_id, "wf-001");
assert!(cp.completed_tasks.is_empty());
}
#[test]
fn test_checkpoint_state() {
let mut cp = Checkpoint::new(1, "wf-001");
let out = std::env::temp_dir()
.join("oximedia-workflow-cp-out.mp4")
.to_string_lossy()
.into_owned();
cp.set_state("output_path", &out);
assert_eq!(
cp.get_state("output_path").expect("should succeed in test"),
&out
);
assert!(cp.get_state("missing").is_none());
}
#[test]
fn test_checkpoint_task_completed() {
let mut cp = Checkpoint::new(1, "wf-001");
cp.completed_tasks.push("task-a".to_string());
assert!(cp.is_task_completed("task-a"));
assert!(!cp.is_task_completed("task-b"));
}
#[test]
fn test_checkpoint_task_count() {
let mut cp = Checkpoint::new(1, "wf-001");
cp.completed_tasks.push("a".to_string());
cp.running_tasks.push("b".to_string());
assert_eq!(cp.task_count(), 2);
}
#[test]
fn test_manager_default() {
let m = CheckpointManager::default();
assert_eq!(m.policy(), CheckpointPolicy::AfterEveryTask);
assert_eq!(m.count(), 0);
}
#[test]
fn test_manager_create_checkpoint() {
let mut m = CheckpointManager::new(CheckpointPolicy::AfterEveryTask);
m.create_checkpoint("wf-001");
assert_eq!(m.count(), 1);
assert_eq!(
m.latest().expect("should succeed in test").workflow_id,
"wf-001"
);
}
#[test]
fn test_manager_multiple_checkpoints() {
let mut m = CheckpointManager::new(CheckpointPolicy::AfterEveryTask);
m.create_checkpoint("wf-001");
m.create_checkpoint("wf-001");
m.create_checkpoint("wf-002");
assert_eq!(m.count(), 3);
assert_eq!(m.checkpoints_for("wf-001").len(), 2);
assert_eq!(m.checkpoints_for("wf-002").len(), 1);
}
#[test]
fn test_manager_retention_limit() {
let mut m = CheckpointManager::new(CheckpointPolicy::AfterEveryTask);
m.set_max_retained(2);
m.create_checkpoint("wf-001");
m.create_checkpoint("wf-001");
m.create_checkpoint("wf-001");
assert_eq!(m.count(), 2);
assert_eq!(m.latest().expect("should succeed in test").id, 3);
}
#[test]
fn test_manager_clear() {
let mut m = CheckpointManager::new(CheckpointPolicy::AfterEveryTask);
m.create_checkpoint("wf-001");
m.clear();
assert_eq!(m.count(), 0);
assert!(m.latest().is_none());
}
#[test]
fn test_manager_with_interval() {
let m = CheckpointManager::with_interval(120);
assert_eq!(m.policy(), CheckpointPolicy::Interval);
assert_eq!(m.interval_secs(), 120);
}
#[test]
fn test_manager_latest_none() {
let m = CheckpointManager::new(CheckpointPolicy::Never);
assert!(m.latest().is_none());
}
}