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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user