Fas 1a punkt 3+4: worker-härdning (DLQ, larm, multi-instance), AAMOS training export
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { DEAD_LETTER_QUEUE_NAME, JOB_QUEUE_NAME } from "@app/shared-types";
|
||||
import { Queue, Job } from "bullmq";
|
||||
|
||||
interface JobSummary {
|
||||
id: string | undefined;
|
||||
name: string;
|
||||
data: unknown;
|
||||
failedReason: string | undefined;
|
||||
attemptsMade: number;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export async function adminWorkersRoutes(app: FastifyInstance) {
|
||||
const admin = { preHandler: [app.requireAdmin] };
|
||||
const dlq = new Queue(DEAD_LETTER_QUEUE_NAME, { connection: app.redis });
|
||||
|
||||
app.addHook("onClose", async () => {
|
||||
await dlq.close();
|
||||
});
|
||||
|
||||
app.get("/admin/v1/workers/health", admin, async (_req, reply) => {
|
||||
const queue = new Queue(JOB_QUEUE_NAME, { connection: app.redis });
|
||||
const [jobCounts, dlqCounts, workers, schedulers] = await Promise.all([
|
||||
queue.getJobCounts("wait", "active", "delayed", "completed", "failed"),
|
||||
dlq.getJobCounts("wait", "active", "completed", "failed"),
|
||||
queue.getWorkers(),
|
||||
queue.getJobSchedulers(),
|
||||
]);
|
||||
await queue.close();
|
||||
|
||||
return reply.send({
|
||||
ok: true,
|
||||
queues: {
|
||||
jobs: jobCounts,
|
||||
deadLetter: dlqCounts,
|
||||
},
|
||||
workers: workers.map((w) => ({
|
||||
id: w.id,
|
||||
addr: w.addr,
|
||||
concurrency: w.concurrency,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
lastSeenAt: (w as any).ts,
|
||||
})),
|
||||
schedulers: schedulers.map((s) => ({ id: s.id, pattern: s.pattern, every: s.every })),
|
||||
});
|
||||
});
|
||||
|
||||
app.get("/admin/v1/workers/dead-letter", admin, async (req, reply) => {
|
||||
const limit = Math.min(Number((req.query as { limit?: string }).limit ?? 50), 200);
|
||||
const jobs = await dlq.getJobs(["wait", "failed"], 0, limit - 1, true);
|
||||
const summary: JobSummary[] = jobs.map((job) => ({
|
||||
id: job.id,
|
||||
name: job.name,
|
||||
data: job.data,
|
||||
failedReason: job.failedReason,
|
||||
attemptsMade: job.attemptsMade,
|
||||
timestamp: job.timestamp,
|
||||
}));
|
||||
return reply.send({ jobs: summary });
|
||||
});
|
||||
|
||||
app.post("/admin/v1/workers/dead-letter/:id/retry", admin, async (req, reply) => {
|
||||
const { id } = req.params as { id: string };
|
||||
const job = await Job.fromId(dlq, id);
|
||||
if (!job) {
|
||||
return reply.status(404).send({ error: { code: "NOT_FOUND", message: "Jobbet finns inte i dead letter queue." } });
|
||||
}
|
||||
|
||||
const mainQueue = new Queue(JOB_QUEUE_NAME, { connection: app.redis });
|
||||
await mainQueue.add(job.name, job.data, {
|
||||
attempts: 3,
|
||||
backoff: { type: "exponential", delay: 2_000 },
|
||||
});
|
||||
await job.remove();
|
||||
await mainQueue.close();
|
||||
|
||||
return reply.send({ ok: true, message: "Jobbet har lagts tillbaka i kön." });
|
||||
});
|
||||
|
||||
app.post("/admin/v1/workers/dead-letter/:id/discard", admin, async (req, reply) => {
|
||||
const { id } = req.params as { id: string };
|
||||
const job = await Job.fromId(dlq, id);
|
||||
if (!job) {
|
||||
return reply.status(404).send({ error: { code: "NOT_FOUND", message: "Jobbet finns inte i dead letter queue." } });
|
||||
}
|
||||
await job.remove();
|
||||
return reply.send({ ok: true, message: "Jobbet har tagits bort från dead letter queue." });
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user