Fix archive reindex and add target dedup

This commit is contained in:
DonVoo 2026-07-14 20:57:09 +02:00
parent e612896e06
commit c83225f5ad
9 changed files with 796 additions and 6 deletions

165
korrektur-brief.md Normal file
View file

@ -0,0 +1,165 @@
# Codex-Brief — Korrekturen aus dem zstd-Messlauf
Vier Befunde aus meinem Messlauf (14.07.). Reihenfolge = Priorität.
**1 und 2 sind Pflicht vor der ersten echten `@dr-gold.de`-Migration.**
---
## 1. 🔴 REGRESSION: Alte plain-Archive sind im Viewer unsichtbar
**Befund:** Die Nachrichtenliste kommt jetzt aus `mbox_index`. Archive, die
**vor** dem zstd-Deploy geschrieben wurden, haben keine Index-Zeilen:
```
mbox_index: codex-abnahme.../INBOX -> 1 Zeile (Datei enthaelt 207 Mails)
Viewer zeigt: "INBOX (1)"
```
Das Perfide: Es zeigt **„(1)" statt eines Fehlers**. Ein halb-wahres Ergebnis ist
schlimmer als gar keins — es sieht aus, als funktioniere es. Der im zstd-Brief
geforderte Fallback auf den alten Parse-Pfad fehlt.
**Fix — die mbox ist append-only, also reicht ein Fortschritts-Marker pro Datei:**
```sql
CREATE TABLE IF NOT EXISTS mbox_index_state(
account_id INTEGER NOT NULL,
folder TEXT NOT NULL,
indexed_bytes INTEGER NOT NULL DEFAULT 0, -- bis hierhin ist die Datei indiziert
UNIQUE(account_id, folder)
);
```
- **Nachindizieren:** Wenn `dateigroesse > indexed_bytes` → ab `indexed_bytes`
bis EOF scannen, Index-Zeilen anhaengen, `indexed_bytes` fortschreiben.
Deckt **leeren** und **teilweisen** Index gleichermassen ab.
- Der **Migrationspfad** schreibt `indexed_bytes` beim Append mit fort — dann
muss im Normalbetrieb nie nachgescannt werden.
- **CLI `--reindex <konto|all>`** fuer den Bestand.
- Der Scanner muss **beide** Formate koennen: plain (`From `-Trennzeilen) und
`.zst` (Frame fuer Frame entpacken, dann innerhalb des Frames an den
`From `-Trennzeilen splitten). Das ist genau die Logik, die Punkt 4 spaeter
auch fuer Batch-Frames braucht — einmal richtig bauen.
- **Nie wieder still halbe Wahrheiten:** Wenn `dateigroesse != indexed_bytes` und
nicht nachindiziert werden kann → **Fehler zeigen**, keine gekuerzte Liste.
---
## 2. 🔴 Ziel-Dedup-Werkzeug (die Feuerwehr)
**Befund:** In `vdevop-03/INBOX` existieren **199 Message-IDs doppelt** — die
INBOX wurde irgendwann komplett ein zweites Mal migriert. Niemand hat es gemerkt,
weil im Log nur `copied=199` stand und wie normaler Fortschritt aussah.
Die *Ursachen* sind inzwischen dicht (Message-ID-Normalisierung, DB-Guard,
Watch-Fail-Fast). **Aber es gibt kein Werkzeug, das Dubletten im Ziel findet oder
repariert.** Wenn das bei einem echten dr-gold-Postfach passiert, gibt es keinen
Ausweg — und das merkt man erst, wenn es zu spaet ist.
**Fix:** `--dedup-target <konto> [--folders ...] [--apply]` (+ Admin-Route).
- Ordner scannen, **header-only** (`Headers()`-Pfad, keine Bodies!), nach
normalisierter Message-ID gruppieren.
- Pro Gruppe mit >1 Kopie: **die niedrigste UID behalten** (die aelteste, zuerst
eingespielte), den Rest `\Deleted` + `EXPUNGE`.
**Sicherheitsregeln — das Ding loescht fremde Mail, ein Bug hier ist schlimmer
als die Dubletten:**
1. **Dry-Run ist Default.** Ohne `--apply` wird nur berichtet: pro Ordner, wie
viele Gruppen, wie viele ueberzaehlige Kopien, welche UIDs.
2. **Nie die letzte Kopie loeschen.** Pro Message-ID bleibt immer genau eine.
3. **Mails ohne Message-ID nie anfassen** — nicht sicher dedupliizierbar.
Separat auszaehlen und berichten.
4. **Jede Loeschung protokollieren** (Konto, Ordner, UID, Message-ID) — es
braucht eine Beweiskette.
5. Nur `requireAdmin`.
---
## 3. 🟡 `folder_map` ist toter Code
Die Tabelle wird angelegt, aber **nirgends gelesen**:
[`07-migrate.go:122`](backend/07-migrate.go) uebergibt den Override hart als `""`:
```go
dstFolder := MapSourceToTarget(folder.Name, folder.Attrs, folder.Delim, dst.Delim(), targets, "")
// ^^ immer leer
```
`MapSourceToTarget` hat den Parameter, `11-folders.go` dokumentiert „manuelles
folder_map hat Vorrang" — es tut es nur nicht. Ein dokumentiertes Feature, das
stillschweigend nichts tut. (Genau daran ist mein Messlauf haengengeblieben: die
Umleitung wurde ignoriert und 207 Mails landeten im echten Ziel-Ordner.)
**Fix:** `folder_map` pro Konto laden und als `override` durchreichen. Plus
Pflege in der UI (Konten-Formular: Quell-Ordner → Ziel-Ordner).
---
## 4. 🟢 Kompressionsrate — gemessen, und die Entscheidung dazu
| | Groesse | Faktor |
|---|---|---|
| plain mbox (207 Mails) | 4.372.002 | — |
| **zstd, Frame pro Mail (heute)** | **1.470.412** | **2,97×** |
| zstd, ganze Datei am Stueck | 113.564 | 38,50× |
Der Abstand ist die **Cross-Message-Redundanz**, die Frame-pro-Mail wegwirft.
(Mein Korpus uebertreibt ihn — 199 fast identische Benachrichtigungsmails. Auf
echter Mail waere die Luecke kleiner, aber real.)
**Was wir NICHT machen: zstd-Dictionary.** Das waere technisch der eleganteste
Weg (genau dafuer sind Dictionaries da), aber es macht das Archiv **abhaengig von
einer externen Datei**: Dictionary weg → Archiv unlesbar. Fuer ein Beweis-Archiv,
das **610 Jahre** halten und im Streitfall herausgegeben werden muss, ist
`zstd -d` funktioniert einfach, ohne Beiwerk" mehr wert als jeder
Kompressionsfaktor. **Selbstgenuegsamkeit schlaegt Rate.**
**Was wir machen koennen: Batch-Frames mit kleinem Deckel (2 MB).**
Der Index (`inner_offset`/`inner_len`) ist bereits dafuer gebaut.
**Aber die Durability-Regel ist dabei nicht verhandelbar:**
Bei Batching ist eine Mail nach `mbox.Append` **noch nicht auf der Platte** — sie
liegt im Frame-Puffer. Deshalb:
```
Frame voll (oder Ordner zu Ende)
-> Frame komprimieren
-> KOMPLETTEN Frame an die Datei anhaengen + fsync
-> DANN erst: Index-Zeilen schreiben
-> DANN erst: MarkCopied fuer alle Mails des Batches (eine Transaktion)
```
`MarkCopied` darf **niemals** vor dem fsync passieren — sonst gilt eine Mail als
gesichert, die nie im Archiv ankam. Das waere der schlimmstmoegliche Bug fuer ein
Beweis-Archiv.
Nebenwirkung, die man kennen muss: Ein Absturz mitten im Batch laesst bis zu
2 MB Mails unmarkiert → sie werden beim naechsten Lauf erneut ins **IMAP-Ziel**
kopiert → Dubletten dort. Dasselbe Fenster wie heute, nur groesser (Batch statt
Einzelmail). **Deshalb der kleine Deckel (2 MB, nicht 8) — und deshalb ist
Punkt 2 (Dedup-Werkzeug) die Voraussetzung dafuer.**
Priorität: **niedriger als 13.** 2,97× ist bereits ein Gewinn, und die Vorschau
wurde durch den Index sogar **6× schneller** (1,8 ms statt 10,8 ms). Korrektheit
zuerst.
---
## Abnahme
1. `go test ./...` gruen.
2. **Reindex:** `--reindex all` → der Viewer zeigt fuer das bestehende
plain-Archiv wieder **alle 207** Mails (statt „(1)"). Liste und Oeffnen
funktionieren fuer plain **und** `.zst`.
3. **Dedup, Dry-Run:** `--dedup-target codex-abnahme-vdevop02-to-vdevop03`
meldet fuer `INBOX` **199 Gruppen mit je einer ueberzaehligen Kopie**
(das ist der bekannte, echte Bestand — ein guter Selbsttest).
Mit `--apply` bleiben danach genau 365 eindeutige Mails uebrig.
**Ohne `--apply` darf sich nichts aendern.**
4. **folder_map:** Ein Eintrag `INBOX → TEST-ZIEL` fuehrt im Log zu
`INBOX -> TEST-ZIEL` (heute: `INBOX -> INBOX`).
5. **Round-trip bleibt byte-identisch** — auch nach jeder Aenderung an der
Frame-Struktur: `zstd -d x.mbox.zst` == plain-mbox, `cmp`-sauber.
Das ist die Linie, die nie fallen darf.
Die Messungen (Rate, Vorschau-Latenz, Round-trip) fahre ich danach erneut.