Skip to content

Commit 3fc5630

Browse files
authored
fix: calculating next schedule should always just be from now (#2102)
1 parent 13d378f commit 3fc5630

File tree

5 files changed

+42
-66
lines changed

5 files changed

+42
-66
lines changed

apps/webapp/app/presenters/v3/ScheduleListPresenter.server.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
1-
import { Prisma, type RuntimeEnvironmentType, type ScheduleType } from "@trigger.dev/database";
1+
import { type RuntimeEnvironmentType, type ScheduleType } from "@trigger.dev/database";
22
import { type ScheduleListFilters } from "~/components/runs/v3/ScheduleFilters";
3-
import { sqlDatabaseSchema } from "~/db.server";
43
import { displayableEnvironment } from "~/models/runtimeEnvironment.server";
54
import { getLimit } from "~/services/platform.v3.server";
6-
import { CheckScheduleService } from "~/v3/services/checkSchedule.server";
7-
import { calculateNextScheduledTimestamp } from "~/v3/utils/calculateNextSchedule.server";
8-
import { BasePresenter } from "./basePresenter.server";
95
import { findCurrentWorkerFromEnvironment } from "~/v3/models/workerDeployment.server";
106
import { ServiceValidationError } from "~/v3/services/baseService.server";
7+
import { CheckScheduleService } from "~/v3/services/checkSchedule.server";
8+
import { calculateNextScheduledTimestampFromNow } from "~/v3/utils/calculateNextSchedule.server";
9+
import { BasePresenter } from "./basePresenter.server";
1110

1211
type ScheduleListOptions = {
1312
projectId: string;
@@ -258,7 +257,10 @@ export class ScheduleListPresenter extends BasePresenter {
258257
active: schedule.active,
259258
externalId: schedule.externalId,
260259
lastRun: schedule.lastRunTriggeredAt ?? undefined,
261-
nextRun: calculateNextScheduledTimestamp(schedule.generatorExpression, schedule.timezone),
260+
nextRun: calculateNextScheduledTimestampFromNow(
261+
schedule.generatorExpression,
262+
schedule.timezone
263+
),
262264
environments: schedule.instances.map((instance) => {
263265
const environment = project.environments.find((env) => env.id === instance.environmentId);
264266
if (!environment) {

apps/webapp/app/v3/services/registerNextTaskScheduleInstance.server.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { startActiveSpan } from "../tracer.server";
2-
import { calculateNextScheduledTimestamp } from "../utils/calculateNextSchedule.server";
2+
import { calculateNextScheduledTimestampFromNow } from "../utils/calculateNextSchedule.server";
33
import { BaseService } from "./baseService.server";
44
import { TriggerScheduledTaskService } from "./triggerScheduledTask.server";
55

@@ -33,10 +33,9 @@ export class RegisterNextTaskScheduleInstanceService extends BaseService {
3333
instance.lastScheduledTimestamp?.toISOString() ?? new Date().toISOString()
3434
);
3535

36-
return calculateNextScheduledTimestamp(
36+
return calculateNextScheduledTimestampFromNow(
3737
instance.taskSchedule.generatorExpression,
38-
instance.taskSchedule.timezone,
39-
instance.lastScheduledTimestamp ?? new Date()
38+
instance.taskSchedule.timezone
4039
);
4140
}
4241
);

apps/webapp/app/v3/services/upsertTaskSchedule.server.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { nanoid } from "nanoid";
44
import { $transaction } from "~/db.server";
55
import { generateFriendlyId } from "../friendlyIdentifiers";
66
import { type UpsertSchedule } from "../schedules";
7-
import { calculateNextScheduledTimestamp } from "../utils/calculateNextSchedule.server";
7+
import { calculateNextScheduledTimestampFromNow } from "../utils/calculateNextSchedule.server";
88
import { BaseService, ServiceValidationError } from "./baseService.server";
99
import { CheckScheduleService } from "./checkSchedule.server";
1010
import { RegisterNextTaskScheduleInstanceService } from "./registerNextTaskScheduleInstance.server";
@@ -262,7 +262,7 @@ export class UpsertTaskScheduleService extends BaseService {
262262
cron: taskSchedule.generatorExpression,
263263
cronDescription: taskSchedule.generatorDescription,
264264
timezone: taskSchedule.timezone,
265-
nextRun: calculateNextScheduledTimestamp(
265+
nextRun: calculateNextScheduledTimestampFromNow(
266266
taskSchedule.generatorExpression,
267267
taskSchedule.timezone
268268
),

apps/webapp/app/v3/utils/calculateNextSchedule.server.ts

Lines changed: 6 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,15 @@
11
import { parseExpression } from "cron-parser";
22

3+
export function calculateNextScheduledTimestampFromNow(schedule: string, timezone: string | null) {
4+
return calculateNextScheduledTimestamp(schedule, timezone, new Date());
5+
}
6+
37
export function calculateNextScheduledTimestamp(
48
schedule: string,
59
timezone: string | null,
6-
lastScheduledTimestamp: Date = new Date()
10+
currentDate: Date = new Date()
711
) {
8-
const now = Date.now();
9-
10-
let nextStep = calculateNextStep(schedule, timezone, lastScheduledTimestamp);
11-
12-
// If the next step is still in the past, we might need to skip ahead
13-
if (nextStep.getTime() <= now) {
14-
// Calculate a second step to determine the interval
15-
const secondStep = calculateNextStep(schedule, timezone, nextStep);
16-
const interval = secondStep.getTime() - nextStep.getTime();
17-
18-
// If we have a consistent interval and it would take many iterations,
19-
// skip ahead mathematically instead of iterating
20-
if (interval > 0) {
21-
const stepsNeeded = Math.floor((now - nextStep.getTime()) / interval);
22-
23-
// Only skip ahead if it would save us more than a few iterations
24-
if (stepsNeeded > 10) {
25-
// Skip ahead by calculating how many intervals to add
26-
const skipAheadTime = nextStep.getTime() + stepsNeeded * interval;
27-
nextStep = calculateNextStep(schedule, timezone, new Date(skipAheadTime));
28-
}
29-
}
30-
31-
// Use the normal iteration for the remaining steps (should be <= 10 now)
32-
while (nextStep.getTime() <= now) {
33-
nextStep = calculateNextStep(schedule, timezone, nextStep);
34-
}
35-
}
36-
37-
return nextStep;
12+
return calculateNextStep(schedule, timezone, currentDate);
3813
}
3914

4015
function calculateNextStep(schedule: string, timezone: string | null, currentDate: Date) {

apps/webapp/test/calculateNextSchedule.test.ts

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { describe, test, expect, beforeEach, afterEach, vi } from "vitest";
2-
import { calculateNextScheduledTimestamp } from "../app/v3/utils/calculateNextSchedule.server";
2+
import { calculateNextScheduledTimestampFromNow } from "../app/v3/utils/calculateNextSchedule.server";
33

4-
describe("calculateNextScheduledTimestamp", () => {
4+
describe("calculateNextScheduledTimestampFromNow", () => {
55
beforeEach(() => {
66
// Mock the current time to make tests deterministic
77
vi.useFakeTimers();
@@ -16,7 +16,7 @@ describe("calculateNextScheduledTimestamp", () => {
1616
const schedule = "0 * * * *"; // Every hour
1717
const lastRun = new Date("2024-01-01T11:00:00.000Z"); // 1.5 hours ago
1818

19-
const nextRun = calculateNextScheduledTimestamp(schedule, null, lastRun);
19+
const nextRun = calculateNextScheduledTimestampFromNow(schedule, null);
2020

2121
// Should be 13:00 (next hour after current time 12:30)
2222
expect(nextRun).toEqual(new Date("2024-01-01T13:00:00.000Z"));
@@ -26,7 +26,7 @@ describe("calculateNextScheduledTimestamp", () => {
2626
const schedule = "0 * * * *"; // Every hour
2727
const lastRun = new Date("2024-01-01T11:00:00.000Z");
2828

29-
const nextRun = calculateNextScheduledTimestamp(schedule, "America/New_York", lastRun);
29+
const nextRun = calculateNextScheduledTimestampFromNow(schedule, "America/New_York");
3030

3131
// The exact time will depend on timezone calculation, but should be in the future
3232
expect(nextRun).toBeInstanceOf(Date);
@@ -38,7 +38,7 @@ describe("calculateNextScheduledTimestamp", () => {
3838
const veryOldTimestamp = new Date("2020-01-01T00:00:00.000Z"); // 4 years ago
3939

4040
const startTime = performance.now();
41-
const nextRun = calculateNextScheduledTimestamp(schedule, null, veryOldTimestamp);
41+
const nextRun = calculateNextScheduledTimestampFromNow(schedule, null);
4242
const duration = performance.now() - startTime;
4343

4444
// Should complete quickly (under 10ms) instead of iterating millions of times
@@ -55,7 +55,7 @@ describe("calculateNextScheduledTimestamp", () => {
5555
const schedule = "0 */2 * * *"; // Every 2 hours
5656
const recentTimestamp = new Date("2024-01-01T10:00:00.000Z"); // 2.5 hours ago
5757

58-
const nextRun = calculateNextScheduledTimestamp(schedule, null, recentTimestamp);
58+
const nextRun = calculateNextScheduledTimestampFromNow(schedule, null);
5959

6060
// Should properly iterate: 10:00 -> 12:00 -> 14:00 (since current time is 12:30)
6161
expect(nextRun).toEqual(new Date("2024-01-01T14:00:00.000Z"));
@@ -66,7 +66,7 @@ describe("calculateNextScheduledTimestamp", () => {
6666
const oldTimestamp = new Date("2023-12-01T00:00:00.000Z"); // Over a month ago
6767

6868
const startTime = performance.now();
69-
const nextRun = calculateNextScheduledTimestamp(schedule, null, oldTimestamp);
69+
const nextRun = calculateNextScheduledTimestampFromNow(schedule, null);
7070
const duration = performance.now() - startTime;
7171

7272
// Should be fast due to dynamic skip-ahead optimization
@@ -80,7 +80,7 @@ describe("calculateNextScheduledTimestamp", () => {
8080
const schedule = "0 9 * * MON"; // Every Monday at 9 AM
8181
const oldTimestamp = new Date("2022-01-01T00:00:00.000Z"); // Very old (beyond 1hr threshold)
8282

83-
const nextRun = calculateNextScheduledTimestamp(schedule, null, oldTimestamp);
83+
const nextRun = calculateNextScheduledTimestampFromNow(schedule, null);
8484

8585
// Should return a valid future Monday at 9 AM
8686
expect(nextRun.getHours()).toBe(9);
@@ -95,7 +95,7 @@ describe("calculateNextScheduledTimestamp", () => {
9595
const extremelyOldTimestamp = new Date("2000-01-01T00:00:00.000Z"); // 24 years ago
9696

9797
const startTime = performance.now();
98-
const nextRun = calculateNextScheduledTimestamp(schedule, null, extremelyOldTimestamp);
98+
const nextRun = calculateNextScheduledTimestampFromNow(schedule, null);
9999
const duration = performance.now() - startTime;
100100

101101
// Should complete extremely quickly due to dynamic skip-ahead
@@ -111,7 +111,7 @@ describe("calculateNextScheduledTimestamp", () => {
111111
const oldTimestamp = new Date("2023-12-31T12:31:00.000Z"); // 23h59m ago
112112

113113
const startTime = performance.now();
114-
const nextRun = calculateNextScheduledTimestamp(schedule, null, oldTimestamp);
114+
const nextRun = calculateNextScheduledTimestampFromNow(schedule, null);
115115
const duration = performance.now() - startTime;
116116

117117
// Should be fast due to dynamic skip-ahead (1439 steps > 10 threshold)
@@ -127,7 +127,7 @@ describe("calculateNextScheduledTimestamp", () => {
127127
const recentTimestamp = new Date("2024-01-01T12:00:00.000Z"); // 30 minutes ago (6 steps)
128128

129129
const startTime = performance.now();
130-
const nextRun = calculateNextScheduledTimestamp(schedule, null, recentTimestamp);
130+
const nextRun = calculateNextScheduledTimestampFromNow(schedule, null);
131131
const duration = performance.now() - startTime;
132132

133133
// Should still be reasonably fast with normal iteration
@@ -142,7 +142,7 @@ describe("calculateNextScheduledTimestamp", () => {
142142
const oldTimestamp = new Date("2023-12-25T09:00:00.000Z"); // Old Monday
143143

144144
const startTime = performance.now();
145-
const nextRun = calculateNextScheduledTimestamp(schedule, null, oldTimestamp);
145+
const nextRun = calculateNextScheduledTimestampFromNow(schedule, null);
146146
const duration = performance.now() - startTime;
147147

148148
// Should be fast and still calculate correctly from the old timestamp
@@ -160,7 +160,7 @@ describe("calculateNextScheduledTimestamp", () => {
160160
const schedule = "0 14 * * SUN"; // Every Sunday at 2 PM
161161
const twoHoursAgo = new Date("2024-01-01T10:30:00.000Z"); // 2 hours before current time (12:30)
162162

163-
const nextRun = calculateNextScheduledTimestamp(schedule, null, twoHoursAgo);
163+
const nextRun = calculateNextScheduledTimestampFromNow(schedule, null);
164164

165165
// Should properly calculate the next Sunday at 2 PM, not skip to "now"
166166
expect(nextRun.getHours()).toBe(14);
@@ -170,7 +170,7 @@ describe("calculateNextScheduledTimestamp", () => {
170170
});
171171
});
172172

173-
describe("calculateNextScheduledTimestamp - Fuzzy Testing", () => {
173+
describe("calculateNextScheduledTimestampFromNow - Fuzzy Testing", () => {
174174
beforeEach(() => {
175175
vi.useFakeTimers();
176176
vi.setSystemTime(new Date("2024-01-15T12:30:00.000Z")); // Monday, mid-day
@@ -254,7 +254,7 @@ describe("calculateNextScheduledTimestamp - Fuzzy Testing", () => {
254254

255255
try {
256256
const startTime = performance.now();
257-
const nextRun = calculateNextScheduledTimestamp(schedule, timezone, lastTimestamp);
257+
const nextRun = calculateNextScheduledTimestampFromNow(schedule, timezone);
258258
const duration = performance.now() - startTime;
259259

260260
// Invariant 1: Result should always be a valid Date
@@ -270,7 +270,7 @@ describe("calculateNextScheduledTimestamp - Fuzzy Testing", () => {
270270
expect(duration).toBeLessThan(100); // Should complete within 100ms
271271

272272
// Invariant 4: Function should be deterministic
273-
const nextRun2 = calculateNextScheduledTimestamp(schedule, timezone, lastTimestamp);
273+
const nextRun2 = calculateNextScheduledTimestampFromNow(schedule, timezone);
274274
expect(nextRun.getTime()).toBe(nextRun2.getTime());
275275
} catch (error) {
276276
// If there's an error, log the inputs for debugging
@@ -292,7 +292,7 @@ describe("calculateNextScheduledTimestamp - Fuzzy Testing", () => {
292292
const veryOldTimestamp = new Date(Date.now() - Math.random() * 5 * 365 * 24 * 60 * 60 * 1000);
293293

294294
const startTime = performance.now();
295-
const nextRun = calculateNextScheduledTimestamp(schedule, null, veryOldTimestamp);
295+
const nextRun = calculateNextScheduledTimestampFromNow(schedule, null);
296296
const duration = performance.now() - startTime;
297297

298298
// Should complete quickly even with very old timestamps
@@ -321,7 +321,7 @@ describe("calculateNextScheduledTimestamp - Fuzzy Testing", () => {
321321

322322
const lastTimestamp = new Date(Date.now() - Math.random() * 7 * 24 * 60 * 60 * 1000);
323323

324-
const nextRun = calculateNextScheduledTimestamp(schedule, timezone, lastTimestamp);
324+
const nextRun = calculateNextScheduledTimestampFromNow(schedule, timezone);
325325

326326
// Should handle DST transitions gracefully
327327
expect(nextRun).toBeInstanceOf(Date);
@@ -354,8 +354,8 @@ describe("calculateNextScheduledTimestamp - Fuzzy Testing", () => {
354354
const beforeBoundary = new Date(Date.now() - 1000);
355355
const afterBoundary = new Date(Date.now() + 1000);
356356

357-
const nextRun1 = calculateNextScheduledTimestamp(test.schedule, null, beforeBoundary);
358-
const nextRun2 = calculateNextScheduledTimestamp(test.schedule, null, afterBoundary);
357+
const nextRun1 = calculateNextScheduledTimestampFromNow(test.schedule, null);
358+
const nextRun2 = calculateNextScheduledTimestampFromNow(test.schedule, null);
359359

360360
expect(nextRun1.getTime()).toBeGreaterThan(Date.now());
361361
expect(nextRun2.getTime()).toBeGreaterThan(Date.now());
@@ -378,7 +378,7 @@ describe("calculateNextScheduledTimestamp - Fuzzy Testing", () => {
378378

379379
try {
380380
const startTime = performance.now();
381-
const nextRun = calculateNextScheduledTimestamp(schedule, null, lastTimestamp);
381+
const nextRun = calculateNextScheduledTimestampFromNow(schedule, null);
382382
const duration = performance.now() - startTime;
383383

384384
expect(nextRun).toBeInstanceOf(Date);
@@ -409,7 +409,7 @@ describe("calculateNextScheduledTimestamp - Fuzzy Testing", () => {
409409

410410
const results: Date[] = [];
411411
for (let j = 0; j < 5; j++) {
412-
results.push(calculateNextScheduledTimestamp(schedule, timezone, lastTimestamp));
412+
results.push(calculateNextScheduledTimestampFromNow(schedule, timezone));
413413
}
414414

415415
// All results should be identical
@@ -436,7 +436,7 @@ describe("calculateNextScheduledTimestamp - Fuzzy Testing", () => {
436436
const lastTimestamp = new Date(Date.now() - testCase.minutesAgo * 60 * 1000);
437437

438438
const startTime = performance.now();
439-
const nextRun = calculateNextScheduledTimestamp(testCase.schedule, null, lastTimestamp);
439+
const nextRun = calculateNextScheduledTimestampFromNow(testCase.schedule, null);
440440
const duration = performance.now() - startTime;
441441

442442
// All cases should complete quickly and return valid results

0 commit comments

Comments
 (0)