use chrono::Utc;
use std::sync::Arc;
use tokio_cron_scheduler::{Job, JobScheduler as TokioJobScheduler};
use tracing::{error, info};
use crate::server::database::Database;
mod grace_period;
mod license_expiration;
mod stale_devices;
pub use grace_period::run_grace_period_check;
pub use license_expiration::run_license_expiration_check;
pub use stale_devices::run_stale_device_cleanup;
#[derive(Debug, Clone)]
pub struct JobConfig {
pub grace_period_cron: String,
pub license_expiration_cron: String,
pub stale_device_cleanup_enabled: bool,
pub stale_device_cron: String,
pub stale_device_days: u32,
}
impl Default for JobConfig {
fn default() -> Self {
Self {
grace_period_cron: "0 0 * * * *".to_string(),
license_expiration_cron: "0 15 * * * *".to_string(),
stale_device_cleanup_enabled: false,
stale_device_cron: "0 0 3 * * *".to_string(),
stale_device_days: 90,
}
}
}
pub struct JobScheduler {
scheduler: TokioJobScheduler,
db: Arc<Database>,
config: JobConfig,
}
impl JobScheduler {
pub async fn new(db: Database, config: JobConfig) -> Result<Self, JobError> {
let scheduler = TokioJobScheduler::new()
.await
.map_err(|e| JobError::SchedulerError(e.to_string()))?;
Ok(Self {
scheduler,
db: Arc::new(db),
config,
})
}
pub async fn start(&self) -> Result<(), JobError> {
info!("Starting Talos job scheduler");
self.add_grace_period_job().await?;
self.add_license_expiration_job().await?;
if self.config.stale_device_cleanup_enabled {
self.add_stale_device_job().await?;
}
self.scheduler
.start()
.await
.map_err(|e| JobError::SchedulerError(e.to_string()))?;
info!("Talos job scheduler started successfully");
Ok(())
}
pub async fn shutdown(&mut self) -> Result<(), JobError> {
info!("Shutting down Talos job scheduler");
self.scheduler
.shutdown()
.await
.map_err(|e| JobError::SchedulerError(e.to_string()))?;
Ok(())
}
async fn add_grace_period_job(&self) -> Result<(), JobError> {
let db = Arc::clone(&self.db);
let job = Job::new_async(self.config.grace_period_cron.as_str(), move |_uuid, _l| {
let db = Arc::clone(&db);
Box::pin(async move {
let now = Utc::now().naive_utc();
info!("Running grace period expiration check at {}", now);
match run_grace_period_check(&db).await {
Ok(count) => {
if count > 0 {
info!("Grace period check: {} licenses revoked", count);
}
}
Err(e) => {
error!("Grace period check failed: {}", e);
}
}
})
})
.map_err(|e| JobError::SchedulerError(e.to_string()))?;
self.scheduler
.add(job)
.await
.map_err(|e| JobError::SchedulerError(e.to_string()))?;
info!(
"Added grace period expiration job (schedule: {})",
self.config.grace_period_cron
);
Ok(())
}
async fn add_license_expiration_job(&self) -> Result<(), JobError> {
let db = Arc::clone(&self.db);
let job = Job::new_async(
self.config.license_expiration_cron.as_str(),
move |_uuid, _l| {
let db = Arc::clone(&db);
Box::pin(async move {
let now = Utc::now().naive_utc();
info!("Running license expiration check at {}", now);
match run_license_expiration_check(&db).await {
Ok(count) => {
if count > 0 {
info!("License expiration check: {} licenses expired", count);
}
}
Err(e) => {
error!("License expiration check failed: {}", e);
}
}
})
},
)
.map_err(|e| JobError::SchedulerError(e.to_string()))?;
self.scheduler
.add(job)
.await
.map_err(|e| JobError::SchedulerError(e.to_string()))?;
info!(
"Added license expiration job (schedule: {})",
self.config.license_expiration_cron
);
Ok(())
}
async fn add_stale_device_job(&self) -> Result<(), JobError> {
let db = Arc::clone(&self.db);
let stale_days = self.config.stale_device_days;
let job = Job::new_async(self.config.stale_device_cron.as_str(), move |_uuid, _l| {
let db = Arc::clone(&db);
Box::pin(async move {
let now = Utc::now().naive_utc();
info!("Running stale device cleanup at {}", now);
match run_stale_device_cleanup(&db, stale_days).await {
Ok(count) => {
if count > 0 {
info!("Stale device cleanup: {} licenses released", count);
}
}
Err(e) => {
error!("Stale device cleanup failed: {}", e);
}
}
})
})
.map_err(|e| JobError::SchedulerError(e.to_string()))?;
self.scheduler
.add(job)
.await
.map_err(|e| JobError::SchedulerError(e.to_string()))?;
info!(
"Added stale device cleanup job (schedule: {}, threshold: {} days)",
self.config.stale_device_cron, self.config.stale_device_days
);
Ok(())
}
pub async fn run_grace_period_check_now(&self) -> Result<u32, JobError> {
run_grace_period_check(&self.db).await
}
pub async fn run_license_expiration_check_now(&self) -> Result<u32, JobError> {
run_license_expiration_check(&self.db).await
}
pub async fn run_stale_device_cleanup_now(&self) -> Result<u32, JobError> {
run_stale_device_cleanup(&self.db, self.config.stale_device_days).await
}
}
#[derive(Debug, thiserror::Error)]
pub enum JobError {
#[error("Scheduler error: {0}")]
SchedulerError(String),
#[error("Database error: {0}")]
DatabaseError(String),
#[error("Job execution error: {0}")]
ExecutionError(String),
}
impl From<crate::errors::LicenseError> for JobError {
fn from(err: crate::errors::LicenseError) -> Self {
JobError::DatabaseError(err.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_config_values() {
let config = JobConfig::default();
assert_eq!(config.grace_period_cron, "0 0 * * * *");
assert_eq!(config.license_expiration_cron, "0 15 * * * *");
assert!(!config.stale_device_cleanup_enabled);
assert_eq!(config.stale_device_days, 90);
}
}