From f81bb29fdaaa16f63b1cceff429f6cbb0cf63c23 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 27 May 2026 19:31:32 +0000 Subject: [PATCH] backup-scheduler: don't replay missed daily slots on startup The wake loop's daily-times branch was firing one missed slot per minute on service restart. With a schedule like "00:00,04:00,08:00,12:00,16:00, 20:00" and a restart at 19:00, the wrapper would catch up by firing 5 backups at 1-minute intervals -- each a full-world zip of the same in-memory world. Pure waste: the live world hasn't diverged across the missed clock slots, the snapshots would be near-identical, and the rapid save-off/save-on churn impacts player experience. Source comment already documented the intended behaviour ("Doesn't catch up if the server was off when a slot passed -- daily/interval backups don't need replay logic.") but the code didn't implement it. Fix: in Start(), pre-populate _firedToday with all times <= now so the wake loop only fires future slots for the rest of today. Tested live on glados: switched schedule to clock-aligned daily-times after deploying the fix, zero catch-up backups fired in the 90s window after restart (vs 5+ before the fix). Co-Authored-By: Claude Opus 4.7 --- server/Services/BackupScheduler.cs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/server/Services/BackupScheduler.cs b/server/Services/BackupScheduler.cs index 45d240b..b473256 100644 --- a/server/Services/BackupScheduler.cs +++ b/server/Services/BackupScheduler.cs @@ -37,11 +37,28 @@ public sealed class BackupScheduler : IDisposable public void Start() { if (string.IsNullOrWhiteSpace(_config.BackupSchedule)) return; - if (Parse(_config.BackupSchedule) == default) + var parsed = Parse(_config.BackupSchedule); + if (parsed == default) { _log($"[backup-scheduler] Invalid backupSchedule '{_config.BackupSchedule}'. Expected 'HH:mm', 'HH:mm,HH:mm', or 'every Nh'/'every Nm'. Disabled."); return; } + + // No-replay startup: mark all of today's already-passed slots as fired so + // the wake loop only fires future slots. Without this, a daily-times + // schedule like "00:00,04:00,...,20:00" on a service restart at 19:00 + // would catch up by firing 5 backups at 1-minute intervals (one per + // missed slot), each producing a full-world zip of the SAME world state. + // For full-snapshot backups that's pure waste -- the live world hasn't + // diverged across missed slots. + if (parsed.Times is not null) + { + var nowTime = TimeOnly.FromDateTime(DateTime.Now); + _lastFireDay = DateOnly.FromDateTime(DateTime.Now); + foreach (var t in parsed.Times) + if (t <= nowTime) _firedToday.Add(t); + } + _cts?.Cancel(); _cts = new CancellationTokenSource(); _loop = Task.Run(() => RunAsync(_cts.Token)); -- 2.52.0