Preserve startup suppression and future timer gates

Keep the existing scheduling suppression check outside timer dispatch
when wiring its deadline worker. Preserve future timers while an idle
drain reconciles a write; only an expired deadline needs the bounded
retry interval when dispatch is paused.

Reproduced the CI startup assertion and a canceled-drain timer failure
before fixing them. Verified 159 focused tests across startup, native
monitors, deadlines, routines, SQL, cron and coordinator behavior, plus
server typecheck and build.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Devin FoleyandPaperclip committed 2026-10-15 18:10:50 -07:00
1 parent c73a58e6be
commit 9fc857dcec
4 files changed
+28 -4

No files matched your search

+2 -1
View File
@@ -1460,7 +1460,8 @@ The heartbeat interval still requests a recovery scan; it does not determine
the dispatch time of a known future deadline. Failed scans retry at that same
interval. Future timers do not hold idle work open, and standby, idle drain and
shutdown retain the coordinator's admission gates. During idle drain, expired
deadlines retry at the recovery interval while dispatch is paused. External database writers
deadlines retry at the recovery interval while dispatch is paused; future
deadlines keep their original timer so a canceled drain does not delay them. External database writers
rely on the periodic recovery scan. Sparse cron searches reuse one parsed
expression and skip impossible UTC dates before localized formatting.
@@ -183,6 +183,24 @@ describe("delivery work coordinator", () => {
expect(s.coordinator.nextWakeAt()).toBeNull();
});
it("preserves a future deadline while reconciling a write during idle drain", async () => {
const s = setup(), t = task();
const due = Date.now() + 1000;
let next: number | null = due;
await s.coordinator.register(DELIVERY_QUEUES.routineSchedules, { ...t, nextRunAt: () => next }).ready;
s.canRun.mockReturnValue(false);
s.canReconcile.mockReturnValue(true);
await s.enqueue(DELIVERY_QUEUES.routineSchedules);
await vi.advanceTimersByTimeAsync(1);
expect(s.coordinator.nextWakeAt()).toBe(due);
expect(t.run).toHaveBeenCalledTimes(1);
next = null;
s.canRun.mockReturnValue(true);
await vi.advanceTimersByTimeAsync(999);
expect(t.run).toHaveBeenCalledTimes(2);
expect(idleWorkSnapshot().active).toBe(0);
});
it("does not query during standby and resumes when admission opens", async () => {
const s = setup(), t = task(); s.canRun.mockReturnValue(false);
await s.coordinator.register(DELIVERY_QUEUES.toolAction, t).ready;
+4 -2
View File
@@ -1881,9 +1881,11 @@ async function startServerWithDatabaseTeardown(
const startHeartbeatScheduler = () => {
const scheduled = registerScheduledWork(deliveryWork, db, {
retryMs: config.heartbeatSchedulerIntervalMs,
tickTimers: () => heartbeat.tickTimers(new Date()).then(result => {
tickTimers: async () => {
if ((await heartbeat.resolveSchedulingSuppression()).suppressed) return;
const result = await heartbeat.tickTimers(new Date());
if (result.enqueued > 0) logger.info({ ...result }, "heartbeat timer tick enqueued runs");
}),
},
tickRoutines: () => routines.tickScheduledTriggers(new Date()).then(result => {
if (result.triggered > 0) logger.info({ ...result }, "routine scheduler tick enqueued runs");
}),
@@ -56,7 +56,10 @@ export function createDeliveryWorkCoordinator(input: {
if (pending || databaseWorkPending(input.owner, queue)) schedule(task.retryMs);
else { finishIntent?.(); finishIntent = undefined; }
const next = task.nextRunAt?.();
if (next != null) schedule(Math.max(input.canRun() ? 0 : task.retryMs, next - Date.now()));
if (next != null) {
const delay = Math.max(0, next - Date.now());
schedule(delay === 0 && !input.canRun() ? task.retryMs : delay);
}
}).catch(error => {
schedule(task.retryMs);
// Logging must never turn a recoverable sweep failure into an