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
|
|
@ -20,6 +20,7 @@
|
|||
"@simplewebauthn/server": "^13.3.0",
|
||||
"@trails-cool/api": "workspace:*",
|
||||
"@trails-cool/db": "workspace:*",
|
||||
"@trails-cool/jobs": "workspace:*",
|
||||
"@trails-cool/gpx": "workspace:*",
|
||||
"@trails-cool/i18n": "workspace:*",
|
||||
"@trails-cool/map": "workspace:*",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { createReadStream, statSync } from "node:fs";
|
|||
import { join, extname, resolve } from "node:path";
|
||||
import { logger } from "./app/lib/logger.server.ts";
|
||||
import { httpRequestDuration, registry } from "./app/lib/metrics.server.ts";
|
||||
import { createBoss, startWorker } from "@trails-cool/jobs";
|
||||
import postgres from "postgres";
|
||||
|
||||
const port = Number(process.env.PORT ?? 3000);
|
||||
|
|
@ -98,4 +99,9 @@ server.listen(port, async () => {
|
|||
// Seed first-party OAuth2 clients
|
||||
const { seedOAuthClient } = await import("./app/lib/oauth.server.ts");
|
||||
await seedOAuthClient("trails-cool-mobile", "trailscool://auth/callback", true);
|
||||
|
||||
// Start background job worker (no jobs yet — placeholder for Komoot import and federation)
|
||||
const boss = createBoss(process.env.DATABASE_URL ?? "postgres://trails:trails@localhost:5432/trails");
|
||||
await startWorker(boss, []);
|
||||
logger.info("Background job worker started");
|
||||
});
|
||||
|
|
|
|||
30
apps/planner/app/jobs/expire-sessions.test.ts
Normal file
30
apps/planner/app/jobs/expire-sessions.test.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import { describe, it, expect, vi } from "vitest";
|
||||
|
||||
vi.mock("../lib/sessions.ts", () => ({
|
||||
expireSessions: vi.fn().mockResolvedValue(5),
|
||||
}));
|
||||
|
||||
vi.mock("../lib/logger.server.ts", () => ({
|
||||
logger: { info: vi.fn() },
|
||||
}));
|
||||
|
||||
describe("expireSessionsJob", () => {
|
||||
it("calls expireSessions with 7-day TTL and returns count", async () => {
|
||||
const { expireSessionsJob } = await import("./expire-sessions.ts");
|
||||
const { expireSessions } = await import("../lib/sessions.ts");
|
||||
|
||||
const result = await expireSessionsJob.handler({ id: "test", name: "expire-sessions", data: {} } as never);
|
||||
|
||||
expect(expireSessions).toHaveBeenCalledWith(7);
|
||||
expect(result).toEqual({ expiredCount: 5 });
|
||||
});
|
||||
|
||||
it("has correct job configuration", async () => {
|
||||
const { expireSessionsJob } = await import("./expire-sessions.ts");
|
||||
|
||||
expect(expireSessionsJob.name).toBe("expire-sessions");
|
||||
expect(expireSessionsJob.cron).toBe("0 * * * *");
|
||||
expect(expireSessionsJob.retryLimit).toBe(2);
|
||||
expect(expireSessionsJob.expireInSeconds).toBe(60);
|
||||
});
|
||||
});
|
||||
15
apps/planner/app/jobs/expire-sessions.ts
Normal file
15
apps/planner/app/jobs/expire-sessions.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import type { JobDefinition } from "@trails-cool/jobs";
|
||||
import { expireSessions } from "../lib/sessions.ts";
|
||||
import { logger } from "../lib/logger.server.ts";
|
||||
|
||||
export const expireSessionsJob: JobDefinition = {
|
||||
name: "expire-sessions",
|
||||
cron: "0 * * * *",
|
||||
retryLimit: 2,
|
||||
expireInSeconds: 60,
|
||||
async handler() {
|
||||
const count = await expireSessions(7);
|
||||
logger.info({ count }, "expired stale planner sessions");
|
||||
return { expiredCount: count };
|
||||
},
|
||||
};
|
||||
|
|
@ -22,6 +22,7 @@
|
|||
"@sentry/node": "catalog:",
|
||||
"@sentry/react": "catalog:",
|
||||
"@trails-cool/db": "workspace:*",
|
||||
"@trails-cool/jobs": "workspace:*",
|
||||
"@trails-cool/gpx": "workspace:*",
|
||||
"@trails-cool/i18n": "workspace:*",
|
||||
"@trails-cool/map": "workspace:*",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import { createServer, type IncomingMessage, type ServerResponse } from "node:ht
|
|||
import { createReadStream, statSync } from "node:fs";
|
||||
import { join, extname, resolve } from "node:path";
|
||||
import { setupYjsWebSocket } from "./app/lib/yjs-server.ts";
|
||||
import { createBoss, startWorker } from "@trails-cool/jobs";
|
||||
import { expireSessionsJob } from "./app/jobs/expire-sessions.ts";
|
||||
import postgres from "postgres";
|
||||
|
||||
const sentryEnvironment = process.env.CI ? "ci" : (process.env.NODE_ENV ?? "development");
|
||||
|
|
@ -113,7 +115,11 @@ const server = createServer((req, res) => {
|
|||
|
||||
setupYjsWebSocket(server);
|
||||
|
||||
server.listen(port, () => {
|
||||
server.listen(port, async () => {
|
||||
logger.info({ port }, "Planner server listening");
|
||||
logger.info({ port, path: "/sync/:sessionId" }, "Yjs WebSocket available");
|
||||
|
||||
const boss = createBoss(process.env.DATABASE_URL ?? "postgres://trails:trails@localhost:5432/trails");
|
||||
await startWorker(boss, [expireSessionsJob]);
|
||||
logger.info("Background job worker started");
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue