diff options
| author | salmonik <raisersoft@proton.me> | 2026-07-10 21:04:21 +0200 |
|---|---|---|
| committer | salmonik <raisersoft@proton.me> | 2026-07-10 21:04:21 +0200 |
| commit | cee8d7ff997afbb5b305ad72924b96277c151d8e (patch) | |
| tree | 41af9ca361b91e5e3ca13159e5c0737e79fabf61 /src | |
| parent | fix cron double fire (diff) | |
| download | senator-sperma-cee8d7ff997afbb5b305ad72924b96277c151d8e.tar.gz senator-sperma-cee8d7ff997afbb5b305ad72924b96277c151d8e.zip | |
make cron follow local time zone
Signed-off-by: salmonik <raisersoft@proton.me>
Diffstat (limited to '')
| -rw-r--r-- | src/tasks/mod.rs | 29 |
1 files 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 @@ | |||
| 1 | use std::sync::Arc; | 1 | use std::sync::Arc; |
| 2 | 2 | ||
| 3 | use chrono::{DateTime, Duration, Timelike, Utc}; | 3 | use chrono::{DateTime, Duration, Local, TimeZone, Timelike, Utc}; |
| 4 | use cron::Schedule; | 4 | use cron::Schedule; |
| 5 | use poise::serenity_prelude as serenity; | 5 | use poise::serenity_prelude as serenity; |
| 6 | use std::str::FromStr; | 6 | use std::str::FromStr; |
| @@ -45,7 +45,7 @@ pub async fn check_and_execute_jobs(ctx: &CronContext) { | |||
| 45 | } | 45 | } |
| 46 | }; | 46 | }; |
| 47 | 47 | ||
| 48 | let now = Utc::now(); | 48 | let now = Local::now(); |
| 49 | 49 | ||
| 50 | for job in jobs { | 50 | for job in jobs { |
| 51 | let schedule = match parse_cron(&job.cron_expression) { | 51 | let schedule = match parse_cron(&job.cron_expression) { |
| @@ -63,13 +63,18 @@ pub async fn check_and_execute_jobs(ctx: &CronContext) { | |||
| 63 | // - If last_fire <= now, the job is due; we record last_run = last_fire | 63 | // - If last_fire <= now, the job is due; we record last_run = last_fire |
| 64 | // (not `now`) so a long execution delay can't skip a legitimately | 64 | // (not `now`) so a long execution delay can't skip a legitimately |
| 65 | // pending later fire. | 65 | // pending later fire. |
| 66 | let reference: DateTime<Utc> = job.last_run.unwrap_or_else(|| { | 66 | // - Evaluate the cron expression in local time so "37 21 * * *" means |
| 67 | // Step back a tick so the first scan after startup catches the fire | 67 | // 21:37 in the server's timezone, not UTC. |
| 68 | // scheduled for the minute we wake up on. | 68 | let reference: DateTime<Local> = job.last_run.map_or_else( |
| 69 | now - chrono::Duration::seconds(TICK_SECONDS as i64) | 69 | || { |
| 70 | }); | 70 | // Step back a tick so the first scan after startup catches the fire |
| 71 | 71 | // scheduled for the minute we wake up on. | |
| 72 | let mut due_fire: Option<DateTime<Utc>> = None; | 72 | now - chrono::Duration::seconds(TICK_SECONDS as i64) |
| 73 | }, | ||
| 74 | |utc_last_run| Local.from_utc_datetime(&utc_last_run.naive_utc()), | ||
| 75 | ); | ||
| 76 | |||
| 77 | let mut due_fire: Option<DateTime<Local>> = None; | ||
| 73 | for next_fire in schedule.after(&reference) { | 78 | for next_fire in schedule.after(&reference) { |
| 74 | if next_fire > now { | 79 | if next_fire > now { |
| 75 | break; | 80 | break; |
| @@ -92,7 +97,9 @@ pub async fn check_and_execute_jobs(ctx: &CronContext) { | |||
| 92 | // Record last_run so we never re-fire the same schedule instant on the | 97 | // Record last_run so we never re-fire the same schedule instant on the |
| 93 | // next tick. Kept outside the match so a failed/empty send still marks | 98 | // next tick. Kept outside the match so a failed/empty send still marks |
| 94 | // the fire as consumed and doesn't retry on every tick. | 99 | // the fire as consumed and doesn't retry on every tick. |
| 95 | if let Err(e) = database::update_cron_last_run(&ctx.pool, job.id, fire_time).await { | 100 | // Convert local fire_time to UTC for consistent DB storage. |
| 101 | let fire_time_utc = fire_time.with_timezone(&Utc); | ||
| 102 | if let Err(e) = database::update_cron_last_run(&ctx.pool, job.id, fire_time_utc).await { | ||
| 96 | error!("Failed to update last_run for cron job {}: {e}", job.id); | 103 | error!("Failed to update last_run for cron job {}: {e}", job.id); |
| 97 | } | 104 | } |
| 98 | } | 105 | } |
| @@ -172,7 +179,7 @@ async fn execute_audio_job(ctx: &CronContext, job: &crate::database::CronJob) { | |||
| 172 | 179 | ||
| 173 | pub async fn cron_scheduler_loop(ctx: CronContext) { | 180 | pub async fn cron_scheduler_loop(ctx: CronContext) { |
| 174 | // Align first tick to the start of the next minute, then run every TICK_SECONDS. | 181 | // Align first tick to the start of the next minute, then run every TICK_SECONDS. |
| 175 | let now = Utc::now(); | 182 | let now = Local::now(); |
| 176 | let next_minute = (now + Duration::minutes(1)) | 183 | let next_minute = (now + Duration::minutes(1)) |
| 177 | .with_second(0) | 184 | .with_second(0) |
| 178 | .and_then(|t| t.with_nanosecond(0)) | 185 | .and_then(|t| t.with_nanosecond(0)) |
