diff --git a/apps/webapp/app/v3/services/createBackgroundWorker.server.test.ts b/apps/webapp/app/v3/services/createBackgroundWorker.server.test.ts index 147721ecfef..ae58319fd2c 100644 --- a/apps/webapp/app/v3/services/createBackgroundWorker.server.test.ts +++ b/apps/webapp/app/v3/services/createBackgroundWorker.server.test.ts @@ -121,7 +121,7 @@ const asEnv = ( organization: { id: env.organizationId, featureFlags: organizationFeatureFlags }, }) as unknown as AuthenticatedEnvironment; -function declarativeTasks(schedule: { cron: string; timezone: string; window?: string }): TasksArg { +function declarativeTasks(schedule: { cron: string; timezone?: string; window?: string }): TasksArg { return [{ id: "my-task", schedule }] as TasksArg; } @@ -488,6 +488,42 @@ describe("syncDeclarativeSchedules registration", () => { } ); + containerTest( + "preserves an existing Redis job when task schedule omits explicit timezone (defaults to UTC)", + 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); + + 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..e76e6bd7d39 100644 --- a/apps/webapp/app/v3/services/createBackgroundWorker.server.ts +++ b/apps/webapp/app/v3/services/createBackgroundWorker.server.ts @@ -1528,7 +1528,7 @@ async function prepareDeclarativeSchedules( } assertCronMeetsFreeMinimum({ cron: task.schedule.cron, - timezone: task.schedule.timezone, + timezone: task.schedule.timezone ?? "UTC", minimumWindowDurationSeconds, scheduleType: "DECLARATIVE", environmentType: environment.type, @@ -1540,7 +1540,7 @@ async function prepareDeclarativeSchedules( taskId: task.id, explicitWindow: task.schedule.window, cron: task.schedule.cron, - timezone: task.schedule.timezone, + timezone: task.schedule.timezone ?? "UTC", defaultWindowDurationSeconds, minimumWindowDurationSeconds, }); @@ -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, @@ -1610,6 +1610,7 @@ export async function syncDeclarativeSchedules( const apply = async (db: PrismaClientOrTransaction) => { for (const { task, existingSchedule, minimumWindowDurationSeconds } of preparedTasks) { const normalizedWindow = normalizeScheduleWindow(task.schedule.window); + const nextTimezone = task.schedule.timezone ?? "UTC"; if (existingSchedule) { // Compare effective windows so equivalent explicit/default values preserve the pending job. @@ -1625,13 +1626,13 @@ export async function syncDeclarativeSchedules( }).window; const timingChanged = existingSchedule.generatorExpression !== task.schedule.cron || - existingSchedule.timezone !== task.schedule.timezone || + existingSchedule.timezone !== nextTimezone || !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 || + existingSchedule.timezone !== nextTimezone || existingSchedule.windowDurationSeconds !== normalizedWindow.windowDurationSeconds || existingSchedule.windowPercentage !== normalizedWindow.windowPercentage || existingSchedule.minimumWindowDurationSeconds !== minimumWindowDurationSeconds; @@ -1643,7 +1644,7 @@ export async function syncDeclarativeSchedules( data: { generatorExpression: task.schedule.cron, generatorDescription: cronstrue.toString(task.schedule.cron), - timezone: task.schedule.timezone, + timezone: nextTimezone, minimumWindowDurationSeconds, ...normalizedWindow, }, @@ -1675,7 +1676,7 @@ export async function syncDeclarativeSchedules( taskIdentifier: task.id, generatorExpression: task.schedule.cron, generatorDescription: cronstrue.toString(task.schedule.cron), - timezone: task.schedule.timezone, + timezone: nextTimezone, type: "DECLARATIVE", minimumWindowDurationSeconds, ...normalizedWindow,