feat(worker): Worker mit pg-boss, Vorgaenge und Docker-Betrieb (#007)
Die Oberflaeche legt einen Vorgang an und schickt nur dessen ID in die Warteschlange; der Worker fuehrt ihn aus und haelt Status, Ergebnis oder Fehler fest. So bleiben lange SSH-Vorgaenge aus dem Webprozess heraus, und der Fortschritt ist ohne Job-Interna sichtbar. Kein automatisches Wiederholen: ein halb ausgefuehrter Schreibvorgang auf einem Host gehoert vor einen Menschen. Fehlertexte aus fremden Bibliotheken erreichen die Oberflaeche nie. Dockerfile und Compose fuer den Betrieb: Master-Key als Secret nur im Worker, Datenbank in einem internen Netz. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
import { afterAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { vorgangEinreihen, vorgangLesen } from "@/server/vorgaenge/vorgaenge";
|
||||
import { WARTESCHLANGE, warteschlangeHolen, type VorgangsJob } from "@/server/vorgaenge/warteschlange";
|
||||
import { VorgangsFehler, vorgangAusfuehren, type HandlerTabelle } from "@/worker/ausfuehren";
|
||||
import { datenbankLeeren } from "./hilfen/db";
|
||||
|
||||
/** Vorgaenge zwischen Web und Worker (#007). */
|
||||
|
||||
beforeEach(async () => {
|
||||
await datenbankLeeren();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
const boss = await warteschlangeHolen();
|
||||
await boss.stop({ graceful: false });
|
||||
});
|
||||
|
||||
async function anlegen() {
|
||||
return prisma.vorgang.create({ data: { art: "ERREICHBARKEIT" } });
|
||||
}
|
||||
|
||||
describe("Vorgang ausfuehren", () => {
|
||||
it("haelt Ergebnis und Status fest", async () => {
|
||||
const vorgang = await anlegen();
|
||||
const handler: HandlerTabelle = { ERREICHBARKEIT: async () => ({ ergebnis: { erreichbar: true } }) };
|
||||
await vorgangAusfuehren(vorgang.id, handler);
|
||||
const fertig = await vorgangLesen(vorgang.id);
|
||||
expect(fertig?.status).toBe("ERLEDIGT");
|
||||
expect(fertig?.ergebnis).toEqual({ erreichbar: true });
|
||||
expect(fertig?.beendetAm).not.toBeNull();
|
||||
});
|
||||
|
||||
it("zeigt nur Meldungen von VorgangsFehlern, alles andere allgemein", async () => {
|
||||
const a = await anlegen();
|
||||
await vorgangAusfuehren(a.id, {
|
||||
ERREICHBARKEIT: async () => {
|
||||
throw new VorgangsFehler("Host antwortet nicht.");
|
||||
},
|
||||
});
|
||||
expect((await vorgangLesen(a.id))?.fehler).toBe("Host antwortet nicht.");
|
||||
|
||||
const b = await anlegen();
|
||||
await vorgangAusfuehren(b.id, {
|
||||
ERREICHBARKEIT: async () => {
|
||||
throw new Error("-----BEGIN OPENSSH PRIVATE KEY----- geheim");
|
||||
},
|
||||
});
|
||||
const fehler = (await vorgangLesen(b.id))?.fehler ?? "";
|
||||
expect(fehler).not.toContain("PRIVATE KEY");
|
||||
expect(fehler).toMatch(/Interner Fehler/);
|
||||
});
|
||||
|
||||
it("startet einen Vorgang nur einmal", async () => {
|
||||
const vorgang = await anlegen();
|
||||
let aufrufe = 0;
|
||||
const handler: HandlerTabelle = {
|
||||
ERREICHBARKEIT: async () => {
|
||||
aufrufe += 1;
|
||||
return {};
|
||||
},
|
||||
};
|
||||
await Promise.all([vorgangAusfuehren(vorgang.id, handler), vorgangAusfuehren(vorgang.id, handler)]);
|
||||
expect(aufrufe).toBe(1);
|
||||
});
|
||||
|
||||
it("meldet fehlende Handler verstaendlich", async () => {
|
||||
const vorgang = await anlegen();
|
||||
await vorgangAusfuehren(vorgang.id, {});
|
||||
expect((await vorgangLesen(vorgang.id))?.fehler).toMatch(/noch keine Ausführung/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Warteschlange", () => {
|
||||
it("bringt einen eingereihten Vorgang bis in den Worker", async () => {
|
||||
const boss = await warteschlangeHolen();
|
||||
const id = await vorgangEinreihen({
|
||||
art: "ERREICHBARKEIT",
|
||||
angefordertVonId: null,
|
||||
eingabe: { probe: 1 },
|
||||
});
|
||||
// Die Warteschlange ueberlebt das Leeren der Tabellen; deshalb gezielt den eigenen Job suchen.
|
||||
const jobs = await boss.fetch<VorgangsJob>(WARTESCHLANGE, { batchSize: 100 });
|
||||
const job = jobs.find((kandidat) => kandidat.data.vorgangId === id);
|
||||
expect(job?.data).toEqual({ vorgangId: id });
|
||||
await vorgangAusfuehren(job!.data.vorgangId, { ERREICHBARKEIT: async () => ({ ergebnis: "ok" }) });
|
||||
await boss.complete(WARTESCHLANGE, job!.id);
|
||||
expect((await vorgangLesen(id))?.status).toBe("ERLEDIGT");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user