Skip to content

fix(samples): serialize concurrent seed runs per company - #99

Merged
Fluory merged 6 commits into
mainfrom
claude/chore-seed-lock-93
Sep 28, 2026
Merged

Fluory merged 6 commits into
mainfrom
claude/chore-seed-lock-93

Conversation

@Fluory

@Fluory Fluory commented Sep 28, 2026 •

Copy link
Copy Markdown
Owner

Warum

Fixes #93 · gefunden im Review von #92

Arbeitsstand

  • Ziel: Zwei gleichzeitige pnpm seed:samples-Läufe für dieselbe Firma stören sich nicht mehr: Jeder Musterfall entsteht genau einmal, und kein Lauf lehnt den Musterfall des anderen ab.
  • Nicht-Ziele: Sperre über Firmen hinweg; Seed aus der App heraus.
  • Erledigt: Sperre je Firma über den ganzen Lauf, Wartezeit mit klarer Meldung, Integrationstests, Runbook §6, Architekturkarte, CHANGELOG.
  • Offen: nichts – Review-Befunde umgesetzt, CI grün.
  • Annahmen: keine.
  • Nächster kleinster Schritt: Merge.

Was ist passiert (Klartext)

Die vorbereiteten Beispiel-Anfragen werden mit einem Befehl angelegt. Liefen zwei solche Befehle gleichzeitig für dieselbe Firma, konnte der zweite das Beispiel des ersten für „abgebrochen“ halten und ablehnen. Jetzt wartet der zweite Lauf, bis der erste fertig ist, und legt dann nur an, was noch fehlt. Dauert der erste länger als 20 Sekunden, bricht der zweite mit einer verständlichen Meldung ab.

Plan-Pflicht (SYSTEM.md §4)

  • Kein Auslöser – keine Modulgrenze, öffentliche API, Migration, Auth/Rechte, kein Zahlungs-/Daten-/Infrapfad, höchstens zwei Module, keine Architekturvarianten, umkehrbar
  • Auslöser zutreffend – Impact Manifest ausgefüllt (Plan vor Code)

Geändert

  • src/features/samples/repository.ts (neu): lockSeedRun, SeedRunBusy – transaktionsweite Advisory-Lock je Firma (pg_advisory_xact_lock, lock_timeout lokal)
  • src/features/samples/seed.ts, index.ts: seedSamples hält die Sperre in einer eigenen Transaktion über den ganzen Lauf; seedLockTimeoutMs (Standard 20 s – unter dem 30-s-statement_timeout von Pool und Rolle)
  • src/seed-samples.ts: Pool mit 3 statt 2 Verbindungen (eine hält die Sperre)
  • tests/integration/samples.test.ts: zwei gleichzeitige Läufe; zweiter Lauf bricht mit SeedRunBusy ab, solange der erste hält
  • docs/technical/deployment-vercel.md §6, docs/technical/architecture.md, CHANGELOG.md

Warum transaktions- statt sitzungsweit: Der Showcase verbindet über den Supabase-Pooler (Transaktionsmodus). Eine Sitzungs-Sperre hinge dort an einer beliebigen Serververbindung. Eine offene Transaktion bindet ihre Verbindung dagegen bis zum Ende und gibt die Sperre auch frei, wenn der Prozess abbricht.

Nachweis (SYSTEM.md §11)

Doku-Entscheidung (genau eine)

  • Keine langlebige Doku betroffen – Begründung: –
  • Doku betroffen und im selben PR aktualisiert:

Entferntes oder Umbenanntes: docs/ + README gegrept, Treffer bereinigt: nichts entfernt

Dateigrößen und neue Bausteine (SYSTEM.md §7)

Dateien über 500 Zeilen im Diff (Ausnahmen: generierter Code, Lockfiles, Fixtures, Migrationen, Schemas, Ressourcen, Doku, Konfiguration):

  • keine
  • bewusst belassen – Begründung: –
  • im selben PR nach fachlicher Verantwortung geteilt
  • Folge-Issue –

Über 800 Zeilen mit neuer Fachlogik oder über 1000 Zeilen (P1/P2): nicht betroffen

Neue Shared-Komponente, Utility-Datei, Adapter oder fachlicher Service:

  • nein
  • ja – gesucht nach: vorhandenen Sperren (advisory, lockDuplicateDetection, lockRequest); gefunden: lockDuplicateDetection (transaktionsweite Advisory-Lock je Firma) – dasselbe Muster, eigener Schlüssel seed-samples:<company> im samples-Modul

Subagent-Einsätze

  • 1 × frischer Review (read-only) – Ergebnis und Umsetzung im PR-Kommentar.

Risiken / offene Punkte


🤖 Generated with Claude Code

Two runs at the same time could reject each other's sample while it was
still in processing (the leftover rule of #84). A seed run now holds a
transaction-level advisory lock per company for its whole duration – safe
behind the Supabase transaction pooler, released when the run ends or its
connection drops. A second run waits and then finds the samples in place;
after a minute it stops with SeedRunBusy and a clear message.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
requestflow-ai Error Error Sep 28, 2026 5:50pm UTC

Fluory and others added 2 commits September 28, 2026 19:46
…out (#99 review)

Pool and Supabase role end every statement after 30 s, so the documented
one-minute wait never happened and the waiting run failed with a raw
statement-timeout error. The wait is now 20 s, and a statement timeout while
waiting also reads as SeedRunBusy. The seed script's pool gets a third
connection: one holds the lock for the whole run.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ext)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 28, 2026

Copy link
Copy Markdown

Deployment failed for project requestflow with the following error:

Resource is limited - try again in 24 hours (more than 100, code: "api-deployments-free-per-day").

Learn More: https://vercel.com/doc-edit?upgradeToPro=build-rate-limit

…ted next)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 28, 2026

