Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .server-changes/stop-persisting-schedule-tick-state.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
area: webapp
type: improvement
---

Stop writing per-tick state (`lastScheduledTimestamp`, `nextScheduledTimestamp`, `lastRunTriggeredAt`) on `TaskSchedule` and `TaskScheduleInstance`. The schedule engine now carries the previous fire time forward via the worker queue payload, eliminating ~270K dead-tuple-driven autovacuums per year on these hot tables and the associated `IO:XactSync` mini-spikes on the writer. Customer-facing `payload.lastTimestamp` semantics are unchanged.
32 changes: 29 additions & 3 deletions apps/webapp/app/presenters/v3/ScheduleListPresenter.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import { getLimit } from "~/services/platform.v3.server";
import { findCurrentWorkerFromEnvironment } from "~/v3/models/workerDeployment.server";
import { ServiceValidationError } from "~/v3/services/baseService.server";
import { CheckScheduleService } from "~/v3/services/checkSchedule.server";
import { calculateNextScheduledTimestampFromNow } from "~/v3/utils/calculateNextSchedule.server";
import {
calculateNextScheduledTimestampFromNow,
previousScheduledTimestamp,
} from "~/v3/utils/calculateNextSchedule.server";
import { BasePresenter } from "./basePresenter.server";

type ScheduleListOptions = {
Expand Down Expand Up @@ -193,8 +196,8 @@ export class ScheduleListPresenter extends BasePresenter {
},
},
active: true,
lastRunTriggeredAt: true,
createdAt: true,
updatedAt: true,
},
where: {
projectId: project.id,
Expand Down Expand Up @@ -244,6 +247,29 @@ export class ScheduleListPresenter extends BasePresenter {
});

const schedules: ScheduleListItem[] = rawSchedules.map((schedule) => {
// Approximate "last run" from the cron's previous slot. Skip inactive
// schedules — the cron's previous slot reflects what *would* have
// fired, but a deactivated schedule didn't actually fire there. Skip
// when the cron's previous slot predates `updatedAt`: any config
// change (cron edited, timezone changed, deactivate/reactivate)
// bumps updatedAt, and a slot from before the most recent change
// didn't fire under the current configuration. cron-parser throws
// on malformed expressions, so degrade to undefined per-row rather
// than failing the whole list. UI is best-effort; the runs page is
// the source of truth.
let lastRun: Date | undefined;
if (schedule.active) {
try {
const cronPrev = previousScheduledTimestamp(
schedule.generatorExpression,
schedule.timezone
);
lastRun = cronPrev.getTime() > schedule.updatedAt.getTime() ? cronPrev : undefined;
} catch {
lastRun = undefined;
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

return {
id: schedule.id,
type: schedule.type,
Expand All @@ -256,7 +282,7 @@ export class ScheduleListPresenter extends BasePresenter {
timezone: schedule.timezone,
active: schedule.active,
externalId: schedule.externalId,
lastRun: schedule.lastRunTriggeredAt ?? undefined,
lastRun,
nextRun: calculateNextScheduledTimestampFromNow(
schedule.generatorExpression,
schedule.timezone
Expand Down
14 changes: 14 additions & 0 deletions apps/webapp/app/v3/utils/calculateNextSchedule.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,20 @@ function calculateNextStep(schedule: string, timezone: string | null, currentDat
.toDate();
}

export function previousScheduledTimestamp(
schedule: string,
timezone: string | null,
fromTimestamp: Date = new Date()
) {
return parseExpression(schedule, {
currentDate: fromTimestamp,
utc: timezone === null,
tz: timezone ?? undefined,
})
.prev()
.toDate();
}

export function nextScheduledTimestamps(
cron: string,
timezone: string | null,
Expand Down
3 changes: 3 additions & 0 deletions internal-packages/database/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -2128,6 +2128,7 @@ model TaskSchedule {
///Instances of the schedule that are active
instances TaskScheduleInstance[]

/// @deprecated stop writing 2026-04-30; reads moved out of code (UI now derives from cron's previous slot). Drop in follow-up.
lastRunTriggeredAt DateTime?

project Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade)
Expand Down Expand Up @@ -2173,7 +2174,9 @@ model TaskScheduleInstance {

active Boolean @default(true)

/// @deprecated stop writing 2026-04-30; engine derives from cron + exactScheduleTime. Drop in follow-up.
lastScheduledTimestamp DateTime?
/// @deprecated stop writing 2026-04-30; engine derives from cron + now(). Drop in follow-up.
nextScheduledTimestamp DateTime?

//you can only have a schedule attached to each environment once
Expand Down
Loading
Loading