From 70402ed855b2db6845b9c25d3149a04eb052567c Mon Sep 17 00:00:00 2001 From: Milind Chatrabhuji Date: Mon, 26 Oct 2026 07:45:09 +0530 Subject: [PATCH] fix(schedules): normalize default UTC timezone before declarative schedule diff (#5066) --- .../createBackgroundWorker.server.test.ts | 37 +++++++++++++++++++ .../services/createBackgroundWorker.server.ts | 13 ++++--- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/apps/webapp/app/v3/services/createBackgroundWorker.server.test.ts b/apps/webapp/app/v3/services/createBackgroundWorker.server.test.ts index 147721ecfef..91fdd1576a4 100644 --- a/apps/webapp/app/v3/services/createBackgroundWorker.server.test.ts +++ b/apps/webapp/app/v3/services/createBackgroundWorker.server.test.ts @@ -488,6 +488,43 @@ describe("syncDeclarativeSchedules registration", () => { } ); + containerTest( + "preserves an existing Redis job when task schedule omits explicit UTC timezone", + async ({ prisma, redisOptions }) => { + const { project, prodEnv } = await seedProjectWithEnvs(prisma); + const schedule = await makeDeclarativeSchedule(prisma, project.id, [prodEnv.id]); + const updatedAt = schedule.updatedAt; + await seedScheduledTask(prisma, project.id, prodEnv.id); + const engine = createTestScheduleEngine(prisma, redisOptions); + + try { + const jobId = scheduleJobId(schedule.instances[0].id); + await engine.registerNextTaskScheduleInstance({ instanceId: schedule.instances[0].id }); + const before = await engine.getJob(jobId); + + // Omit explicit timezone to verify default UTC normalization does not trigger false timing change + await syncDeclarativeSchedules( + declarativeTasks({ cron: "0 * * * *" }), + noWorker, + asEnv(prodEnv), + prisma, + engine + ); + + expect(before).toBeDefined(); + expect(await engine.getJob(jobId)).toEqual(before); + expect( + await prisma.taskSchedule.findUniqueOrThrow({ + where: { id: schedule.id }, + select: { updatedAt: true }, + }) + ).toEqual({ updatedAt }); + } finally { + await engine.quit(); + } + } + ); + containerTest( "replaces the Redis job when declarative timing changes", async ({ prisma, redisOptions }) => { diff --git a/apps/webapp/app/v3/services/createBackgroundWorker.server.ts b/apps/webapp/app/v3/services/createBackgroundWorker.server.ts index bbdfee76e85..11ec228225f 100644 --- a/apps/webapp/app/v3/services/createBackgroundWorker.server.ts +++ b/apps/webapp/app/v3/services/createBackgroundWorker.server.ts @@ -1588,7 +1588,7 @@ export async function syncDeclarativeSchedules( environment.projectId, { cron: task.schedule.cron, - timezone: task.schedule.timezone, + timezone: task.schedule.timezone ?? "UTC", taskIdentifier: task.id, friendlyId: existingSchedule?.friendlyId, window: task.schedule.window, @@ -1623,15 +1623,18 @@ export async function syncDeclarativeSchedules( windowPercentage: normalizedWindow.windowPercentage, defaultWindowDurationSeconds: existingSchedule.defaultWindowDurationSeconds, }).window; + const taskTimezone = task.schedule.timezone ?? "UTC"; + const existingTimezone = existingSchedule.timezone ?? "UTC"; + const timezoneChanged = existingTimezone !== taskTimezone; const timingChanged = existingSchedule.generatorExpression !== task.schedule.cron || - existingSchedule.timezone !== task.schedule.timezone || + timezoneChanged || !scheduleWindowsEqual(previousWindow, nextWindow) || // Clearing the minimum changes the effective range. existingSchedule.minimumWindowDurationSeconds !== minimumWindowDurationSeconds; const persistedValuesChanged = existingSchedule.generatorExpression !== task.schedule.cron || - existingSchedule.timezone !== task.schedule.timezone || + timezoneChanged || existingSchedule.windowDurationSeconds !== normalizedWindow.windowDurationSeconds || existingSchedule.windowPercentage !== normalizedWindow.windowPercentage || existingSchedule.minimumWindowDurationSeconds !== minimumWindowDurationSeconds; @@ -1643,7 +1646,7 @@ export async function syncDeclarativeSchedules( data: { generatorExpression: task.schedule.cron, generatorDescription: cronstrue.toString(task.schedule.cron), - timezone: task.schedule.timezone, + timezone: taskTimezone, minimumWindowDurationSeconds, ...normalizedWindow, }, @@ -1675,7 +1678,7 @@ export async function syncDeclarativeSchedules( taskIdentifier: task.id, generatorExpression: task.schedule.cron, generatorDescription: cronstrue.toString(task.schedule.cron), - timezone: task.schedule.timezone, + timezone: task.schedule.timezone ?? "UTC", type: "DECLARATIVE", minimumWindowDurationSeconds, ...normalizedWindow,