use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
use uuid::Uuid;
use crate::types::AgentConfig;
use super::heartbeat::HeartbeatContextMode;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct CronJobId(Uuid);
impl CronJobId {
pub fn new() -> Self {
Self(Uuid::new_v4())
}
pub fn from_uuid(uuid: Uuid) -> Self {
Self(uuid)
}
pub fn as_uuid(&self) -> &Uuid {
&self.0
}
}
impl Default for CronJobId {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for CronJobId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::str::FromStr for CronJobId {
type Err = uuid::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(Uuid::parse_str(s)?))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum AuditLevel {
#[default]
None,
ErrorsOnly,
AllOperations,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub enum CronJobStatus {
#[default]
Active,
Paused,
Completed,
Failed,
DeadLetter,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeliveryConfig {
pub channels: Vec<DeliveryChannel>,
#[serde(default)]
pub fail_fast: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum DeliveryChannel {
Stdout,
LogFile { path: String },
Webhook {
url: String,
#[serde(default = "default_webhook_method")]
method: String,
#[serde(default)]
headers: HashMap<String, String>,
#[serde(default = "default_retry_count")]
retry_count: u32,
#[serde(default = "default_webhook_timeout")]
timeout_secs: u64,
},
Slack {
webhook_url: String,
#[serde(default)]
channel: Option<String>,
},
Email {
smtp_host: String,
smtp_port: u16,
to: Vec<String>,
from: String,
#[serde(default)]
subject_template: Option<String>,
},
Custom {
handler_name: String,
#[serde(default)]
config: HashMap<String, String>,
},
}
fn default_webhook_method() -> String {
"POST".to_string()
}
fn default_retry_count() -> u32 {
3
}
fn default_webhook_timeout() -> u64 {
30
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeliveryReceipt {
pub channel_description: String,
pub delivered_at: DateTime<Utc>,
pub success: bool,
pub status_code: Option<u16>,
pub error: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CronJobDefinition {
pub job_id: CronJobId,
pub name: String,
pub cron_expression: String,
pub timezone: String,
pub agent_config: AgentConfig,
pub policy_ids: Vec<String>,
pub audit_level: AuditLevel,
pub status: CronJobStatus,
pub enabled: bool,
pub one_shot: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
pub last_run: Option<DateTime<Utc>>,
pub next_run: Option<DateTime<Utc>>,
pub run_count: u64,
pub failure_count: u64,
pub max_retries: u32,
pub max_concurrent: u32,
pub delivery_config: Option<DeliveryConfig>,
#[serde(default)]
pub jitter_max_secs: u32,
#[serde(default)]
pub session_mode: HeartbeatContextMode,
#[serde(default)]
pub agentpin_jwt: Option<String>,
}
impl CronJobDefinition {
pub fn new(
name: String,
cron_expression: String,
timezone: String,
agent_config: AgentConfig,
) -> Self {
let now = Utc::now();
Self {
job_id: CronJobId::new(),
name,
cron_expression,
timezone,
agent_config,
policy_ids: Vec::new(),
audit_level: AuditLevel::default(),
status: CronJobStatus::Active,
enabled: true,
one_shot: false,
created_at: now,
updated_at: now,
last_run: None,
next_run: None,
run_count: 0,
failure_count: 0,
max_retries: 3,
max_concurrent: 1,
delivery_config: None,
jitter_max_secs: 0,
session_mode: HeartbeatContextMode::default(),
agentpin_jwt: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JobRunRecord {
pub run_id: Uuid,
pub job_id: CronJobId,
pub agent_id: crate::types::AgentId,
pub started_at: DateTime<Utc>,
pub completed_at: Option<DateTime<Utc>>,
pub status: JobRunStatus,
pub error: Option<String>,
pub execution_time_ms: Option<u64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum JobRunStatus {
Running,
Succeeded,
Failed,
TimedOut,
Skipped,
}
impl fmt::Display for JobRunStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
JobRunStatus::Running => write!(f, "running"),
JobRunStatus::Succeeded => write!(f, "succeeded"),
JobRunStatus::Failed => write!(f, "failed"),
JobRunStatus::TimedOut => write!(f, "timed_out"),
JobRunStatus::Skipped => write!(f, "skipped"),
}
}
}
impl std::str::FromStr for JobRunStatus {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"running" => Ok(JobRunStatus::Running),
"succeeded" => Ok(JobRunStatus::Succeeded),
"failed" => Ok(JobRunStatus::Failed),
"timed_out" => Ok(JobRunStatus::TimedOut),
"skipped" => Ok(JobRunStatus::Skipped),
other => Err(format!("unknown job run status: {other}")),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cron_job_id_roundtrip() {
let id = CronJobId::new();
let s = id.to_string();
let parsed: CronJobId = s.parse().unwrap();
assert_eq!(id, parsed);
}
#[test]
fn cron_job_id_default() {
let a = CronJobId::default();
let b = CronJobId::default();
assert_ne!(a, b);
}
#[test]
fn audit_level_default_is_none() {
assert_eq!(AuditLevel::default(), AuditLevel::None);
}
#[test]
fn cron_job_status_default_is_active() {
assert_eq!(CronJobStatus::default(), CronJobStatus::Active);
}
#[test]
fn job_run_status_display_roundtrip() {
for status in [
JobRunStatus::Running,
JobRunStatus::Succeeded,
JobRunStatus::Failed,
JobRunStatus::TimedOut,
JobRunStatus::Skipped,
] {
let s = status.to_string();
let parsed: JobRunStatus = s.parse().unwrap();
assert_eq!(status, parsed);
}
}
#[test]
fn cron_job_definition_new_defaults() {
use crate::types::{
AgentConfig, AgentId, ExecutionMode, Priority, ResourceLimits, SecurityTier,
};
use std::collections::HashMap;
let config = AgentConfig {
id: AgentId::new(),
name: "test".to_string(),
dsl_source: "test".to_string(),
execution_mode: ExecutionMode::Ephemeral,
security_tier: SecurityTier::Tier1,
resource_limits: ResourceLimits::default(),
capabilities: vec![],
policies: vec![],
metadata: HashMap::new(),
priority: Priority::Normal,
};
let job = CronJobDefinition::new(
"test_job".to_string(),
"0 * * * *".to_string(),
"UTC".to_string(),
config,
);
assert!(job.enabled);
assert!(!job.one_shot);
assert_eq!(job.run_count, 0);
assert_eq!(job.max_retries, 3);
assert_eq!(job.max_concurrent, 1);
assert_eq!(job.status, CronJobStatus::Active);
}
#[test]
fn cron_job_definition_serialization() {
use crate::types::{
AgentConfig, AgentId, ExecutionMode, Priority, ResourceLimits, SecurityTier,
};
use std::collections::HashMap;
let config = AgentConfig {
id: AgentId::new(),
name: "test".to_string(),
dsl_source: "test".to_string(),
execution_mode: ExecutionMode::Ephemeral,
security_tier: SecurityTier::Tier1,
resource_limits: ResourceLimits::default(),
capabilities: vec![],
policies: vec![],
metadata: HashMap::new(),
priority: Priority::Normal,
};
let job = CronJobDefinition::new(
"ser_test".to_string(),
"*/5 * * * *".to_string(),
"US/Eastern".to_string(),
config,
);
let json = serde_json::to_string(&job).unwrap();
let deserialized: CronJobDefinition = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.name, "ser_test");
assert_eq!(deserialized.cron_expression, "*/5 * * * *");
assert_eq!(deserialized.timezone, "US/Eastern");
}
}