Fas 1a punkt 3+4: worker-härdning (DLQ, larm, multi-instance), AAMOS training export

This commit is contained in:
Sven (AAMOS AI)
2026-08-05 23:32:18 +07:00
parent d1455fb581
commit b2b6601f54
15 changed files with 9355 additions and 32 deletions
+90
View File
@@ -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." });
});
}