From cee8d7ff997afbb5b305ad72924b96277c151d8e Mon Sep 17 00:00:00 2001 From: salmonik Date: Fri, 10 Jul 2026 21:04:21 +0200 Subject: make cron follow local time zone Signed-off-by: salmonik --- src/tasks/mod.rs | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/tasks/mod.rs b/src/tasks/mod.rs index 1519c12..4408857 100644 --- a/src/tasks/mod.rs +++ b/src/tasks/mod.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use chrono::{DateTime, Duration, Timelike, Utc}; +use chrono::{DateTime, Duration, Local, TimeZone, Timelike, Utc}; use cron::Schedule; use poise::serenity_prelude as serenity; use std::str::FromStr; @@ -45,7 +45,7 @@ pub async fn check_and_execute_jobs(ctx: &CronContext) { } }; - let now = Utc::now(); + let now = Local::now(); for job in jobs { let schedule = match parse_cron(&job.cron_expression) { @@ -63,13 +63,18 @@ pub async fn check_and_execute_jobs(ctx: &CronContext) { // - If last_fire <= now, the job is due; we record last_run = last_fire // (not `now`) so a long execution delay can't skip a legitimately // pending later fire. - let reference: DateTime = job.last_run.unwrap_or_else(|| { - // Step back a tick so the first scan after startup catches the fire - // scheduled for the minute we wake up on. - now - chrono::Duration::seconds(TICK_SECONDS as i64) - }); - - let mut due_fire: Option> = None; + // - Evaluate the cron expression in local time so "37 21 * * *" means + // 21:37 in the server's timezone, not UTC. + let reference: DateTime = job.last_run.map_or_else( + || { + // Step back a tick so the first scan after startup catches the fire + // scheduled for the minute we wake up on. + now - chrono::Duration::seconds(TICK_SECONDS as i64) + }, + |utc_last_run| Local.from_utc_datetime(&utc_last_run.naive_utc()), + ); + + let mut due_fire: Option> = None; for next_fire in schedule.after(&reference) { if next_fire > now { break; @@ -92,7 +97,9 @@ pub async fn check_and_execute_jobs(ctx: &CronContext) { // Record last_run so we never re-fire the same schedule instant on the // next tick. Kept outside the match so a failed/empty send still marks // the fire as consumed and doesn't retry on every tick. - if let Err(e) = database::update_cron_last_run(&ctx.pool, job.id, fire_time).await { + // Convert local fire_time to UTC for consistent DB storage. + let fire_time_utc = fire_time.with_timezone(&Utc); + if let Err(e) = database::update_cron_last_run(&ctx.pool, job.id, fire_time_utc).await { error!("Failed to update last_run for cron job {}: {e}", job.id); } } @@ -172,7 +179,7 @@ async fn execute_audio_job(ctx: &CronContext, job: &crate::database::CronJob) { pub async fn cron_scheduler_loop(ctx: CronContext) { // Align first tick to the start of the next minute, then run every TICK_SECONDS. - let now = Utc::now(); + let now = Local::now(); let next_minute = (now + Duration::minutes(1)) .with_second(0) .and_then(|t| t.with_nanosecond(0)) -- cgit v1.2.3