Skip to content

Commit b01701f

Browse files
committed
fix(webapp): normalize default UTC timezone on declarative schedule sync (#5066)
1 parent 5f8171c commit b01701f

2 files changed

Lines changed: 45 additions & 8 deletions

File tree

‎apps/webapp/app/v3/services/createBackgroundWorker.server.test.ts‎

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ const asEnv = (
121121
organization: { id: env.organizationId, featureFlags: organizationFeatureFlags },
122122
}) as unknown as AuthenticatedEnvironment;
123123

124-
function declarativeTasks(schedule: { cron: string; timezone: string; window?: string }): TasksArg {
124+
function declarativeTasks(schedule: { cron: string; timezone?: string; window?: string }): TasksArg {
125125
return [{ id: "my-task", schedule }] as TasksArg;
126126
}
127127

@@ -488,6 +488,42 @@ describe("syncDeclarativeSchedules registration", () => {
488488
}
489489
);
490490

491+
containerTest(
492+
"preserves an existing Redis job when task schedule omits explicit timezone (defaults to UTC)",
493+
async ({ prisma, redisOptions }) => {
494+
const { project, prodEnv } = await seedProjectWithEnvs(prisma);
495+
const schedule = await makeDeclarativeSchedule(prisma, project.id, [prodEnv.id]);
496+
const updatedAt = schedule.updatedAt;
497+
await seedScheduledTask(prisma, project.id, prodEnv.id);
498+
const engine = createTestScheduleEngine(prisma, redisOptions);
499+
500+
try {
501+
const jobId = scheduleJobId(schedule.instances[0].id);
502+
await engine.registerNextTaskScheduleInstance({ instanceId: schedule.instances[0].id });
503+
const before = await engine.getJob(jobId);
504+
505+
await syncDeclarativeSchedules(
506+
declarativeTasks({ cron: "0 * * * *" }),
507+
noWorker,
508+
asEnv(prodEnv),
509+
prisma,
510+
engine
511+
);
512+
513+
expect(before).toBeDefined();
514+
expect(await engine.getJob(jobId)).toEqual(before);
515+
expect(
516+
await prisma.taskSchedule.findUniqueOrThrow({
517+
where: { id: schedule.id },
518+
select: { updatedAt: true },
519+
})
520+
).toEqual({ updatedAt });
521+
} finally {
522+
await engine.quit();
523+
}
524+
}
525+
);
526+
491527
containerTest(
492528
"replaces the Redis job when declarative timing changes",
493529
async ({ prisma, redisOptions }) => {

‎apps/webapp/app/v3/services/createBackgroundWorker.server.ts‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1528,7 +1528,7 @@ async function prepareDeclarativeSchedules(
15281528
}
15291529
assertCronMeetsFreeMinimum({
15301530
cron: task.schedule.cron,
1531-
timezone: task.schedule.timezone,
1531+
timezone: task.schedule.timezone ?? "UTC",
15321532
minimumWindowDurationSeconds,
15331533
scheduleType: "DECLARATIVE",
15341534
environmentType: environment.type,
@@ -1540,7 +1540,7 @@ async function prepareDeclarativeSchedules(
15401540
taskId: task.id,
15411541
explicitWindow: task.schedule.window,
15421542
cron: task.schedule.cron,
1543-
timezone: task.schedule.timezone,
1543+
timezone: task.schedule.timezone ?? "UTC",
15441544
defaultWindowDurationSeconds,
15451545
minimumWindowDurationSeconds,
15461546
});
@@ -1588,7 +1588,7 @@ export async function syncDeclarativeSchedules(
15881588
environment.projectId,
15891589
{
15901590
cron: task.schedule.cron,
1591-
timezone: task.schedule.timezone,
1591+
timezone: task.schedule.timezone ?? "UTC",
15921592
taskIdentifier: task.id,
15931593
friendlyId: existingSchedule?.friendlyId,
15941594
window: task.schedule.window,
@@ -1610,6 +1610,7 @@ export async function syncDeclarativeSchedules(
16101610
const apply = async (db: PrismaClientOrTransaction) => {
16111611
for (const { task, existingSchedule, minimumWindowDurationSeconds } of preparedTasks) {
16121612
const normalizedWindow = normalizeScheduleWindow(task.schedule.window);
1613+
const nextTimezone = task.schedule.timezone ?? "UTC";
16131614

16141615
if (existingSchedule) {
16151616
// Compare effective windows so equivalent explicit/default values preserve the pending job.
@@ -1625,13 +1626,13 @@ export async function syncDeclarativeSchedules(
16251626
}).window;
16261627
const timingChanged =
16271628
existingSchedule.generatorExpression !== task.schedule.cron ||
1628-
existingSchedule.timezone !== task.schedule.timezone ||
1629+
existingSchedule.timezone !== nextTimezone ||
16291630
!scheduleWindowsEqual(previousWindow, nextWindow) ||
16301631
// Clearing the minimum changes the effective range.
16311632
existingSchedule.minimumWindowDurationSeconds !== minimumWindowDurationSeconds;
16321633
const persistedValuesChanged =
16331634
existingSchedule.generatorExpression !== task.schedule.cron ||
1634-
existingSchedule.timezone !== task.schedule.timezone ||
1635+
existingSchedule.timezone !== nextTimezone ||
16351636
existingSchedule.windowDurationSeconds !== normalizedWindow.windowDurationSeconds ||
16361637
existingSchedule.windowPercentage !== normalizedWindow.windowPercentage ||
16371638
existingSchedule.minimumWindowDurationSeconds !== minimumWindowDurationSeconds;
@@ -1643,7 +1644,7 @@ export async function syncDeclarativeSchedules(
16431644
data: {
16441645
generatorExpression: task.schedule.cron,
16451646
generatorDescription: cronstrue.toString(task.schedule.cron),
1646-
timezone: task.schedule.timezone,
1647+
timezone: nextTimezone,
16471648
minimumWindowDurationSeconds,
16481649
...normalizedWindow,
16491650
},
@@ -1675,7 +1676,7 @@ export async function syncDeclarativeSchedules(
16751676
taskIdentifier: task.id,
16761677
generatorExpression: task.schedule.cron,
16771678
generatorDescription: cronstrue.toString(task.schedule.cron),
1678-
timezone: task.schedule.timezone,
1679+
timezone: nextTimezone,
16791680
type: "DECLARATIVE",
16801681
minimumWindowDurationSeconds,
16811682
...normalizedWindow,

0 commit comments

Comments
 (0)