Upgrade pg-boss from 10 to 12
Supersedes dependabot #264 — this one involves code changes that PR couldn't make. ## Breaking changes in our usage surface ### v12: default export removed → named `PgBoss` export Affects every file that imports the SDK: - `packages/jobs/src/boss.ts`: `import PgBoss` → `import { PgBoss }` - `packages/jobs/src/worker.ts`: same for the type import - `packages/jobs/src/types.ts`: `PgBoss.Job<T>` → `Job<T>` (types.ts re-exports `Job` at the package root in v12) ### v11: queue names restricted to `[A-Za-z0-9_.-]` Colon `:` is no longer allowed. Renamed the two journal queues that had it: - `demo-bot:generate` → `demo-bot-generate` - `demo-bot:prune` → `demo-bot-prune` The planner's `expire-sessions` was already valid. ### v12: minimum Node 22.12 Not a code change for us — journal + planner Dockerfiles are on `node:25-slim`, CI runners on 24. ## Regression guard Added `assertValidJobName()` in `@trails-cool/jobs`, called by `startWorker()` before any side effects. Pg-boss v11+ silently accepts an invalid name then rejects the underlying SQL call later — we fail loudly at boot instead. Unit tests cover the character-class rule and exercise the exact old-name (`demo-bot:generate`) as a regression fence. ## Prod rollout note Pg-boss v11 dropped the auto-migration path from v10. On deploy, the live `pgboss` schema from v10 won't migrate cleanly. Simplest path: `DROP SCHEMA pgboss CASCADE` before the first v12 worker starts — our jobs are all cron-scheduled and will re-register themselves on boot, so there's nothing durable to preserve in the queue. ## Verified - `pnpm typecheck` / `pnpm lint` / `pnpm test` — all clean - `pnpm exec playwright test --workers=2` — 50/50 passed Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5b48ba388b
commit
c2e8461f9c
8 changed files with 117 additions and 35 deletions
|
|
@ -13,7 +13,7 @@
|
|||
"typecheck": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"pg-boss": "^10.3.1"
|
||||
"pg-boss": "^12.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import PgBoss from "pg-boss";
|
||||
import { PgBoss } from "pg-boss";
|
||||
|
||||
export function createBoss(connectionString: string): PgBoss {
|
||||
return new PgBoss({ connectionString });
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import type PgBoss from "pg-boss";
|
||||
import type { Job } from "pg-boss";
|
||||
|
||||
export interface JobDefinition<T = unknown> {
|
||||
export interface JobDefinition<T extends object = object> {
|
||||
name: string;
|
||||
handler: (jobs: PgBoss.Job<T>[]) => Promise<unknown>;
|
||||
handler: (jobs: Job<T>[]) => Promise<unknown>;
|
||||
cron?: string;
|
||||
retryLimit?: number;
|
||||
expireInSeconds?: number;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { startWorker } from "./worker.ts";
|
||||
import { assertValidJobName, startWorker } from "./worker.ts";
|
||||
import type { JobDefinition } from "./types.ts";
|
||||
|
||||
function createMockBoss() {
|
||||
|
|
@ -87,4 +87,49 @@ describe("startWorker", () => {
|
|||
expect(boss.work).toHaveBeenCalledTimes(2);
|
||||
expect(boss.schedule).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("rejects queue names with characters that pg-boss v11+ forbids", async () => {
|
||||
const boss = createMockBoss();
|
||||
// `:` was allowed by pg-boss v10 and got us into trouble on upgrade
|
||||
// (our queues were named demo-bot:generate / demo-bot:prune until we
|
||||
// renamed them for v11+). Regression-guard that shape.
|
||||
const jobs: JobDefinition[] = [{ name: "demo-bot:generate", handler: vi.fn() }];
|
||||
|
||||
await expect(startWorker(boss as never, jobs)).rejects.toThrow(
|
||||
/Invalid pg-boss queue name/,
|
||||
);
|
||||
// start/createQueue/schedule/work must not have been called — we
|
||||
// fail before any side effects.
|
||||
expect(boss.start).not.toHaveBeenCalled();
|
||||
expect(boss.createQueue).not.toHaveBeenCalled();
|
||||
expect(boss.schedule).not.toHaveBeenCalled();
|
||||
expect(boss.work).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("assertValidJobName", () => {
|
||||
it("accepts letters, numbers, hyphens, underscores, and periods", () => {
|
||||
for (const name of [
|
||||
"demo-bot-generate",
|
||||
"expire_sessions",
|
||||
"job.v2",
|
||||
"DemoBot123",
|
||||
"a",
|
||||
]) {
|
||||
expect(() => assertValidJobName(name)).not.toThrow();
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects colon, slash, whitespace, and other punctuation", () => {
|
||||
for (const name of [
|
||||
"demo-bot:generate",
|
||||
"foo/bar",
|
||||
"foo bar",
|
||||
"foo@bar",
|
||||
"foo#bar",
|
||||
"",
|
||||
]) {
|
||||
expect(() => assertValidJobName(name)).toThrow(/Invalid pg-boss queue name/);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,10 +1,31 @@
|
|||
import type PgBoss from "pg-boss";
|
||||
import type { PgBoss } from "pg-boss";
|
||||
import type { JobDefinition } from "./types.ts";
|
||||
|
||||
/**
|
||||
* Characters pg-boss v11+ accepts for queue and schedule keys. Stricter
|
||||
* than v10, which silently accepted `:` (and therefore let our
|
||||
* `demo-bot:generate` etc. names through on older pg-boss). On the
|
||||
* upgrade path we renamed them, and this guard keeps us from regressing.
|
||||
*/
|
||||
const VALID_NAME = /^[A-Za-z0-9_.-]+$/;
|
||||
|
||||
export function assertValidJobName(name: string): void {
|
||||
if (!VALID_NAME.test(name)) {
|
||||
throw new Error(
|
||||
`Invalid pg-boss queue name "${name}": only letters, numbers, hyphens, underscores, and periods are allowed.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function startWorker(
|
||||
boss: PgBoss,
|
||||
jobs: JobDefinition[],
|
||||
): Promise<void> {
|
||||
// Validate every job name before any side effects so a bad name fails
|
||||
// loudly at worker boot instead of silently producing an unreachable
|
||||
// queue somewhere downstream.
|
||||
for (const job of jobs) assertValidJobName(job.name);
|
||||
|
||||
await boss.start();
|
||||
|
||||
for (const job of jobs) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue