Files
Wabenhain/src/lib/db-adapter.ts
T
Nick SzaunigandClaude Opus 5 9b10a9f0c5 perf: Checkout haelt gleichzeitigem Andrang stand
Ausgangsfrage: Was passiert, wenn 20 oder 100 Leute zugleich bestellen?
20 waren unproblematisch, 100 nicht. Drei Engpaesse, alle im Checkout.

1. Serialisierungskonflikte landeten ungefiltert bei der Kundschaft

Die Bestellanlage laeuft unter "Serializable". Bricht die Datenbank eine
von zwei widerspruechlichen Transaktionen ab (P2034), ist das die
VORGESEHENE Antwort auf Gleichzeitigkeit und kein Sachfehler — erwartet
wird, dass der Aufrufer es noch einmal versucht. Genau das fehlte: der
Abbruch wurde direkt als 409 durchgereicht ("bitte in ein paar Sekunden
erneut versuchen"). Weil alle Bestellungen desselben Produkts auf
dieselbe Lagerbestandszeile zugreifen, sind solche Konflikte bei Andrang
der Normalfall — ein Teil der Kundschaft haette ausgerechnet im Moment
der Kaufentscheidung eine Fehlermeldung bekommen.

Jetzt bis zu vier Versuche mit wachsender, zufaellig gestreuter
Wartezeit. Ohne den Zufallsanteil starten alle abgebrochenen Anfragen
gleichzeitig neu und kollidieren wieder. Wiederholt wird ausschliesslich
dieser Fehlerfall: "ausverkauft" und "Gutschein aufgebraucht" sind
Sachentscheidungen und faenden beim zweiten Versuch dasselbe vor.

2. Zu enger Verbindungspool, zu knappe Transaktionsfristen

Der Postgres-Adapter lief mit der Voreinstellung von node-postgres: 10
Verbindungen. Eine Bestellung haelt ihre Verbindung ueber die gesamte
Transaktion, und Prisma gibt einer Transaktion nur 2 Sekunden, um
ueberhaupt eine zu bekommen (maxWait), sowie 5 Sekunden zum Ausfuehren.
Sind alle zehn belegt, scheitern weitere Checkouts nicht an der Sache,
sondern am Warten.

Pool auf 20 (PostgreSQL erlaubt voreingestellt 100 Verbindungen, daneben
brauchen CronJob, Sicherung und Konsole noch welche; die Anwendung laeuft
mit einer Instanz). maxWait 10s, timeout 15s — Puffer fuer die
Warteschlange, nicht fuer lange Rechnungen. Dazu eine Zeitgrenze beim
Holen einer Verbindung, damit sich unter Last nichts mehr lautlos
aufstaut.

Der MariaDB-Zweig bleibt unveraendert: sein Adapter nimmt entweder die
Verbindungszeichenkette ODER ein Einstellungsobjekt, nicht beides.
Produktiv laeuft PostgreSQL.

3. Drei Regressionstests

Wiederholung fuehrt zum Erfolg, nach vier Versuchen wird aufgegeben, und
bei "ausverkauft" wird NICHT wiederholt.

tsc: 0 Fehler. vitest: 336/336 gruen (zuvor 333). eslint: 0 Warnungen.
"next build" laeuft durch.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-19 06:10:41 +02:00

62 lines
2.8 KiB
TypeScript

import { PrismaPg } from "@prisma/adapter-pg";
import { PrismaMariaDb } from "@prisma/adapter-mariadb";
/**
* Gleichzeitig offene Datenbankverbindungen dieses Prozesses.
*
* Ohne Angabe nimmt node-postgres 10. Das reicht im Alltag mühelos, wird aber
* beim Andrang zur Obergrenze: eine Bestellung haelt ihre Verbindung ueber
* die gesamte Transaktion (Bestandsreservierung, Gutscheinpruefung,
* Bestellanlage), und Prisma gibt einer Transaktion nur begrenzt Zeit,
* ueberhaupt eine Verbindung zu bekommen (maxWait). Sind alle zehn belegt,
* scheitern weitere Checkouts nicht an der Sache, sondern am Warten.
*
* 20 ist bewusst moderat: PostgreSQL erlaubt in der Voreinstellung 100
* gleichzeitige Verbindungen, und daneben brauchen noch der Aufraeum-CronJob,
* die Sicherung und gelegentlich eine Konsole welche. Die Anwendung laeuft
* ausserdem mit einer einzigen Instanz (siehe k8s/05-app-deployment.yaml) —
* bei mehreren Instanzen multipliziert sich dieser Wert und will neu bedacht
* werden.
*/
const POOL_SIZE = 20;
/**
* Wie lange eine Anfrage auf eine freie Verbindung wartet, bevor sie
* aufgibt. Lieber ein klarer Fehler nach zehn Sekunden als eine Seite, die
* ewig laedt — und im Log ein Hinweis darauf, dass der Pool zu klein ist.
*/
const POOL_ACQUIRE_TIMEOUT_MS = 10_000;
/**
* Prisma 7 liest die Verbindung nicht mehr aus dem Schema, sondern erfordert
* einen expliziten Treiber-Adapter im Client — welcher, haengt von
* DATABASE_PROVIDER ab (muss zum "provider" passen, den
* scripts/prisma-provider.mjs zuletzt in prisma/schema.prisma geschrieben
* hat). Gemeinsam genutzt von lib/prisma.ts (App-Laufzeit) und
* prisma/seed.ts (Standalone-Skript), damit beide immer denselben Provider
* verwenden.
*/
export function createDbAdapter(): PrismaPg | PrismaMariaDb {
const provider = (process.env.DATABASE_PROVIDER ?? "postgresql").trim();
const connectionString = process.env.DATABASE_URL;
if (!connectionString) throw new Error("DATABASE_URL ist nicht gesetzt.");
if (provider === "mysql") {
// Bewusst unveraendert die Verbindungszeichenkette: der MariaDB-Adapter
// nimmt entweder sie ODER ein Einstellungsobjekt aus Host, Benutzer,
// Passwort und Datenbank — beides zugleich geht nicht, die Zeichenkette
// muesste also zerlegt werden. Der Produktivbetrieb laeuft auf
// PostgreSQL (DATABASE_PROVIDER-Vorgabe); fuer MySQL gilt weiterhin die
// Voreinstellung des Treibers.
return new PrismaMariaDb(connectionString);
}
return new PrismaPg({
connectionString,
max: POOL_SIZE,
// Ohne Zeitgrenze wartet eine Anfrage unbegrenzt auf eine freie
// Verbindung. Unter Andrang staut sich dann alles lautlos auf, statt dass
// irgendwo ein Fehler sichtbar wuerde.
connectionTimeoutMillis: POOL_ACQUIRE_TIMEOUT_MS,
});
}