import type { Auftrag, Prisma } from "@/generated/prisma/client"; import type { AuftragsArt } from "@/generated/prisma/enums"; import type { AblaufErgebnis, EreignisSenke } from "@/engine/ablauf/ablauf"; import { prisma } from "@/lib/db"; import { logger } from "@/lib/logger"; import { DatenbankSenke, melden } from "@/server/ereignisse/senke"; import { reservierungenFreigeben } from "@/server/services/vmid"; /** * Führt einen Auftrag aus: Status setzen, den passenden Ablauf starten, Ergebnis festhalten. * Die Abläufe selbst (Template bauen, VM/LXC ausrollen, aufräumen) melden sich hier an. */ export type AblaufKontext = { auftrag: Auftrag; senke: EreignisSenke; abbruchGewuenscht: () => Promise; /** Merkt sich, was angelegt wurde – fürs Aufräumen, auch wenn später etwas scheitert. */ angelegt: (eintrag: Record) => Promise; }; export type AblaufAusgang = AblaufErgebnis & { ergebnis?: Record }; export type AblaufFunktion = (kontext: AblaufKontext) => Promise; const ABLAEUFE = new Map(); export function ablaufRegistrieren(art: AuftragsArt, funktion: AblaufFunktion): void { ABLAEUFE.set(art, funktion); } export async function auftragAusfuehren( auftragId: string, senke: EreignisSenke = new DatenbankSenke(auftragId), ) { // Nur einmal starten, auch wenn pg-boss einen Job doppelt liefern sollte. const gestartet = await prisma.auftrag.updateMany({ where: { id: auftragId, status: "WARTEND" }, data: { status: "LAEUFT", gestartetAm: new Date() }, }); if (gestartet.count === 0) { logger.warn({ auftragId }, "Auftrag ist nicht (mehr) wartend – übersprungen"); return; } const auftrag = await prisma.auftrag.findUniqueOrThrow({ where: { id: auftragId } }); await melden(auftragId); let ausgang: AblaufAusgang; const ablauf = ABLAEUFE.get(auftrag.art); try { if (!ablauf) throw new Error(`Für ${auftrag.art} ist kein Ablauf registriert.`); ausgang = await ablauf({ auftrag, senke, abbruchGewuenscht: async () => (await prisma.auftrag.findUnique({ where: { id: auftragId }, select: { abbrechen: true } })) ?.abbrechen ?? false, angelegt: async (eintrag) => { const aktuell = ( await prisma.auftrag.findUnique({ where: { id: auftragId }, select: { angelegt: true } }) )?.angelegt as Record | null; await prisma.auftrag.update({ where: { id: auftragId }, data: { angelegt: { ...(aktuell ?? {}), ...eintrag } as Prisma.InputJsonValue }, }); }, }); } catch (fehler) { // Fehler außerhalb einer Phase (Programmfehler) – trotzdem sauber beenden. logger.error({ err: fehler, auftragId }, "Ablauf ist abgestürzt"); const text = fehler instanceof Error ? fehler.message : String(fehler); ausgang = { ok: false, abgebrochen: false, phase: "", fehler: { ursache: `Unerwarteter Fehler: ${text}`, hinweis: "Detail-Log prüfen." }, }; await senke.log("error", `Unerwarteter Fehler: ${text}`); } await prisma.auftrag.update({ where: { id: auftragId }, data: { status: ausgang.ok ? "ERFOLGREICH" : ausgang.abgebrochen ? "ABGEBROCHEN" : "FEHLGESCHLAGEN", beendetAm: new Date(), ...(ausgang.ok ? { ergebnis: (ausgang.ergebnis ?? {}) as Prisma.InputJsonValue } : { fehler: { phase: ausgang.phase, ...ausgang.fehler } }), }, }); await reservierungenFreigeben(auftragId); await melden(auftragId); } /** * Nach einem Absturz oder Neustart des workers hängen Aufträge auf LAEUFT. Sie werden ehrlich als * fehlgeschlagen markiert – fortsetzen wäre riskanter als neu starten. */ export async function verwaisteAuftraegeBeenden(): Promise { const verwaist = await prisma.auftrag.findMany({ where: { status: "LAEUFT" }, select: { id: true } }); for (const { id } of verwaist) { await prisma.$transaction([ prisma.phase.updateMany({ where: { auftragId: id, status: "LAEUFT" }, data: { status: "FEHLER", beendetAm: new Date(), ursache: "Der worker wurde neu gestartet.", hinweis: "Auftrag wiederholen; Reste ggf. aufräumen.", }, }), prisma.auftrag.update({ where: { id }, data: { status: "FEHLGESCHLAGEN", beendetAm: new Date(), fehler: { ursache: "Der worker wurde während des Auftrags neu gestartet.", hinweis: "Auftrag wiederholen; Reste ggf. aufräumen.", }, }, }), ]); await reservierungenFreigeben(id); await melden(id); } return verwaist.length; }