Copy link
Copy Markdown

Deployment failed for project requestflow-ai with the following error:

Resource is limited - try again in 24 hours (more than 100, code: "api-deployments-free-per-day").

Learn More: https://vercel.com/doc-edit?upgradeToPro=build-rate-limit

@Fluory

Fluory commented Sep 28, 2026

Copy link
Copy Markdown
Owner Author

Frischer Review (unabhängiger Subagent, liest nur, Stand 5ffc926)

  1. [Important] – seed.ts (SEED_LOCK_TIMEOUT_MS = 60_000) / repository.ts:21 – lock_timeout 60 s ist wirkungslos. Der Pool setzt statement_timeout: 30_000 (src/db/client.ts:18), auf Supabase zusätzlich die Rolle app_rw (scripts/supabase-bootstrap.sql:43). Nach 30 s bricht der wartende pg_advisory_xact_lock mit 57014 (statement timeout) ab, nicht mit 55P03. – Kein SeedRunBusy, die CLI gibt die rohe Drizzle-Meldung aus. Runbook §6, CHANGELOG und Klartext („nach einer Minute“, „verständliche Meldung“) stimmen nicht, das AC „stops with a clear message“ gilt nur für Timeouts unter 30 s. Getestet sind nur 300 ms. – Standard unter 30 s (z. B. 20 s) mit Begründung im Kommentar, oder statement_timeout im Sperr-Tx per set_config(…, true) über den Sperr-Timeout heben. Doku korrigieren.
  2. [Note] – src/seed-samples.ts:21 (max: 2) – Die Sperr-Transaktion belegt jetzt dauerhaft eine der zwei Verbindungen. Heute laufen alle Schritte sequenziell mit je einer Transaktion, daher keine Erschöpfung. – Braucht ein künftiger Schritt zwei gleichzeitige Transaktionen, wartet er auf den Pool, und nach 5 s (connectionTimeoutMillis) bricht der Lauf ab. Die Tests laufen mit max: 4 und würden das nicht zeigen. – max: 2 kommentieren oder auf 3 erhöhen.
  3. [Note] – PR-Nachweis – „Ohne Sperre würde der erste Test …“ ist nur begründet, nicht vorgeführt: Der Branch hat einen Commit, einen roten Lauf gibt es nicht. Die Tests selbst sind schlüssig (sortiert und damit reihenfolgeunabhängig, das entered-Gate greift erst nach der Sperre). – Den roten Lauf im PR belegen.

Geprüft ohne Befund:

  • 55P03 wird über cause?.code ?? code erkannt (wie process-request.ts:107), CI bestätigt es.
  • Sperrschlüssel je Firma über tenantOf.
  • Kein Deadlock: keine Zeilensperren in der Sperr-Tx, anderer Schlüssel als intake-duplicates:.
  • Ein abgebrochener Lauf lässt sein ERROR-Beispiel committed (markProcessingFailed in eigener Tx).
  • Die transaktionsweite Sperre ist hinter dem Pooler sicher.
  • Modulgrenzen, Architekturkarte und Klartext vorhanden.
  • CI check grün.

Urteil: changes requested – Der voreingestellte Sperr-Timeout wird vom 30-s-statement_timeout überholt. Damit stimmen die dokumentierte Minute und die klare Meldung im Normalfall nicht, der Fix ist aber klein.


Umsetzung

# Befund Umsetzung
1 60-s-Wartezeit vom 30-s-statement_timeout überholt 055464a: Die Wartezeit ist jetzt 20 s, der Kommentar begründet das. Ein Statement-Timeout während des Wartens (57014) meldet ebenfalls SeedRunBusy, so kommt auch bei einem niedrigeren Rollen-Timeout die klare Meldung. Runbook §6 und CHANGELOG sagen jetzt „nach 20 Sekunden“. Der 57014-Pfad hat keinen eigenen Test (30 s Laufzeit), er nutzt dieselbe Erkennung wie der getestete 55P03-Pfad.
2 Seed-Pool ohne Reserve 055464a: max: 3 mit Kommentar („one connection holds the per-company seed lock“)
3 roter Lauf nur behauptet Probe-Commits 24a21b2 + 88c81bf ohne lockSeedRun. Im CI-Lauf 36461390392 (88c81bf; der erste Lauf 36460519453 scheiterte schon am Lint der Probe) scheitern genau die zwei #93-Tests: „expected [ 'pumpe-p204', 'pumpe-p204', …(2) ] to deeply equal [ 'pumpe-p204', 'werk-ost' ]“ (vier statt zwei Musterfälle) und „promise resolved … instead of rejecting“. Beide Probe-Commits sind per 145ad9e + b69bd3a zurückgenommen, der Baum ist identisch mit 055464a.

@Fluory
Fluory marked this pull request as ready for review September 28, 2026 18:04
@Fluory
Fluory merged commit 2c83323 into main Sep 28, 2026
5 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

chore(samples): serialize concurrent seed runs per company

1 participant