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]>
Die beiden Dateien, in denen Geld und Lagerbestand zusammenlaufen, hatten
bisher keinen einzigen Test. Dazu meldete die Go-Live-Pruefung gruene
Haken fuer Werte, die gar nicht gesetzt waren.
Tests fuer api/checkout/route.ts (10 Faelle)
Torwaechter (fremder Origin, Turnstile, Rate-Limit), deaktiviertes
Produkt, Preis 0, ausverkaufte Ware, Sitzungslebensdauer — und die
beiden Regressionen: scheitert die Stripe-Sitzung, wird die Bestellung
aufgeloest und OHNE orderNumber geantwortet; laesst sich die
Sitzungs-ID nicht speichern, wird die Sitzung geschlossen. Per
Mutationstest geprueft, dass der zweite Fall ohne den Fix rot wird.
Tests fuer api/checkout/webhook/route.ts (12 Faelle)
Signaturpruefung, verzoegerte Zahlung (SEPA), Auslieferung samt
Rechnung und Anhang, mehrfach zugestelltes Ereignis (Gutschein und
Rechnungsnummer duerfen sich nicht wiederholen), Zahlung auf eine
stornierte Bestellung, Betragsabweichung, abgelaufene Sitzung,
fehlgeschlagener Einzug, volle und teilweise Erstattung, Rueckbuchung.
Datenbank und Stripe sind in beiden Dateien vollstaendig ersetzt.
Go-Live-Pruefung: falsch-positive Haken beseitigt
Regeln, die einen Wert voraussetzen, bestanden bei fehlendem Wert
einfach — eine leere Zeichenkette enthaelt nun einmal kein
"example.com". Der Bericht meldete deshalb "NEXT_PUBLIC_SITE_URL ist
die echte Domain" als bestanden, direkt neben dem Blocker
"NEXT_PUBLIC_SITE_URL gesetzt". Ohne jeden Stripe-Schluessel galt
sogar der Live-Modus als bestaetigt. Das ist die gefaehrlichste
Fehlmeldung, die eine solche Pruefung machen kann: sie beruhigt ueber
genau den Punkt, der offen ist. mustWhenSet/shouldWhenSet lassen
Folgeregeln bei fehlendem Wert entfallen.
Ausserdem sagt der Bericht jetzt ausdruecklich, dass er die Umgebung
prueft, in der er LAEUFT. Lokal meldet er Blocker, die in der
ConfigMap des Clusters laengst erledigt sind; wer das ein paarmal
erlebt, liest ueber den Bericht hinweg. Das README nennt den Aufruf
im Pod.
tsc: 0 Fehler. vitest: 316/316 gruen (zuvor 294). eslint: 0 Warnungen.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>