# Replace/Reparse-Parallelität — Analyse und Mitigation (Loop 5)

Stand: 2026-08-12 · Baut auf dem benannten Befund des Silent-Failure-Reviews
vom 11.08. auf (program-status, Loop 5): `replaceComparisonFile` patcht die
Datei-Zeiger last-write-wins ohne Compare-and-Swap; zwei gleichzeitige
Replace/Reparse-Aufrufe derselben Rolle (zwei Tabs/Nutzer) können Zeiger und
abgeleitete Zeilen verschränken. Der Ein-Klick-Reparse senkt die Hürde dafür.
Genannte Mitigation dort: „Advisory-Lock je comparisonId+role".

## Warum kein Postgres-Advisory-Lock im Wortsinn

- `pg_advisory_xact_lock` hält nur die EINE Transaktion des jeweiligen
  RPC-Aufrufs. Der Replace-Flow ist aber ein Multi-Roundtrip-Ablauf
  (Ingest → Guards → Part-Upsert → Re-Read → Zeiger-Patch → Recompare),
  keine Transaktion.
- Session-Level `pg_advisory_lock` via PostgREST scheitert am
  Connection-Pooling: das unlock trifft ggf. eine andere Session.

→ Das korrekte Primitiv ist eine **Lock-Tabelle** mit PK-Konflikt als Mutex.

## Umsetzung (dieser PR)

1. **Migration `supabase-migration-qaf-operation-lock.sql` (#121):**
   `qaf_operation_lock` mit PK `(comparison_id, role)`, `project_id` (für das
   #115-RLS-Muster, wörtlich wie #119 übernommen), `operation`, `locked_by`
   (zufällige Operations-ID — derselbe Nutzer in zwei Tabs darf sich nicht
   gegenseitig entsperren), `locked_at`.
2. **Erwerb** (`acquireOperationLock`, nicht exportiert — kein RPC):
   `upsert` mit `ignoreDuplicates` + `select` → leeres Ergebnis = vergeben.
   Dann TTL-Takeover-CAS (`update … lt(locked_at, now−15 min) … select`) für
   verwaiste Locks abgestürzter Flows; leer → ehrlicher Abbruch („läuft
   bereits"). Lock-Infrastrukturfehler brechen fail-closed mit benanntem
   Grund ab.
3. **Freigabe** im `finally` (deckt Happy-Path, jeden Guard-Abbruch mit
   compensate und jeden Wurf): `delete` mit `locked_by`-Gleichheit — nie
   fremde Locks lösen. Fehlgeschlagene Freigabe wird geloggt, nicht geworfen
   (TTL-Takeover räumt auf).
4. **Ehrlicher Degrade:** Fehlt die Tabelle (42P01/PGRST205 — Migration noch
   nicht applied), läuft der Flow wie bisher ohne Lock, mit `logger.warn`.
   Deshalb hat #121 — anders als #118/#119 — **keine
   Apply-vor-Deploy-Pflicht**; der Lock wird mit dem Apply wirksam.
5. **Position:** Erwerb NACH den billigen Guards (uuid, Zugriff, Pfad-Shape:
   Abweisungen ohne Lock-Verkehr), VOR dem teuren, verschränkbaren Teil.
   Der Reparse-Wrapper erbt den Lock über seinen inneren
   `replaceComparisonFile`-Aufruf (operation `reparse_file`).

## Bewusst NICHT in diesem Schritt

- **Recompare-/Pins-Parallelität:** eigener, bestehender Kanal
  (KAR-899-Design, drei UI-Aufrufstellen) — nicht Replace-spezifisch,
  eigene Betrachtung nötig.
- **CAS auf dem Zeiger-Patch als Zusatzverteidigung:** nach dem Lock
  redundant für den adressierten Fall; erst bewerten, wenn die
  Recompare-Parallelität ansteht (Explicit > clever, engineered enough).
- **UI-Anzeige des Lock-Zustands** (Button-Disable bei laufender Operation):
  Ausbaustufe; der ehrliche Abbruchtext deckt den Konflikt ab.

## Restrisiko (benannt)

- Zwischen Ablauf des TTL (15 min) und Ende eines tatsächlich noch laufenden,
  nur langsamen Flows kann ein Konkurrent übernehmen — das Fenster ist klein
  und der Zustand nicht schlechter als heute (ohne Lock). TTL bewusst
  konservativ über der beobachteten Flow-Dauer (Sekunden bis ~1 min).
- Bis zum Apply von #121 bleibt das heutige Verhalten bestehen (Degrade-Pfad,
  getestet).
