Audit-Kette: Race beim Fortschreiben behoben (parallele Requests)

createAuditLog las den Vorgaenger-Hash und schrieb den neuen Eintrag als zwei
getrennte Schritte. Zwei parallele Requests lasen denselben letzten Hash und
haengten sich beide daran - die Kette zerriss (Bruchstellen im Bestand vom
05.05. und 07.05.2026).

Fix: Lesen + Schreiben in einer Transaktion, serialisiert ueber einen benannten
MySQL-Lock (GET_LOCK). Der Lock liegt in der DB und wirkt daher auch ueber
mehrere App-Instanzen hinweg. Release im finally, weil benannte Locks nicht
transaktional sind - sonst wandert die Sperre mit der Verbindung zurueck in den
Pool und blockiert alle weiteren Schreiber.

Verworfener erster Ansatz: SELECT ... FOR UPDATE auf das Kettenende nimmt Gap-/
Next-Key-Locks, die mit den gleichzeitigen INSERTs kollidieren - gemessen gingen
38 von 40 parallelen Eintraegen durch Deadlocks verloren, still verschluckt vom
catch. Ein fehlender Audit-Eintrag ist unsichtbar und damit gefaehrlicher als
ein sichtbarer Kettenbruch.

Verifiziert: 100 parallele Schreiber -> 100/100 geschrieben, 0 neue Brueche
(444 ms); Folge-Schreiber in 6 ms, IS_FREE_LOCK frei (kein Lock-Leak).
Ungueltige Zeilen bleiben bei den 7 historischen. tsc gruen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 09:37:34 +02:00
co-authored by Claude Opus 5
parent 89ae7b73a1
commit 477a850a91
2 changed files with 102 additions and 49 deletions
+22
View File
@@ -97,6 +97,28 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
## ✅ Erledigt
- [x] **🔗 Audit-Kette: Race beim Fortschreiben behoben (parallele Requests)** (2026-08-18)
- `createAuditLog` las den Vorgaenger-Hash und schrieb den neuen Eintrag als
zwei getrennte Schritte. Zwei parallele Requests lasen denselben letzten
Hash und haengten sich beide daran → Kette zerrissen (echte Bruchstellen im
Bestand: 05.05./07.05.2026).
- Fix: Lesen + Schreiben in einer Transaktion, serialisiert ueber einen
benannten MySQL-Lock (`GET_LOCK('opencrm_audit_chain')`). Liegt in der DB,
wirkt daher auch ueber mehrere App-Instanzen hinweg. Release im `finally`,
da benannte Locks nicht transaktional sind (sonst wandert die Sperre mit der
Verbindung zurueck in den Pool und blockiert alle Schreiber).
- **Verworfener erster Ansatz wichtig:** `SELECT … FOR UPDATE` auf das
Kettenende nimmt Gap-/Next-Key-Locks, die mit den gleichzeitigen INSERTs
kollidieren. Gemessen: **38 von 40** parallelen Eintraegen gingen durch
Deadlocks verloren (vom `catch` still verschluckt). Ein FEHLENDER
Audit-Eintrag ist unsichtbar und damit gefaehrlicher als ein sichtbarer
Kettenbruch deshalb der Umbau auf den benannten Lock.
- Verifiziert: 100 parallele Schreiber → **100/100 geschrieben, 0 neue
Brueche** (444 ms); Folge-Schreiber danach in 6 ms, `IS_FREE_LOCK` = frei
(kein Lock-Leak). Gesamtzahl ungueltiger Zeilen bleibt bei den 7
historischen. Rechenintensives (Serialisieren/Verschluesseln) liegt bewusst
VOR der Transaktion, damit die Sperre kurz bleibt. `tsc` gruen.
- [x] **🛡️ Audit-Integritaet: Dauer-Fehlalarm ueber 67 % des Logs behoben** (2026-08-18)
- Beim Nachpruefen aufgefallen: `verifyIntegrity` meldete **3107 von 4630**
Zeilen als „manipuliert“. Davon waren **3100 Fehlalarme** eingegrenzt auf