docker-compose-updater/src/scheduler.rs
Andras Schmelczer 3f60b72c3b
Some checks failed
Build and Publish Docker Image / test (push) Failing after 5s
Build and Publish Docker Image / build-and-push (push) Has been skipped
Build and Publish Docker Image / security-scan (push) Has been skipped
Initial
2026-03-23 07:44:26 +00:00

164 lines
4.9 KiB
Rust

use crate::health::HealthHandle;
use crate::{compose::updater::ComposeUpdater, config::Config};
use anyhow::{anyhow, Context, Result};
use cron::Schedule;
use std::str::FromStr;
use std::time::Duration;
use tokio::time::{sleep, Instant};
use tracing::error;
use tracing::info;
pub struct Scheduler {
config: Config,
updater: ComposeUpdater,
schedule: Schedule,
health_handle: Option<HealthHandle>,
}
impl Scheduler {
pub fn new(config: Config, health_handle: Option<HealthHandle>) -> Result<Self> {
let schedule = Schedule::from_str(&config.schedule)
.map_err(|e| anyhow!("Invalid cron expression '{}': {}", config.schedule, e))?;
let updater = ComposeUpdater::new(config.clone());
Ok(Self {
config,
updater,
schedule,
health_handle,
})
}
pub async fn start(&self) -> Result<()> {
info!(
"Starting scheduler with cron expression: {}",
self.config.schedule
);
loop {
if let Err(err) = self.run_update().await.context("Failed to run update") {
error!("{:?}", err)
}
if let Some(next_run) = self.schedule.upcoming(chrono::Utc).take(1).next() {
let now = chrono::Utc::now();
let duration_until_next = next_run.signed_duration_since(now);
if duration_until_next.num_seconds() > 0 {
info!(
"Next update scheduled for: {}",
next_run.format("%Y-%m-%d %H:%M:%S UTC")
);
let sleep_duration =
Duration::from_secs(duration_until_next.num_seconds() as u64);
sleep(sleep_duration).await;
}
}
}
}
pub async fn run_once(&self) -> Result<()> {
info!("Running one-time update");
self.run_update().await
}
async fn run_update(&self) -> Result<()> {
let start_time = Instant::now();
info!("Starting Docker Compose update cycle");
match self.updater.update_all_compose_files().await {
Ok(updated_files) => {
let duration = start_time.elapsed();
if updated_files.is_empty() {
info!(
"Update cycle completed in {:?} - no files updated",
duration
);
} else {
info!(
"Update cycle completed in {:?} - {} {} files:",
duration,
if self.config.dry_run {
"would update"
} else {
"updated"
},
updated_files.len()
);
for file in &updated_files {
info!(" - {}", file);
}
}
// Report success to health monitor
if let Some(ref health) = self.health_handle {
health.report_update_success();
}
Ok(())
}
Err(e) => {
let error = e.context("Failed to update Docker Compose files");
// Report failure to health monitor
if let Some(ref health) = self.health_handle {
health.report_update_failure();
}
Err(error)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{Config, UpdateStrategy};
use std::collections::HashMap;
use std::path::PathBuf;
#[test]
fn test_scheduler_creation() {
let config = Config {
compose_paths: vec![PathBuf::from("./test")],
schedule: "0 0 2 * * *".to_string(),
registries: HashMap::new(),
update_strategy: UpdateStrategy::LatestPatchOfPreviousMinor,
ignore_images: vec![],
dry_run: true,
};
let scheduler = Scheduler::new(config, None).unwrap();
assert!(scheduler
.schedule
.upcoming(chrono::Utc)
.take(1)
.next()
.is_some());
}
#[test]
fn test_cron_parsing() {
let config = Config {
compose_paths: vec![PathBuf::from("./test")],
schedule: "0 30 1 * * *".to_string(), // 1:30 AM daily
registries: HashMap::new(),
update_strategy: UpdateStrategy::LatestPatchOfPreviousMinor,
ignore_images: vec![],
dry_run: true,
};
let scheduler = Scheduler::new(config, None).unwrap();
// Just verify the scheduler can be created
assert!(scheduler
.schedule
.upcoming(chrono::Utc)
.take(1)
.next()
.is_some());
}
}