Add pg-boss background job queue with session expiry
Add @trails-cool/jobs package wrapping pg-boss for durable background job execution using the existing PostgreSQL database. Wire up planner session expiry as the first scheduled job (hourly, 7-day TTL) — this was previously dead code that never ran. Add placeholder worker to journal for future Komoot import and federation jobs. Infrastructure: grant grafana_reader access to pgboss schema, add job queue health panels to Service Health dashboard, add failed job alert. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
fc80e66ad4
commit
32c5fbde8f
25 changed files with 614 additions and 6 deletions
21
packages/jobs/package.json
Normal file
21
packages/jobs/package.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"name": "@trails-cool/jobs",
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
"test": "vitest run",
|
||||
"lint": "eslint .",
|
||||
"typecheck": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"pg-boss": "^10.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:"
|
||||
}
|
||||
}
|
||||
5
packages/jobs/src/boss.ts
Normal file
5
packages/jobs/src/boss.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import PgBoss from "pg-boss";
|
||||
|
||||
export function createBoss(connectionString: string): PgBoss {
|
||||
return new PgBoss({ connectionString });
|
||||
}
|
||||
3
packages/jobs/src/index.ts
Normal file
3
packages/jobs/src/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export { createBoss } from "./boss.ts";
|
||||
export { startWorker } from "./worker.ts";
|
||||
export type { JobDefinition } from "./types.ts";
|
||||
9
packages/jobs/src/types.ts
Normal file
9
packages/jobs/src/types.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import type PgBoss from "pg-boss";
|
||||
|
||||
export interface JobDefinition<T = unknown> {
|
||||
name: string;
|
||||
handler: (jobs: PgBoss.Job<T>[]) => Promise<unknown>;
|
||||
cron?: string;
|
||||
retryLimit?: number;
|
||||
expireInSeconds?: number;
|
||||
}
|
||||
79
packages/jobs/src/worker.test.ts
Normal file
79
packages/jobs/src/worker.test.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { startWorker } from "./worker.ts";
|
||||
import type { JobDefinition } from "./types.ts";
|
||||
|
||||
function createMockBoss() {
|
||||
return {
|
||||
start: vi.fn().mockResolvedValue(undefined),
|
||||
stop: vi.fn().mockResolvedValue(undefined),
|
||||
schedule: vi.fn().mockResolvedValue(undefined),
|
||||
work: vi.fn().mockResolvedValue("worker-id"),
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("startWorker", () => {
|
||||
it("starts the boss instance", async () => {
|
||||
const boss = createMockBoss();
|
||||
await startWorker(boss as never, []);
|
||||
expect(boss.start).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("registers job handlers", async () => {
|
||||
const boss = createMockBoss();
|
||||
const handler = vi.fn();
|
||||
const jobs: JobDefinition[] = [{ name: "test-job", handler }];
|
||||
|
||||
await startWorker(boss as never, jobs);
|
||||
|
||||
expect(boss.work).toHaveBeenCalledWith("test-job", handler);
|
||||
});
|
||||
|
||||
it("schedules cron jobs with options", async () => {
|
||||
const boss = createMockBoss();
|
||||
const handler = vi.fn();
|
||||
const jobs: JobDefinition[] = [
|
||||
{
|
||||
name: "cron-job",
|
||||
handler,
|
||||
cron: "0 * * * *",
|
||||
retryLimit: 3,
|
||||
expireInSeconds: 120,
|
||||
},
|
||||
];
|
||||
|
||||
await startWorker(boss as never, jobs);
|
||||
|
||||
expect(boss.schedule).toHaveBeenCalledWith("cron-job", "0 * * * *", undefined, {
|
||||
retryLimit: 3,
|
||||
expireInSeconds: 120,
|
||||
});
|
||||
expect(boss.work).toHaveBeenCalledWith("cron-job", handler);
|
||||
});
|
||||
|
||||
it("does not schedule non-cron jobs", async () => {
|
||||
const boss = createMockBoss();
|
||||
const jobs: JobDefinition[] = [{ name: "one-shot", handler: vi.fn() }];
|
||||
|
||||
await startWorker(boss as never, jobs);
|
||||
|
||||
expect(boss.schedule).not.toHaveBeenCalled();
|
||||
expect(boss.work).toHaveBeenCalledWith("one-shot", jobs[0]!.handler);
|
||||
});
|
||||
|
||||
it("registers multiple jobs", async () => {
|
||||
const boss = createMockBoss();
|
||||
const jobs: JobDefinition[] = [
|
||||
{ name: "job-a", handler: vi.fn() },
|
||||
{ name: "job-b", handler: vi.fn(), cron: "*/5 * * * *" },
|
||||
];
|
||||
|
||||
await startWorker(boss as never, jobs);
|
||||
|
||||
expect(boss.work).toHaveBeenCalledTimes(2);
|
||||
expect(boss.schedule).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
28
packages/jobs/src/worker.ts
Normal file
28
packages/jobs/src/worker.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import type PgBoss from "pg-boss";
|
||||
import type { JobDefinition } from "./types.ts";
|
||||
|
||||
export async function startWorker(
|
||||
boss: PgBoss,
|
||||
jobs: JobDefinition[],
|
||||
): Promise<void> {
|
||||
await boss.start();
|
||||
|
||||
for (const job of jobs) {
|
||||
if (job.cron) {
|
||||
await boss.schedule(job.name, job.cron, undefined, {
|
||||
retryLimit: job.retryLimit,
|
||||
expireInSeconds: job.expireInSeconds,
|
||||
});
|
||||
}
|
||||
|
||||
await boss.work(job.name, job.handler);
|
||||
}
|
||||
|
||||
const onShutdown = async () => {
|
||||
await boss.stop({ graceful: true, timeout: 10_000 });
|
||||
process.exit(0);
|
||||
};
|
||||
|
||||
process.on("SIGTERM", onShutdown);
|
||||
process.on("SIGINT", onShutdown);
|
||||
}
|
||||
9
packages/jobs/tsconfig.json
Normal file
9
packages/jobs/tsconfig.json
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
1
packages/jobs/vitest.config.ts
Normal file
1
packages/jobs/vitest.config.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from "../../vitest.shared.ts";
|
||||
Loading…
Add table
Add a link
Reference in a new issue