Files
HealthLog/tests/integration/heavy-lane-defer.test.ts
T
Marc-André Bombeck 19732b5aa1 fix(jobs): let a sliced import give way to a waiting heavy job and warn on long lane waits
A job sent back by the heavy lane now leaves word that the lane is
wanted. The holder, and only the holder, sees it through laneWanted();
the Google Health backfill asks it where its slice budget already
stops resumable walks, after the first page, ends the slice there and
queues the rest past the deferred job's return. Holders that cannot
stop part-way run to their end as before.

The way back to the queue now returns when the oldest deferred job was
first queued; a job that has stood in line for two hours is reported
once per holder as job.heavy_lane.waiting_long at warn, so a lane held
for hours reaches the alerts.
2026-10-20 10:52:54 +02:00

157 lines
5.0 KiB
TypeScript

/**
* The heavy lane against a real pg-boss on a real Postgres: a heavy job that
* finds the lane taken goes back to its queue, keeps its id and its attempt,
* is not settled by pg-boss's completion of the handler that sent it back,
* and does not spend its expiry while it waits.
*
* The case it replaces: a job that waited in the handler ran its expiry down
* while it stood in line, because pg-boss counts the expiry from the fetch.
* Here the holder runs for six seconds against a two-second expiry on the
* waiting job; the waiting job still runs once, after the holder, without a
* single failed attempt.
*/
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { PgBoss, type Job } from "pg-boss";
import { prisma } from "@/lib/db";
import { deferLaneJobs, heavyLane } from "@/lib/jobs/heavy-lane";
import { jobDone } from "@/lib/jobs/job-outcome";
let boss: PgBoss;
const HOLDER_QUEUE = "data-backup-offhost";
const WAITER_QUEUE = "data-backup";
const WORK = { localConcurrency: 1, pollingIntervalSeconds: 0.5 } as const;
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
beforeAll(async () => {
boss = new PgBoss({ connectionString: process.env.DATABASE_URL });
await boss.start();
await boss.createQueue(HOLDER_QUEUE);
await boss.createQueue(WAITER_QUEUE);
}, 120_000);
afterAll(async () => {
await boss?.stop({ graceful: false });
});
async function row(id: string) {
const rows = await prisma.$queryRaw<
Array<{
state: string;
retry_count: number;
started_on: Date | null;
future: boolean;
}>
>`
SELECT state::text AS state, retry_count, started_on,
start_after > now() AS future
FROM pgboss.job WHERE id = ${id}::uuid
`;
return rows[0];
}
describe("deferLaneJobs", () => {
it("returns an active job to created, later, at the same attempt", async () => {
const id = await boss.send(WAITER_QUEUE, { probe: 1 });
const [fetched] = await boss.fetch(WAITER_QUEUE);
expect(fetched?.id).toBe(id);
expect((await row(id!))?.state).toBe("active");
const deferral = await deferLaneJobs(WAITER_QUEUE, [fetched!], 30);
expect(deferral.deferred).toBe(1);
// When it was first queued, so a long wait can be told from a short one.
expect(deferral.oldestCreatedOn).toBeInstanceOf(Date);
expect(Date.now() - deferral.oldestCreatedOn!.getTime()).toBeLessThan(
60_000,
);
expect(await row(id!)).toMatchObject({
state: "created",
retry_count: 0,
started_on: null,
future: true,
});
// Not claimable before its time.
expect(await boss.fetch(WAITER_QUEUE)).toEqual([]);
// pg-boss's completion of the handler finds no active row to settle.
await boss.complete(WAITER_QUEUE, id!);
expect((await row(id!))?.state).toBe("created");
await boss.deleteJob(WAITER_QUEUE, id!);
});
it("leaves a job alone that is no longer active", async () => {
const id = await boss.send(WAITER_QUEUE, { probe: 2 });
const job = { id, name: WAITER_QUEUE, data: {} } as unknown as Job<object>;
expect(await deferLaneJobs(WAITER_QUEUE, [job], 30)).toEqual({
deferred: 0,
oldestCreatedOn: null,
});
expect((await row(id!))?.future).toBe(false);
await boss.deleteJob(WAITER_QUEUE, id!);
});
});
describe("a heavy job behind a long one", () => {
it("waits in the queue, not in the handler, and runs once after it", async () => {
const log: string[] = [];
const deliveries: string[] = [];
const shortDefer = (queue: string, jobs: readonly Job<unknown>[]) =>
deferLaneJobs(queue, jobs, 1);
await boss.work(
HOLDER_QUEUE,
WORK,
heavyLane(
HOLDER_QUEUE,
async () => {
log.push("start:holder");
await sleep(6_000);
log.push("end:holder");
return jobDone({ jobs: 1 });
},
undefined,
shortDefer,
),
);
await boss.work(
WAITER_QUEUE,
WORK,
heavyLane(
WAITER_QUEUE,
async () => {
log.push("run:waiter");
return jobDone({ jobs: 1 });
},
undefined,
async (queue, jobs) => {
deliveries.push(jobs[0]!.id);
return shortDefer(queue, jobs);
},
),
);
await boss.send(HOLDER_QUEUE, {}, { expireInSeconds: 60 });
await sleep(1_000);
const waiter = await boss.send(
WAITER_QUEUE,
{},
{ expireInSeconds: 2, retryLimit: 0 },
);
await sleep(9_000);
await boss.offWork(WAITER_QUEUE);
await boss.offWork(HOLDER_QUEUE);
// Three times its expiry in line, and not a single failed attempt.
expect(await row(waiter!)).toMatchObject({
state: "completed",
retry_count: 0,
});
// Sent back several times while the holder ran, then run once after it.
expect(deliveries.length).toBeGreaterThanOrEqual(2);
expect(new Set(deliveries)).toEqual(new Set([waiter]));
expect(log).toEqual(["start:holder", "end:holder", "run:waiter"]);
}, 60_000);
});