feat: use userTimezone as default for cron schedules
Fixes #3318 Previously, cron schedules without an explicit `tz` field would use the system timezone, which could cause confusion if the server is in a different timezone than the user. Now, `agents.defaults.userTimezone` from the config is used as the default timezone for cron schedule computations when the schedule doesn't specify its own `tz`. Changes: - Added `defaultTimezone` option to `computeNextRunAtMs()` - Added `defaultTimezone` to `CronServiceDeps` - Gateway passes `userTimezone` from config to cron service - All job computations now respect the default timezone
This commit is contained in:
parent
3f83afe4a6
commit
8d34bcf32e
@ -1,7 +1,16 @@
|
|||||||
import { Cron } from "croner";
|
import { Cron } from "croner";
|
||||||
import type { CronSchedule } from "./types.js";
|
import type { CronSchedule } from "./types.js";
|
||||||
|
|
||||||
export function computeNextRunAtMs(schedule: CronSchedule, nowMs: number): number | undefined {
|
export type ComputeNextRunOpts = {
|
||||||
|
/** Default timezone to use when schedule.tz is not specified. */
|
||||||
|
defaultTimezone?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function computeNextRunAtMs(
|
||||||
|
schedule: CronSchedule,
|
||||||
|
nowMs: number,
|
||||||
|
opts?: ComputeNextRunOpts,
|
||||||
|
): number | undefined {
|
||||||
if (schedule.kind === "at") {
|
if (schedule.kind === "at") {
|
||||||
return schedule.atMs > nowMs ? schedule.atMs : undefined;
|
return schedule.atMs > nowMs ? schedule.atMs : undefined;
|
||||||
}
|
}
|
||||||
@ -17,8 +26,10 @@ export function computeNextRunAtMs(schedule: CronSchedule, nowMs: number): numbe
|
|||||||
|
|
||||||
const expr = schedule.expr.trim();
|
const expr = schedule.expr.trim();
|
||||||
if (!expr) return undefined;
|
if (!expr) return undefined;
|
||||||
|
// Use schedule.tz if specified, otherwise fall back to default timezone
|
||||||
|
const timezone = schedule.tz?.trim() || opts?.defaultTimezone?.trim() || undefined;
|
||||||
const cron = new Cron(expr, {
|
const cron = new Cron(expr, {
|
||||||
timezone: schedule.tz?.trim() || undefined,
|
timezone,
|
||||||
catch: false,
|
catch: false,
|
||||||
});
|
});
|
||||||
const next = cron.nextRun(new Date(nowMs));
|
const next = cron.nextRun(new Date(nowMs));
|
||||||
|
|||||||
@ -33,19 +33,24 @@ export function findJobOrThrow(state: CronServiceState, id: string) {
|
|||||||
return job;
|
return job;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function computeJobNextRunAtMs(job: CronJob, nowMs: number): number | undefined {
|
export function computeJobNextRunAtMs(
|
||||||
|
job: CronJob,
|
||||||
|
nowMs: number,
|
||||||
|
opts?: { defaultTimezone?: string },
|
||||||
|
): number | undefined {
|
||||||
if (!job.enabled) return undefined;
|
if (!job.enabled) return undefined;
|
||||||
if (job.schedule.kind === "at") {
|
if (job.schedule.kind === "at") {
|
||||||
// One-shot jobs stay due until they successfully finish.
|
// One-shot jobs stay due until they successfully finish.
|
||||||
if (job.state.lastStatus === "ok" && job.state.lastRunAtMs) return undefined;
|
if (job.state.lastStatus === "ok" && job.state.lastRunAtMs) return undefined;
|
||||||
return job.schedule.atMs;
|
return job.schedule.atMs;
|
||||||
}
|
}
|
||||||
return computeNextRunAtMs(job.schedule, nowMs);
|
return computeNextRunAtMs(job.schedule, nowMs, { defaultTimezone: opts?.defaultTimezone });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function recomputeNextRuns(state: CronServiceState) {
|
export function recomputeNextRuns(state: CronServiceState) {
|
||||||
if (!state.store) return;
|
if (!state.store) return;
|
||||||
const now = state.deps.nowMs();
|
const now = state.deps.nowMs();
|
||||||
|
const defaultTimezone = state.deps.defaultTimezone;
|
||||||
for (const job of state.store.jobs) {
|
for (const job of state.store.jobs) {
|
||||||
if (!job.state) job.state = {};
|
if (!job.state) job.state = {};
|
||||||
if (!job.enabled) {
|
if (!job.enabled) {
|
||||||
@ -61,7 +66,7 @@ export function recomputeNextRuns(state: CronServiceState) {
|
|||||||
);
|
);
|
||||||
job.state.runningAtMs = undefined;
|
job.state.runningAtMs = undefined;
|
||||||
}
|
}
|
||||||
job.state.nextRunAtMs = computeJobNextRunAtMs(job, now);
|
job.state.nextRunAtMs = computeJobNextRunAtMs(job, now, { defaultTimezone });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,6 +82,7 @@ export function nextWakeAtMs(state: CronServiceState) {
|
|||||||
|
|
||||||
export function createJob(state: CronServiceState, input: CronJobCreate): CronJob {
|
export function createJob(state: CronServiceState, input: CronJobCreate): CronJob {
|
||||||
const now = state.deps.nowMs();
|
const now = state.deps.nowMs();
|
||||||
|
const defaultTimezone = state.deps.defaultTimezone;
|
||||||
const id = crypto.randomUUID();
|
const id = crypto.randomUUID();
|
||||||
const job: CronJob = {
|
const job: CronJob = {
|
||||||
id,
|
id,
|
||||||
@ -97,7 +103,7 @@ export function createJob(state: CronServiceState, input: CronJobCreate): CronJo
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
assertSupportedJobSpec(job);
|
assertSupportedJobSpec(job);
|
||||||
job.state.nextRunAtMs = computeJobNextRunAtMs(job, now);
|
job.state.nextRunAtMs = computeJobNextRunAtMs(job, now, { defaultTimezone });
|
||||||
return job;
|
return job;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -24,6 +24,8 @@ export type CronServiceDeps = {
|
|||||||
log: Logger;
|
log: Logger;
|
||||||
storePath: string;
|
storePath: string;
|
||||||
cronEnabled: boolean;
|
cronEnabled: boolean;
|
||||||
|
/** Default timezone (IANA) for cron schedules that don't specify their own. */
|
||||||
|
defaultTimezone?: string;
|
||||||
enqueueSystemEvent: (text: string, opts?: { agentId?: string }) => void;
|
enqueueSystemEvent: (text: string, opts?: { agentId?: string }) => void;
|
||||||
requestHeartbeatNow: (opts?: { reason?: string }) => void;
|
requestHeartbeatNow: (opts?: { reason?: string }) => void;
|
||||||
runHeartbeatOnce?: (opts?: { reason?: string }) => Promise<HeartbeatRunResult>;
|
runHeartbeatOnce?: (opts?: { reason?: string }) => Promise<HeartbeatRunResult>;
|
||||||
|
|||||||
@ -43,9 +43,13 @@ export function buildGatewayCronService(params: {
|
|||||||
return { agentId, cfg: runtimeConfig };
|
return { agentId, cfg: runtimeConfig };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Use userTimezone from config as default for cron schedules
|
||||||
|
const defaultTimezone = params.cfg.agents?.defaults?.userTimezone;
|
||||||
|
|
||||||
const cron = new CronService({
|
const cron = new CronService({
|
||||||
storePath,
|
storePath,
|
||||||
cronEnabled,
|
cronEnabled,
|
||||||
|
defaultTimezone,
|
||||||
enqueueSystemEvent: (text, opts) => {
|
enqueueSystemEvent: (text, opts) => {
|
||||||
const { agentId, cfg: runtimeConfig } = resolveCronAgent(opts?.agentId);
|
const { agentId, cfg: runtimeConfig } = resolveCronAgent(opts?.agentId);
|
||||||
const sessionKey = resolveAgentMainSessionKey({
|
const sessionKey = resolveAgentMainSessionKey({
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user