feat(wipes): wire the auto-wiper — scheduled wipes now actually fire
wipe_schedules rows existed but nothing read or fired them — an operator could
set a wipe schedule and it would never trigger (the headline auto-wipe feature
was inert; the manual trigger worked, the scheduler did not).
- WipesService now implements OnModuleInit/OnModuleDestroy with a 60s executor
(mirrors SchedulesService): bootstraps next_scheduled_run, then fires every
active schedule whose next_scheduled_run <= now via triggerWipe(...'scheduled')
-> instancesService.wipeForLicense -> the agent wipe handler, advancing
next_scheduled_run from the cron each cycle (advances even on failure so a
broken schedule can't re-fire every 60s).
- triggerWipe parameterized with triggerType ('manual' | 'scheduled') so
wipe_history records the real origin.
- Extracted nextCronDate into src/common/cron.util.ts (shared by the event and
wipe schedulers; was duplicated/private). Cron is evaluated UTC — the per-
schedule timezone column is still not honored, a known limitation shared by
both schedulers (follow-up: tz-aware cron lib).
Backend tsc green. Scheduling logic is at parity with the in-production event
scheduler; live end-to-end (a scheduled wipe deleting real files) verifies when
a game stack + agent are connected.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
51
backend-nest/src/common/cron.util.ts
Normal file
51
backend-nest/src/common/cron.util.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Minimal 5-field cron "next run" calculator, shared by the event scheduler
|
||||
* (SchedulesService) and the wipe scheduler (WipesService).
|
||||
*
|
||||
* Supports `*` and exact numeric fields (minute hour day-of-month month
|
||||
* day-of-week). Walks minute-by-minute up to 366 days ahead. Returns null on a
|
||||
* malformed expression or if no match is found within a year.
|
||||
*
|
||||
* NOTE: the expression is evaluated in **UTC**. A per-schedule `timezone`
|
||||
* column exists on both schedule tables but is NOT yet honored here — fixing it
|
||||
* properly needs a timezone-aware cron library; tracked as a shared follow-up.
|
||||
*/
|
||||
export function nextCronDate(expr: string, after: Date): Date | null {
|
||||
const parts = expr.trim().split(/\s+/);
|
||||
if (parts.length !== 5) return null;
|
||||
|
||||
const [minuteExpr, hourExpr, domExpr, monthExpr, dowExpr] = parts;
|
||||
|
||||
const matches = (e: string, value: number): boolean => {
|
||||
if (e === '*') return true;
|
||||
return parseInt(e, 10) === value;
|
||||
};
|
||||
|
||||
// Walk minute-by-minute up to 366 days forward to find the next match.
|
||||
const candidate = new Date(after.getTime() + 60_000); // advance at least 1 minute
|
||||
candidate.setSeconds(0, 0);
|
||||
|
||||
const limit = new Date(after.getTime() + 366 * 24 * 60 * 60 * 1000);
|
||||
|
||||
while (candidate < limit) {
|
||||
const min = candidate.getUTCMinutes();
|
||||
const hour = candidate.getUTCHours();
|
||||
const dom = candidate.getUTCDate();
|
||||
const month = candidate.getUTCMonth() + 1; // 1-12
|
||||
const dow = candidate.getUTCDay(); // 0=Sun
|
||||
|
||||
if (
|
||||
matches(minuteExpr, min) &&
|
||||
matches(hourExpr, hour) &&
|
||||
matches(domExpr, dom) &&
|
||||
matches(monthExpr, month) &&
|
||||
matches(dowExpr, dow)
|
||||
) {
|
||||
return candidate;
|
||||
}
|
||||
|
||||
candidate.setTime(candidate.getTime() + 60_000);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user