use crate::SchedulerError;
use chrono::{Datelike, Timelike};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::sync::Mutex;
use std::time::{Duration, Instant};
#[derive(Debug, Clone)]
pub struct EnhancedCronExpr {
pub second: String,
pub minute: String,
pub hour: String,
pub day_of_month: String,
pub month: String,
pub day_of_week: String,
pub year: Option<String>,
}
impl EnhancedCronExpr {
pub fn parse(expr: &str) -> Result<Self, SchedulerError> {
let parts: Vec<&str> = expr.split_whitespace().collect();
match parts.len() {
5 => Ok(Self {
second: parts[0].to_string(),
minute: parts[1].to_string(),
hour: parts[2].to_string(),
day_of_month: parts[3].to_string(),
month: parts[4].to_string(),
day_of_week: "*".to_string(),
year: None,
}),
6 => Ok(Self {
second: parts[0].to_string(),
minute: parts[1].to_string(),
hour: parts[2].to_string(),
day_of_month: parts[3].to_string(),
month: parts[4].to_string(),
day_of_week: "*".to_string(),
year: Some(parts[5].to_string()),
}),
7 => Ok(Self {
second: parts[0].to_string(),
minute: parts[1].to_string(),
hour: parts[2].to_string(),
day_of_month: parts[3].to_string(),
month: parts[4].to_string(),
day_of_week: parts[5].to_string(),
year: Some(parts[6].to_string()),
}),
n => Err(SchedulerError::InvalidCronExpr(format!(
"Expected 5-7 fields, got {}",
n
))),
}
}
pub fn matches(&self, dt: chrono::DateTime<chrono::Utc>) -> bool {
let naive = dt.naive_utc();
if !field_matches(&self.second, naive.second()) {
return false;
}
if !field_matches(&self.minute, naive.minute()) {
return false;
}
if !field_matches(&self.hour, naive.hour()) {
return false;
}
if !field_matches(&self.day_of_month, naive.day()) {
return false;
}
if !field_matches(&self.month, naive.month()) {
return false;
}
if !field_matches(&self.day_of_week, weekday_to_cron(dt.weekday())) {
return false;
}
if let Some(ref year_field) = self.year {
if !field_matches(year_field, naive.year() as u32) {
return false;
}
}
true
}
pub fn field_count(&self) -> usize {
if self.year.is_some() {
6
} else {
5
}
}
}
fn weekday_to_cron(wd: chrono::Weekday) -> u32 {
use chrono::Weekday::*;
match wd {
Sun => 0,
Mon => 1,
Tue => 2,
Wed => 3,
Thu => 4,
Fri => 5,
Sat => 6,
}
}
fn field_matches(field: &str, value: u32) -> bool {
if field == "*" {
return true;
}
if field.contains(',') {
return field
.split(',')
.any(|v| v.trim().parse::<u32>().is_ok_and(|n| n == value));
}
if field.contains('/') {
let parts: Vec<&str> = field.split('/').collect();
if parts.len() == 2 {
let step: u32 = parts[1].parse().unwrap_or(1);
if step == 0 {
return false;
}
let base_part = parts[0];
if base_part == "*" {
return value.is_multiple_of(step);
}
if base_part.contains('-') {
let range: Vec<&str> = base_part.split('-').collect();
if range.len() == 2 {
let start: u32 = range[0].trim().parse().unwrap_or(0);
let end: u32 = range[1].trim().parse().unwrap_or(0);
return value >= start && value <= end && (value - start).is_multiple_of(step);
}
}
let start: u32 = base_part.trim().parse().unwrap_or(0);
return value >= start && (value - start).is_multiple_of(step);
}
}
if field.contains('-') {
let parts: Vec<&str> = field.split('-').collect();
if parts.len() == 2 {
let start: u32 = parts[0].trim().parse().unwrap_or(0);
let end: u32 = parts[1].trim().parse().unwrap_or(0);
return value >= start && value <= end;
}
}
field.parse::<u32>().is_ok_and(|n| n == value)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DagTask {
pub id: String,
pub name: String,
pub dependencies: Vec<String>,
pub completed: bool,
pub running: bool,
pub failed: bool,
}
impl DagTask {
pub fn new(id: impl Into<String>, name: impl Into<String>) -> Self {
Self {
id: id.into(),
name: name.into(),
dependencies: Vec::new(),
completed: false,
running: false,
failed: false,
}
}
pub fn depends_on(mut self, dep_id: impl Into<String>) -> Self {
self.dependencies.push(dep_id.into());
self
}
}
pub struct TaskDag {
tasks: HashMap<String, DagTask>,
}
impl TaskDag {
pub fn new() -> Self {
Self {
tasks: HashMap::new(),
}
}
pub fn add_task(&mut self, task: DagTask) -> Result<(), SchedulerError> {
if self.tasks.contains_key(&task.id) {
return Err(SchedulerError::Internal(format!(
"task '{}' already exists in DAG",
task.id
)));
}
for dep in &task.dependencies {
if !self.tasks.contains_key(dep) {
return Err(SchedulerError::Internal(format!(
"dependency '{}' not found for task '{}'",
dep, task.id
)));
}
}
self.tasks.insert(task.id.clone(), task);
Ok(())
}
pub fn add_dependency(&mut self, task_id: &str, dep_id: &str) -> Result<(), SchedulerError> {
if !self.tasks.contains_key(dep_id) {
return Err(SchedulerError::Internal(format!(
"dependency '{}' not found for task '{}'",
dep_id, task_id
)));
}
let task = self
.tasks
.get_mut(task_id)
.ok_or_else(|| SchedulerError::TaskNotFound(task_id.to_string()))?;
if !task.dependencies.contains(&dep_id.to_string()) {
task.dependencies.push(dep_id.to_string());
}
Ok(())
}
pub fn task_ids(&self) -> Vec<String> {
self.tasks.keys().cloned().collect()
}
pub fn task_count(&self) -> usize {
self.tasks.len()
}
pub fn get_task(&self, id: &str) -> Option<&DagTask> {
self.tasks.get(id)
}
pub fn get_task_mut(&mut self, id: &str) -> Option<&mut DagTask> {
self.tasks.get_mut(id)
}
pub fn has_cycle(&self) -> bool {
let mut visited = HashSet::new();
let mut recursion_stack = HashSet::new();
for task_id in self.tasks.keys() {
if self.dfs_cycle_detect(task_id, &mut visited, &mut recursion_stack) {
return true;
}
}
false
}
fn dfs_cycle_detect(
&self,
task_id: &str,
visited: &mut HashSet<String>,
recursion_stack: &mut HashSet<String>,
) -> bool {
if recursion_stack.contains(task_id) {
return true;
}
if visited.contains(task_id) {
return false;
}
visited.insert(task_id.to_string());
recursion_stack.insert(task_id.to_string());
if let Some(task) = self.tasks.get(task_id) {
for dep in &task.dependencies {
if self.dfs_cycle_detect(dep, visited, recursion_stack) {
return true;
}
}
}
recursion_stack.remove(task_id);
false
}
pub fn topological_sort(&self) -> Result<Vec<String>, SchedulerError> {
if self.has_cycle() {
return Err(SchedulerError::Internal(
"DAG contains a cycle, cannot topological sort".to_string(),
));
}
let mut result = Vec::new();
let mut visited = HashSet::new();
let mut all_ids: Vec<String> = self.tasks.keys().cloned().collect();
all_ids.sort();
for id in &all_ids {
self.topo_dfs(id, &mut visited, &mut result);
}
Ok(result)
}
fn topo_dfs(&self, id: &str, visited: &mut HashSet<String>, result: &mut Vec<String>) {
if visited.contains(id) {
return;
}
visited.insert(id.to_string());
if let Some(task) = self.tasks.get(id) {
for dep in &task.dependencies {
self.topo_dfs(dep, visited, result);
}
}
result.push(id.to_string());
}
pub fn ready_tasks(&self) -> Vec<String> {
self.tasks
.iter()
.filter(|(_, t)| !t.completed && !t.running && !t.failed)
.filter(|(_, t)| {
t.dependencies
.iter()
.all(|dep| self.tasks.get(dep).map(|d| d.completed).unwrap_or(false))
})
.map(|(id, _)| id.clone())
.collect()
}
pub fn mark_running(&mut self, task_id: &str) -> Result<(), SchedulerError> {
let task = self
.tasks
.get_mut(task_id)
.ok_or_else(|| SchedulerError::TaskNotFound(task_id.to_string()))?;
task.running = true;
Ok(())
}
pub fn mark_completed(&mut self, task_id: &str) -> Result<(), SchedulerError> {
let task = self
.tasks
.get_mut(task_id)
.ok_or_else(|| SchedulerError::TaskNotFound(task_id.to_string()))?;
task.running = false;
task.completed = true;
Ok(())
}
pub fn mark_failed(&mut self, task_id: &str) -> Result<(), SchedulerError> {
let task = self
.tasks
.get_mut(task_id)
.ok_or_else(|| SchedulerError::TaskNotFound(task_id.to_string()))?;
task.running = false;
task.failed = true;
Ok(())
}
pub fn reset_failed(&mut self, task_id: &str) -> Result<(), SchedulerError> {
let task = self
.tasks
.get_mut(task_id)
.ok_or_else(|| SchedulerError::TaskNotFound(task_id.to_string()))?;
task.failed = false;
task.running = false;
task.completed = false;
Ok(())
}
pub fn all_completed(&self) -> bool {
self.tasks.values().all(|t| t.completed)
}
pub fn completed_count(&self) -> usize {
self.tasks.values().filter(|t| t.completed).count()
}
pub fn failed_count(&self) -> usize {
self.tasks.values().filter(|t| t.failed).count()
}
pub fn dependents(&self, task_id: &str) -> Vec<String> {
self.tasks
.iter()
.filter(|(_, t)| t.dependencies.contains(&task_id.to_string()))
.map(|(id, _)| id.clone())
.collect()
}
}
impl Default for TaskDag {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum RetryPolicy {
Fixed {
max_retries: u32,
interval_ms: u64,
},
ExponentialBackoff {
max_retries: u32,
initial_interval_ms: u64,
multiplier: f64,
max_interval_ms: u64,
},
}
impl RetryPolicy {
pub fn fixed(max_retries: u32, interval: Duration) -> Self {
Self::Fixed {
max_retries,
interval_ms: interval.as_millis() as u64,
}
}
pub fn exponential(
max_retries: u32,
initial: Duration,
multiplier: f64,
max_interval: Duration,
) -> Self {
Self::ExponentialBackoff {
max_retries,
initial_interval_ms: initial.as_millis() as u64,
multiplier,
max_interval_ms: max_interval.as_millis() as u64,
}
}
pub fn max_retries(&self) -> u32 {
match self {
RetryPolicy::Fixed { max_retries, .. } => *max_retries,
RetryPolicy::ExponentialBackoff { max_retries, .. } => *max_retries,
}
}
pub fn retry_delay_ms(&self, attempt: u32) -> u64 {
match self {
RetryPolicy::Fixed { interval_ms, .. } => *interval_ms,
RetryPolicy::ExponentialBackoff {
initial_interval_ms,
multiplier,
max_interval_ms,
..
} => {
let delay = (*initial_interval_ms as f64) * multiplier.powi(attempt as i32);
delay.min(*max_interval_ms as f64) as u64
}
}
}
}
pub struct RetryExecutor {
policy: RetryPolicy,
history: Mutex<Vec<RetryRecord>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RetryRecord {
pub task_id: String,
pub attempt: u32,
pub success: bool,
pub error: Option<String>,
pub wait_ms: u64,
pub duration_ms: u64,
}
impl RetryExecutor {
pub fn new(policy: RetryPolicy) -> Self {
Self {
policy,
history: Mutex::new(Vec::new()),
}
}
pub fn execute<F>(&self, task_id: &str, mut task_fn: F) -> bool
where
F: FnMut() -> Result<(), String>,
{
let max_retries = self.policy.max_retries();
for attempt in 0..=max_retries {
let start = Instant::now();
let result = task_fn();
let duration_ms = start.elapsed().as_millis() as u64;
let success = result.is_ok();
let error = result.err();
let wait_ms = if success || attempt >= max_retries {
0
} else {
self.policy.retry_delay_ms(attempt)
};
if let Ok(mut history) = self.history.lock() {
history.push(RetryRecord {
task_id: task_id.to_string(),
attempt: attempt + 1,
success,
error: error.clone(),
wait_ms,
duration_ms,
});
}
if success {
return true;
}
if attempt < max_retries && wait_ms > 0 {
std::thread::sleep(Duration::from_millis(wait_ms));
}
}
false
}
pub fn history(&self) -> Vec<RetryRecord> {
self.history.lock().map(|h| h.clone()).unwrap_or_default()
}
pub fn attempt_count(&self, task_id: &str) -> usize {
self.history
.lock()
.map(|h| h.iter().filter(|r| r.task_id == task_id).count())
.unwrap_or(0)
}
pub fn is_successful(&self, task_id: &str) -> bool {
self.history
.lock()
.map(|h| h.iter().filter(|r| r.task_id == task_id).any(|r| r.success))
.unwrap_or(false)
}
pub fn clear_history(&self) {
if let Ok(mut h) = self.history.lock() {
h.clear();
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum LockStatus {
Acquired,
HeldByOther,
Expired,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct LockEntry {
owner: String,
acquired_at: u64,
expires_at: u64,
}
pub struct DistributedLockManager {
locks: Mutex<HashMap<String, LockEntry>>,
pub default_ttl_secs: u64,
}
impl DistributedLockManager {
pub fn new(default_ttl_secs: u64) -> Self {
Self {
locks: Mutex::new(HashMap::new()),
default_ttl_secs,
}
}
pub fn try_acquire(&self, task_id: &str, owner: &str) -> LockStatus {
self.try_acquire_with_ttl(task_id, owner, self.default_ttl_secs)
}
pub fn try_acquire_with_ttl(&self, task_id: &str, owner: &str, ttl_secs: u64) -> LockStatus {
let now = current_timestamp_secs();
let mut locks = match self.locks.lock() {
Ok(l) => l,
Err(_) => return LockStatus::HeldByOther,
};
if let Some(entry) = locks.get(task_id) {
if now >= entry.expires_at {
let entry = LockEntry {
owner: owner.to_string(),
acquired_at: now,
expires_at: now + ttl_secs,
};
locks.insert(task_id.to_string(), entry);
return LockStatus::Acquired;
}
if entry.owner == owner {
let entry = LockEntry {
owner: owner.to_string(),
acquired_at: entry.acquired_at,
expires_at: now + ttl_secs,
};
locks.insert(task_id.to_string(), entry);
return LockStatus::Acquired;
}
return LockStatus::HeldByOther;
}
let entry = LockEntry {
owner: owner.to_string(),
acquired_at: now,
expires_at: now + ttl_secs,
};
locks.insert(task_id.to_string(), entry);
LockStatus::Acquired
}
pub fn release(&self, task_id: &str, owner: &str) -> bool {
let mut locks = match self.locks.lock() {
Ok(l) => l,
Err(_) => return false,
};
if let Some(entry) = locks.get(task_id) {
if entry.owner == owner {
locks.remove(task_id);
return true;
}
}
false
}
pub fn renew(&self, task_id: &str, owner: &str, ttl_secs: u64) -> bool {
let now = current_timestamp_secs();
let mut locks = match self.locks.lock() {
Ok(l) => l,
Err(_) => return false,
};
if let Some(entry) = locks.get_mut(task_id) {
if entry.owner == owner {
entry.expires_at = now + ttl_secs;
return true;
}
}
false
}
pub fn is_locked(&self, task_id: &str) -> bool {
let now = current_timestamp_secs();
let locks = match self.locks.lock() {
Ok(l) => l,
Err(_) => return false,
};
locks
.get(task_id)
.map(|e| now < e.expires_at)
.unwrap_or(false)
}
pub fn lock_owner(&self, task_id: &str) -> Option<String> {
let now = current_timestamp_secs();
let locks = self.locks.lock().ok()?;
locks.get(task_id).and_then(|e| {
if now < e.expires_at {
Some(e.owner.clone())
} else {
None
}
})
}
pub fn cleanup_expired(&self) -> usize {
let now = current_timestamp_secs();
let mut locks = match self.locks.lock() {
Ok(l) => l,
Err(_) => return 0,
};
let before = locks.len();
locks.retain(|_, e| now < e.expires_at);
before - locks.len()
}
pub fn active_lock_count(&self) -> usize {
let now = current_timestamp_secs();
let locks = match self.locks.lock() {
Ok(l) => l,
Err(_) => return 0,
};
locks.values().filter(|e| now < e.expires_at).count()
}
}
impl Default for DistributedLockManager {
fn default() -> Self {
Self::new(300) }
}
fn current_timestamp_secs() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct TaskStats {
pub total_executions: u64,
pub successful_executions: u64,
pub failed_executions: u64,
pub total_duration_ms: u64,
pub last_execution_at: Option<u64>,
pub last_success: Option<bool>,
}
impl TaskStats {
pub fn new() -> Self {
Self::default()
}
pub fn record(&mut self, success: bool, duration_ms: u64) {
self.total_executions += 1;
if success {
self.successful_executions += 1;
} else {
self.failed_executions += 1;
}
self.total_duration_ms += duration_ms;
self.last_execution_at = Some(current_timestamp_secs());
self.last_success = Some(success);
}
pub fn success_rate(&self) -> f64 {
if self.total_executions == 0 {
return 0.0;
}
(self.successful_executions as f64 / self.total_executions as f64) * 100.0
}
pub fn avg_duration_ms(&self) -> f64 {
if self.total_executions == 0 {
return 0.0;
}
self.total_duration_ms as f64 / self.total_executions as f64
}
}
pub struct TaskStatsManager {
stats: Mutex<HashMap<String, TaskStats>>,
}
impl TaskStatsManager {
pub fn new() -> Self {
Self {
stats: Mutex::new(HashMap::new()),
}
}
pub fn record(&self, task_id: &str, success: bool, duration_ms: u64) {
if let Ok(mut stats) = self.stats.lock() {
stats
.entry(task_id.to_string())
.or_default()
.record(success, duration_ms);
}
}
pub fn get_stats(&self, task_id: &str) -> Option<TaskStats> {
self.stats.lock().ok().and_then(|s| s.get(task_id).cloned())
}
pub fn tracked_tasks(&self) -> Vec<String> {
self.stats
.lock()
.map(|s| s.keys().cloned().collect())
.unwrap_or_default()
}
pub fn reset(&self, task_id: &str) -> bool {
if let Ok(mut stats) = self.stats.lock() {
return stats.remove(task_id).is_some();
}
false
}
}
impl Default for TaskStatsManager {
fn default() -> Self {
Self::new()
}
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default,
)]
pub enum TaskPriority {
Low = 1,
#[default]
Normal = 5,
High = 10,
Urgent = 20,
}
impl TaskPriority {
pub fn from_value(v: u32) -> Self {
match v {
0..=4 => TaskPriority::Low,
5..=9 => TaskPriority::Normal,
10..=19 => TaskPriority::High,
_ => TaskPriority::Urgent,
}
}
pub fn as_str(&self) -> &'static str {
match self {
TaskPriority::Low => "low",
TaskPriority::Normal => "normal",
TaskPriority::High => "high",
TaskPriority::Urgent => "urgent",
}
}
pub fn value(&self) -> u32 {
*self as u32
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScheduleWindow {
pub start_hour: u32,
pub start_minute: u32,
pub end_hour: u32,
pub end_minute: u32,
pub allowed_weekdays: Vec<u32>,
}
impl ScheduleWindow {
pub fn new(start_hour: u32, end_hour: u32) -> Self {
Self {
start_hour,
start_minute: 0,
end_hour,
end_minute: 0,
allowed_weekdays: vec![1, 2, 3, 4, 5, 6, 7],
}
}
pub fn with_minutes(mut self, start_min: u32, end_min: u32) -> Self {
self.start_minute = start_min;
self.end_minute = end_min;
self
}
pub fn weekdays_only(mut self) -> Self {
self.allowed_weekdays = vec![1, 2, 3, 4, 5];
self
}
pub fn weekends_only(mut self) -> Self {
self.allowed_weekdays = vec![6, 7];
self
}
pub fn contains(&self, hour: u32, minute: u32, weekday: u32) -> bool {
if !self.allowed_weekdays.contains(&weekday) {
return false;
}
let start_mins = self.start_hour * 60 + self.start_minute;
let end_mins = self.end_hour * 60 + self.end_minute;
let check_mins = hour * 60 + minute;
if start_mins <= end_mins {
(start_mins..=end_mins).contains(&check_mins)
} else {
check_mins >= start_mins || check_mins <= end_mins
}
}
}
impl TaskStats {
pub fn failure_rate(&self) -> f64 {
if self.total_executions == 0 {
return 0.0;
}
(self.failed_executions as f64 / self.total_executions as f64) * 100.0
}
pub fn never_executed(&self) -> bool {
self.total_executions == 0
}
pub fn all_succeeded(&self) -> bool {
self.total_executions > 0 && self.failed_executions == 0
}
pub fn all_failed(&self) -> bool {
self.total_executions > 0 && self.successful_executions == 0
}
}
impl TaskStatsManager {
pub fn total_executions(&self) -> u64 {
self.stats
.lock()
.map(|s| s.values().map(|st| st.total_executions).sum())
.unwrap_or(0)
}
pub fn total_successes(&self) -> u64 {
self.stats
.lock()
.map(|s| s.values().map(|st| st.successful_executions).sum())
.unwrap_or(0)
}
pub fn reset_all(&self) {
if let Ok(mut stats) = self.stats.lock() {
stats.clear();
}
}
pub fn summary(&self) -> String {
let stats = match self.stats.lock() {
Ok(s) => s,
Err(_) => return "TaskStatsManager: lock poisoned".to_string(),
};
let mut out = format!("TaskStatsManager: {} task(s)\n", stats.len());
for (task_id, st) in stats.iter() {
out.push_str(&format!(
" {} : total={}, success={}, fail={}, rate={:.1}%, avg={}ms\n",
task_id,
st.total_executions,
st.successful_executions,
st.failed_executions,
st.success_rate(),
st.avg_duration_ms()
));
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_enhanced_cron_parse_5_fields() {
let expr = EnhancedCronExpr::parse("0 * * * *").unwrap();
assert_eq!(expr.second, "0");
assert_eq!(expr.minute, "*");
assert_eq!(expr.hour, "*");
assert_eq!(expr.day_of_month, "*");
assert_eq!(expr.month, "*");
assert!(expr.year.is_none());
assert_eq!(expr.field_count(), 5);
}
#[test]
fn test_enhanced_cron_parse_6_fields_with_year() {
let expr = EnhancedCronExpr::parse("0 0 * * * 2025").unwrap();
assert_eq!(expr.second, "0");
assert_eq!(expr.minute, "0");
assert_eq!(expr.year, Some("2025".to_string()));
assert_eq!(expr.field_count(), 6);
}
#[test]
fn test_enhanced_cron_parse_7_fields() {
let expr = EnhancedCronExpr::parse("0 0 12 * * 1 2025").unwrap();
assert_eq!(expr.second, "0");
assert_eq!(expr.day_of_week, "1");
assert_eq!(expr.year, Some("2025".to_string()));
}
#[test]
fn test_enhanced_cron_parse_invalid_field_count() {
assert!(EnhancedCronExpr::parse("0 * *").is_err());
assert!(EnhancedCronExpr::parse("0 * * * * * * *").is_err());
}
#[test]
fn test_enhanced_cron_matches_with_year() {
let expr = EnhancedCronExpr::parse("0 0 0 1 1 * 2025").unwrap();
let dt = chrono::DateTime::parse_from_rfc3339("2025-01-01T00:00:00Z")
.unwrap()
.with_timezone(&chrono::Utc);
assert!(expr.matches(dt));
}
#[test]
fn test_enhanced_cron_does_not_match_wrong_year() {
let expr = EnhancedCronExpr::parse("0 0 0 1 1 * 2025").unwrap();
let dt = chrono::DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
.unwrap()
.with_timezone(&chrono::Utc);
assert!(!expr.matches(dt));
}
#[test]
fn test_enhanced_cron_matches_without_year() {
let expr = EnhancedCronExpr::parse("* * * * *").unwrap();
let dt = chrono::Utc::now();
assert!(expr.matches(dt));
}
#[test]
fn test_enhanced_cron_day_of_week_match() {
let expr = EnhancedCronExpr::parse("0 0 0 * * 1 *").unwrap();
let dt = chrono::DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
.unwrap()
.with_timezone(&chrono::Utc);
assert!(expr.matches(dt));
}
#[test]
fn test_enhanced_cron_day_of_week_no_match() {
let expr = EnhancedCronExpr::parse("0 0 0 * * 1 *").unwrap();
let dt = chrono::DateTime::parse_from_rfc3339("2024-01-02T00:00:00Z")
.unwrap()
.with_timezone(&chrono::Utc);
assert!(!expr.matches(dt));
}
#[test]
fn test_field_matches_step_with_range() {
assert!(field_matches("1-10/3", 1));
assert!(field_matches("1-10/3", 4));
assert!(field_matches("1-10/3", 7));
assert!(field_matches("1-10/3", 10));
assert!(!field_matches("1-10/3", 2));
assert!(!field_matches("1-10/3", 5));
}
#[test]
fn test_dag_add_task() {
let mut dag = TaskDag::new();
dag.add_task(DagTask::new("a", "Task A")).unwrap();
dag.add_task(DagTask::new("b", "Task B")).unwrap();
assert_eq!(dag.task_count(), 2);
}
#[test]
fn test_dag_add_duplicate_task_fails() {
let mut dag = TaskDag::new();
dag.add_task(DagTask::new("a", "Task A")).unwrap();
let result = dag.add_task(DagTask::new("a", "Task A Duplicate"));
assert!(result.is_err());
}
#[test]
fn test_dag_add_task_with_missing_dependency_fails() {
let mut dag = TaskDag::new();
let result = dag.add_task(DagTask::new("a", "Task A").depends_on("nonexistent"));
assert!(result.is_err());
}
#[test]
fn test_dag_add_dependency_success() {
let mut dag = TaskDag::new();
dag.add_task(DagTask::new("a", "A")).unwrap();
dag.add_task(DagTask::new("b", "B")).unwrap();
assert!(dag.add_dependency("b", "a").is_ok());
let task_b = dag.get_task("b").unwrap();
assert!(task_b.dependencies.contains(&"a".to_string()));
}
#[test]
fn test_dag_add_dependency_task_not_found() {
let mut dag = TaskDag::new();
dag.add_task(DagTask::new("a", "A")).unwrap();
assert!(dag.add_dependency("nonexistent", "a").is_err());
}
#[test]
fn test_dag_add_dependency_dep_not_found() {
let mut dag = TaskDag::new();
dag.add_task(DagTask::new("a", "A")).unwrap();
assert!(dag.add_dependency("a", "nonexistent").is_err());
}
#[test]
fn test_dag_add_dependency_dedup() {
let mut dag = TaskDag::new();
dag.add_task(DagTask::new("a", "A")).unwrap();
dag.add_task(DagTask::new("b", "B").depends_on("a"))
.unwrap();
dag.add_dependency("b", "a").unwrap();
let task_b = dag.get_task("b").unwrap();
let count = task_b.dependencies.iter().filter(|d| *d == "a").count();
assert_eq!(count, 1);
}
#[test]
fn test_dag_no_cycle() {
let mut dag = TaskDag::new();
dag.add_task(DagTask::new("a", "A")).unwrap();
dag.add_task(DagTask::new("b", "B").depends_on("a"))
.unwrap();
dag.add_task(DagTask::new("c", "C").depends_on("b"))
.unwrap();
assert!(!dag.has_cycle());
}
#[test]
fn test_dag_detects_cycle() {
let mut dag = TaskDag::new();
dag.add_task(DagTask::new("a", "A")).unwrap();
dag.add_task(DagTask::new("b", "B").depends_on("a"))
.unwrap();
dag.add_task(DagTask::new("c", "C").depends_on("b"))
.unwrap();
dag.add_dependency("a", "c").unwrap();
assert!(dag.has_cycle());
}
#[test]
fn test_dag_topological_sort() {
let mut dag = TaskDag::new();
dag.add_task(DagTask::new("a", "A")).unwrap();
dag.add_task(DagTask::new("b", "B").depends_on("a"))
.unwrap();
dag.add_task(DagTask::new("c", "C").depends_on("a"))
.unwrap();
dag.add_task(DagTask::new("d", "D").depends_on("b").depends_on("c"))
.unwrap();
let order = dag.topological_sort().unwrap();
let pos_a = order.iter().position(|x| x == "a").unwrap();
let pos_b = order.iter().position(|x| x == "b").unwrap();
let pos_c = order.iter().position(|x| x == "c").unwrap();
let pos_d = order.iter().position(|x| x == "d").unwrap();
assert!(pos_a < pos_b);
assert!(pos_a < pos_c);
assert!(pos_b < pos_d);
assert!(pos_c < pos_d);
}
#[test]
fn test_dag_topological_sort_valid() {
let mut dag = TaskDag::new();
dag.add_task(DagTask::new("a", "A")).unwrap();
dag.add_task(DagTask::new("b", "B").depends_on("a"))
.unwrap();
dag.add_task(DagTask::new("c", "C").depends_on("a"))
.unwrap();
dag.add_task(DagTask::new("d", "D").depends_on("b"))
.unwrap();
let order = dag.topological_sort().unwrap();
let pos_a = order.iter().position(|x| x == "a").unwrap();
let pos_b = order.iter().position(|x| x == "b").unwrap();
let pos_c = order.iter().position(|x| x == "c").unwrap();
let pos_d = order.iter().position(|x| x == "d").unwrap();
assert!(pos_a < pos_b);
assert!(pos_a < pos_c);
assert!(pos_b < pos_d);
}
#[test]
fn test_dag_topological_sort_with_cycle_fails() {
let mut dag = TaskDag::new();
dag.add_task(DagTask::new("a", "A")).unwrap();
dag.add_task(DagTask::new("b", "B")).unwrap();
dag.add_dependency("a", "b").unwrap();
dag.add_dependency("b", "a").unwrap();
assert!(dag.topological_sort().is_err());
}
#[test]
fn test_dag_ready_tasks_initial() {
let mut dag = TaskDag::new();
dag.add_task(DagTask::new("a", "A")).unwrap();
dag.add_task(DagTask::new("b", "B").depends_on("a"))
.unwrap();
let ready = dag.ready_tasks();
assert_eq!(ready.len(), 1);
assert!(ready.contains(&"a".to_string()));
}
#[test]
fn test_dag_ready_tasks_after_completion() {
let mut dag = TaskDag::new();
dag.add_task(DagTask::new("a", "A")).unwrap();
dag.add_task(DagTask::new("b", "B").depends_on("a"))
.unwrap();
dag.add_task(DagTask::new("c", "C").depends_on("a"))
.unwrap();
dag.mark_completed("a").unwrap();
let ready = dag.ready_tasks();
assert_eq!(ready.len(), 2);
assert!(ready.contains(&"b".to_string()));
assert!(ready.contains(&"c".to_string()));
}
#[test]
fn test_dag_mark_running_and_completed() {
let mut dag = TaskDag::new();
dag.add_task(DagTask::new("a", "A")).unwrap();
dag.mark_running("a").unwrap();
assert!(dag.get_task("a").unwrap().running);
dag.mark_completed("a").unwrap();
assert!(!dag.get_task("a").unwrap().running);
assert!(dag.get_task("a").unwrap().completed);
}
#[test]
fn test_dag_mark_failed_and_reset() {
let mut dag = TaskDag::new();
dag.add_task(DagTask::new("a", "A")).unwrap();
dag.mark_running("a").unwrap();
dag.mark_failed("a").unwrap();
assert!(dag.get_task("a").unwrap().failed);
dag.reset_failed("a").unwrap();
assert!(!dag.get_task("a").unwrap().failed);
assert!(!dag.get_task("a").unwrap().completed);
}
#[test]
fn test_dag_all_completed() {
let mut dag = TaskDag::new();
dag.add_task(DagTask::new("a", "A")).unwrap();
dag.add_task(DagTask::new("b", "B").depends_on("a"))
.unwrap();
assert!(!dag.all_completed());
dag.mark_completed("a").unwrap();
assert!(!dag.all_completed());
dag.mark_completed("b").unwrap();
assert!(dag.all_completed());
}
#[test]
fn test_dag_dependents() {
let mut dag = TaskDag::new();
dag.add_task(DagTask::new("a", "A")).unwrap();
dag.add_task(DagTask::new("b", "B").depends_on("a"))
.unwrap();
dag.add_task(DagTask::new("c", "C").depends_on("a"))
.unwrap();
let deps = dag.dependents("a");
assert_eq!(deps.len(), 2);
assert!(deps.contains(&"b".to_string()));
assert!(deps.contains(&"c".to_string()));
}
#[test]
fn test_retry_policy_fixed() {
let policy = RetryPolicy::fixed(3, Duration::from_millis(100));
assert_eq!(policy.max_retries(), 3);
assert_eq!(policy.retry_delay_ms(0), 100);
assert_eq!(policy.retry_delay_ms(1), 100);
assert_eq!(policy.retry_delay_ms(2), 100);
}
#[test]
fn test_retry_policy_exponential() {
let policy = RetryPolicy::exponential(
3,
Duration::from_millis(100),
2.0,
Duration::from_millis(1000),
);
assert_eq!(policy.max_retries(), 3);
assert_eq!(policy.retry_delay_ms(0), 100); assert_eq!(policy.retry_delay_ms(1), 200); assert_eq!(policy.retry_delay_ms(2), 400); }
#[test]
fn test_retry_policy_exponential_capped() {
let policy = RetryPolicy::exponential(
5,
Duration::from_millis(100),
2.0,
Duration::from_millis(500),
);
assert_eq!(policy.retry_delay_ms(3), 500);
}
#[test]
fn test_retry_executor_success_first_try() {
let executor = RetryExecutor::new(RetryPolicy::fixed(3, Duration::from_millis(1)));
let result = executor.execute("task1", || Ok(()));
assert!(result);
assert_eq!(executor.attempt_count("task1"), 1);
assert!(executor.is_successful("task1"));
}
#[test]
fn test_retry_executor_success_after_retries() {
let executor = RetryExecutor::new(RetryPolicy::fixed(3, Duration::from_millis(1)));
let attempts = std::sync::Arc::new(std::sync::atomic::AtomicU32::new(0));
let attempts_clone = attempts.clone();
let result = executor.execute("task1", || {
let count = attempts_clone.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
if count < 2 {
Err("not yet".to_string())
} else {
Ok(())
}
});
assert!(result);
assert_eq!(executor.attempt_count("task1"), 3);
}
#[test]
fn test_retry_executor_all_fail() {
let executor = RetryExecutor::new(RetryPolicy::fixed(2, Duration::from_millis(1)));
let result = executor.execute("task1", || Err("always fail".to_string()));
assert!(!result);
assert_eq!(executor.attempt_count("task1"), 3); assert!(!executor.is_successful("task1"));
}
#[test]
fn test_retry_executor_history() {
let executor = RetryExecutor::new(RetryPolicy::fixed(2, Duration::from_millis(1)));
executor.execute("task1", || Ok(()));
executor.execute("task2", || Err("fail".to_string()));
let history = executor.history();
assert_eq!(history.len(), 4); }
#[test]
fn test_retry_executor_clear_history() {
let executor = RetryExecutor::new(RetryPolicy::fixed(0, Duration::from_millis(1)));
executor.execute("task1", || Ok(()));
assert!(!executor.history().is_empty());
executor.clear_history();
assert!(executor.history().is_empty());
}
#[test]
fn test_lock_acquire_success() {
let mgr = DistributedLockManager::new(60);
let status = mgr.try_acquire("task1", "instance-1");
assert_eq!(status, LockStatus::Acquired);
assert!(mgr.is_locked("task1"));
}
#[test]
fn test_lock_acquire_held_by_other() {
let mgr = DistributedLockManager::new(60);
mgr.try_acquire("task1", "instance-1");
let status = mgr.try_acquire("task1", "instance-2");
assert_eq!(status, LockStatus::HeldByOther);
}
#[test]
fn test_lock_acquire_same_owner_renews() {
let mgr = DistributedLockManager::new(60);
mgr.try_acquire("task1", "instance-1");
let status = mgr.try_acquire("task1", "instance-1");
assert_eq!(status, LockStatus::Acquired);
}
#[test]
fn test_lock_release() {
let mgr = DistributedLockManager::new(60);
mgr.try_acquire("task1", "instance-1");
assert!(mgr.release("task1", "instance-1"));
assert!(!mgr.is_locked("task1"));
}
#[test]
fn test_lock_release_wrong_owner_fails() {
let mgr = DistributedLockManager::new(60);
mgr.try_acquire("task1", "instance-1");
assert!(!mgr.release("task1", "instance-2"));
assert!(mgr.is_locked("task1"));
}
#[test]
fn test_lock_renew() {
let mgr = DistributedLockManager::new(60);
mgr.try_acquire("task1", "instance-1");
assert!(mgr.renew("task1", "instance-1", 120));
}
#[test]
fn test_lock_renew_wrong_owner_fails() {
let mgr = DistributedLockManager::new(60);
mgr.try_acquire("task1", "instance-1");
assert!(!mgr.renew("task1", "instance-2", 120));
}
#[test]
fn test_lock_owner() {
let mgr = DistributedLockManager::new(60);
mgr.try_acquire("task1", "instance-1");
assert_eq!(mgr.lock_owner("task1"), Some("instance-1".to_string()));
assert_eq!(mgr.lock_owner("nonexistent"), None);
}
#[test]
fn test_lock_expired_can_be_reacquired() {
let mgr = DistributedLockManager::new(60);
mgr.try_acquire_with_ttl("task1", "instance-1", 0); let status = mgr.try_acquire("task1", "instance-2");
assert_eq!(status, LockStatus::Acquired);
}
#[test]
fn test_lock_cleanup_expired() {
let mgr = DistributedLockManager::new(60);
mgr.try_acquire_with_ttl("task1", "instance-1", 0);
mgr.try_acquire_with_ttl("task2", "instance-1", 0);
mgr.try_acquire("task3", "instance-1"); let cleaned = mgr.cleanup_expired();
assert_eq!(cleaned, 2);
assert_eq!(mgr.active_lock_count(), 1);
}
#[test]
fn test_lock_active_count() {
let mgr = DistributedLockManager::new(60);
assert_eq!(mgr.active_lock_count(), 0);
mgr.try_acquire("task1", "instance-1");
mgr.try_acquire("task2", "instance-1");
assert_eq!(mgr.active_lock_count(), 2);
}
#[test]
fn test_task_stats_record() {
let mut stats = TaskStats::new();
stats.record(true, 100);
stats.record(false, 200);
stats.record(true, 150);
assert_eq!(stats.total_executions, 3);
assert_eq!(stats.successful_executions, 2);
assert_eq!(stats.failed_executions, 1);
assert_eq!(stats.total_duration_ms, 450);
}
#[test]
fn test_task_stats_success_rate() {
let mut stats = TaskStats::new();
assert_eq!(stats.success_rate(), 0.0);
stats.record(true, 100);
stats.record(true, 100);
stats.record(false, 100);
assert!((stats.success_rate() - 66.66666666666667).abs() < 0.01);
}
#[test]
fn test_task_stats_avg_duration() {
let mut stats = TaskStats::new();
stats.record(true, 100);
stats.record(true, 200);
assert_eq!(stats.avg_duration_ms(), 150.0);
}
#[test]
fn test_task_stats_last_execution() {
let mut stats = TaskStats::new();
assert!(stats.last_execution_at.is_none());
assert!(stats.last_success.is_none());
stats.record(true, 100);
assert!(stats.last_execution_at.is_some());
assert_eq!(stats.last_success, Some(true));
}
#[test]
fn test_task_stats_manager() {
let mgr = TaskStatsManager::new();
mgr.record("task1", true, 100);
mgr.record("task1", false, 200);
mgr.record("task2", true, 50);
let stats1 = mgr.get_stats("task1").unwrap();
assert_eq!(stats1.total_executions, 2);
let stats2 = mgr.get_stats("task2").unwrap();
assert_eq!(stats2.total_executions, 1);
assert_eq!(mgr.tracked_tasks().len(), 2);
}
#[test]
fn test_task_stats_manager_reset() {
let mgr = TaskStatsManager::new();
mgr.record("task1", true, 100);
assert!(mgr.get_stats("task1").is_some());
assert!(mgr.reset("task1"));
assert!(mgr.get_stats("task1").is_none());
assert!(!mgr.reset("nonexistent"));
}
#[test]
fn test_task_priority_default() {
assert_eq!(TaskPriority::default(), TaskPriority::Normal);
}
#[test]
fn test_task_priority_from_value() {
assert_eq!(TaskPriority::from_value(0), TaskPriority::Low);
assert_eq!(TaskPriority::from_value(5), TaskPriority::Normal);
assert_eq!(TaskPriority::from_value(10), TaskPriority::High);
assert_eq!(TaskPriority::from_value(20), TaskPriority::Urgent);
}
#[test]
fn test_task_priority_as_str() {
assert_eq!(TaskPriority::Low.as_str(), "low");
assert_eq!(TaskPriority::Normal.as_str(), "normal");
assert_eq!(TaskPriority::High.as_str(), "high");
assert_eq!(TaskPriority::Urgent.as_str(), "urgent");
}
#[test]
fn test_task_priority_value() {
assert_eq!(TaskPriority::Low.value(), 1);
assert_eq!(TaskPriority::Normal.value(), 5);
assert_eq!(TaskPriority::High.value(), 10);
assert_eq!(TaskPriority::Urgent.value(), 20);
}
#[test]
fn test_task_priority_ordering() {
assert!(TaskPriority::Low < TaskPriority::Normal);
assert!(TaskPriority::Normal < TaskPriority::High);
assert!(TaskPriority::High < TaskPriority::Urgent);
}
#[test]
fn test_schedule_window_contains() {
let window = ScheduleWindow::new(9, 17);
assert!(window.contains(10, 0, 1));
assert!(!window.contains(8, 0, 1));
assert!(!window.contains(18, 0, 1));
}
#[test]
fn test_schedule_window_weekdays_only() {
let window = ScheduleWindow::new(9, 17).weekdays_only();
assert!(window.contains(10, 0, 1));
assert!(!window.contains(10, 0, 7));
}
#[test]
fn test_schedule_window_weekends_only() {
let window = ScheduleWindow::new(9, 17).weekends_only();
assert!(!window.contains(10, 0, 1));
assert!(window.contains(10, 0, 7));
}
#[test]
fn test_schedule_window_with_minutes() {
let window = ScheduleWindow::new(9, 17).with_minutes(30, 30);
assert!(window.contains(10, 0, 1));
assert!(!window.contains(9, 0, 1));
assert!(window.contains(9, 30, 1));
}
#[test]
fn test_task_stats_failure_rate() {
let mut stats = TaskStats::new();
assert_eq!(stats.failure_rate(), 0.0);
stats.record(true, 100);
stats.record(false, 100);
assert!((stats.failure_rate() - 50.0).abs() < 0.01);
}
#[test]
fn test_task_stats_never_executed() {
let stats = TaskStats::new();
assert!(stats.never_executed());
}
#[test]
fn test_task_stats_all_succeeded() {
let mut stats = TaskStats::new();
stats.record(true, 100);
stats.record(true, 200);
assert!(stats.all_succeeded());
assert!(!stats.all_failed());
}
#[test]
fn test_task_stats_all_failed() {
let mut stats = TaskStats::new();
stats.record(false, 100);
stats.record(false, 200);
assert!(stats.all_failed());
assert!(!stats.all_succeeded());
}
#[test]
fn test_task_stats_manager_total_executions() {
let mgr = TaskStatsManager::new();
mgr.record("t1", true, 100);
mgr.record("t1", false, 200);
mgr.record("t2", true, 50);
assert_eq!(mgr.total_executions(), 3);
assert_eq!(mgr.total_successes(), 2);
}
#[test]
fn test_task_stats_manager_reset_all() {
let mgr = TaskStatsManager::new();
mgr.record("t1", true, 100);
mgr.record("t2", true, 200);
mgr.reset_all();
assert_eq!(mgr.tracked_tasks().len(), 0);
}
#[test]
fn test_task_stats_manager_summary() {
let mgr = TaskStatsManager::new();
mgr.record("t1", true, 100);
let s = mgr.summary();
assert!(s.contains("t1"));
assert!(s.contains("100.0%"));
}
#[test]
fn test_schedule_window_cross_midnight() {
let window = ScheduleWindow::new(22, 6);
assert!(window.contains(23, 0, 1));
assert!(window.contains(2, 0, 1));
assert!(!window.contains(12, 0, 1));
}
}