Initial: Tibi-Doku auf Starlight-Basis (Nova-Theme, Docker-Stack, Sync-Skripte)
This commit is contained in:
commit
58502aa3c8
78 files changed
+22860
No files matched your search
@@ -0,0 +1,534 @@
|
||||
---
|
||||
title: 'Backup & Restore'
|
||||
---
|
||||
|
||||
# Backup & Restore
|
||||
|
||||
## Overview
|
||||
|
||||
Die Backup & Restore-API ermöglicht das Exportieren eines kompletten Projekts (Collections + Media)
|
||||
als ZIP-Datei sowie den Import auf dem gleichen oder einem anderen Server.
|
||||
|
||||
**Nur Admin-User** können diese Endpunkte nutzen (Server-Token, Projekt-Token mit `allowBackup`,
|
||||
oder JWT mit AdminRole).
|
||||
|
||||
---
|
||||
|
||||
## Authentifizierung
|
||||
|
||||
Dreistufiges Auth-Modell (identisch mit anderen Admin-Endpunkten wie `reload`):
|
||||
|
||||
### 1. Server-Level Admin Token
|
||||
|
||||
```yaml
|
||||
api:
|
||||
adminTokens:
|
||||
- token: "backup-agent-token"
|
||||
label: "ci-backup"
|
||||
permissions:
|
||||
- project.backup # Nur Backup/Restore
|
||||
# - project.* # Alle Projekt-Operationen inkl. Backup
|
||||
```
|
||||
|
||||
Gesendet via `X-Admin-Token` Header oder `Authorization: Bearer <token>`.
|
||||
|
||||
### 2. Projekt-Level Admin Token
|
||||
|
||||
```yaml
|
||||
# In der Projekt-Config (config.yml)
|
||||
admin:
|
||||
tokens:
|
||||
- token: "ADMINTOKEN"
|
||||
allowReload: true
|
||||
allowRegenerateSearch: true
|
||||
allowBackup: true # NEU: erlaubt Backup/Restore
|
||||
```
|
||||
|
||||
Das Feld `allowBackup` steuert gezielt, ob dieser Token Backup/Restore verwenden darf.
|
||||
|
||||
### 3. JWT Login mit AdminRole
|
||||
|
||||
User mit `role: 0` (AdminRole) können Backup/Restore ausführen.
|
||||
|
||||
---
|
||||
|
||||
## Endpunkte
|
||||
|
||||
| Methode | Pfad | Beschreibung |
|
||||
|--------|------|-------------|
|
||||
| **GET** | `/_/:project/_/admin/backup` | Backup als ZIP herunterladen |
|
||||
| **GET** | `/_/:project/_/admin/backup/download/:file` | Temp-ZIP aus SSE-Download abholen |
|
||||
| **POST** | `/_/:project/_/admin/backup` | ZIP hochladen & analysieren → `restoreToken` (SSE via `?sse=1`) |
|
||||
| **POST** | `/_/:project/_/admin/backup/:restoreToken` | Wiederherstellung ausführen |
|
||||
| **DELETE** | `/_/:project/_/admin/backup/:restoreToken` | Temp-Daten manuell aufräumen |
|
||||
|
||||
---
|
||||
|
||||
## 1. Backup Download
|
||||
|
||||
```text
|
||||
GET /_/:project/_/admin/backup?collections=contacts,pages&sse=1
|
||||
```
|
||||
|
||||
| Query-Param | Default | Beschreibung |
|
||||
|-------------|---------|-------------|
|
||||
| `collections` | _(alle)_ | Komma-separierte Collection-Namen (selektiver Export) |
|
||||
| `sse` | `0` | `1` = Fortschritt als SSE-Stream, `0` = direktes ZIP |
|
||||
|
||||
### Direkter Download (`sse=0`)
|
||||
|
||||
Response: `Content-Type: application/zip` mit folgender Struktur:
|
||||
|
||||
```
|
||||
backup-<namespace>-<timestamp>.zip
|
||||
├── backup-info.json
|
||||
├── collections/
|
||||
│ ├── <collection>.json # Array aller Dokumente
|
||||
│ └── ...
|
||||
└── media/ # Nur Dateien im konfigurierten uploadPath
|
||||
└── <collection>/
|
||||
└── <docId>/
|
||||
└── <fieldName>/
|
||||
└── <filename>
|
||||
```
|
||||
|
||||
**Wichtig:** Es werden NUR Media-Dateien exportiert, die tatsächlich im konfigurierten
|
||||
`uploadPath` der Collection existieren. Fehlende Dateien werden übersprungen und geloggt.
|
||||
|
||||
Die Datei `backup-info.json` enthält Metadaten, die beim Import zur Schema-Validierung
|
||||
verwendet werden:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "tibi-backup",
|
||||
"version": 1,
|
||||
"sourceNamespace": "mein-projekt",
|
||||
"createdAt": "2026-06-24T12:00:00Z",
|
||||
"collections": {
|
||||
"contacts": {
|
||||
"count": 42,
|
||||
"fields": ["email", "firstname", "lastname", "phone"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### SSE-Modus (`sse=1`)
|
||||
|
||||
Response-Type: `text/event-stream` mit folgenden Events:
|
||||
|
||||
```
|
||||
event: progress
|
||||
data: {"phase":"init","totalCollections":3}
|
||||
|
||||
event: result
|
||||
data: {"phase":"done","downloadUrl":"/_/:project/_/admin/backup/download/<token>","size":52428800,"expiresAt":"..."}
|
||||
```
|
||||
|
||||
Der Client erhält eine temporäre Download-URL (TTL: 5 Minuten) und ruft die ZIP
|
||||
anschließend separat ab:
|
||||
|
||||
```bash
|
||||
# Schritt 1: SSE-Download initiieren
|
||||
curl -H "X-Admin-Token: $TOKEN" \
|
||||
"http://localhost:8080/api/v1/_/myproject/_/admin/backup?sse=1"
|
||||
|
||||
# Schritt 2: ZIP vom temp-Link abholen
|
||||
curl -H "X-Admin-Token: $TOKEN" \
|
||||
-o backup.zip \
|
||||
"http://localhost:8080/api/v1/_/myproject/_/admin/backup/download/<token>"
|
||||
```
|
||||
|
||||
Der Download-Link ist **One-Time-Use** — ein zweiter Request erhält 404.
|
||||
|
||||
---
|
||||
|
||||
## 2. Backup Upload & Analyse (Phase 1)
|
||||
|
||||
```text
|
||||
POST /_/:project/_/admin/backup
|
||||
Content-Type: multipart/form-data
|
||||
```
|
||||
|
||||
**Body:** Feld `file` = backup.zip
|
||||
|
||||
| Query-Param | Default | Beschreibung |
|
||||
|-------------|---------|-------------|
|
||||
| `sse` | `0` | `1` = Fortschritt via SSE streamen |
|
||||
|
||||
### Direkter Modus (`sse=0`)
|
||||
|
||||
**Response (200):**
|
||||
|
||||
```json
|
||||
{
|
||||
"restoreToken": "a1b2c3d4e5f6...",
|
||||
"sourceNamespace": "quelle-projekt",
|
||||
"createdAt": "2026-06-24T12:00:00Z",
|
||||
"expiresAt": "2026-06-24T13:00:00Z",
|
||||
"collections": {
|
||||
"contacts": {
|
||||
"documentCount": 42,
|
||||
"mediaFileCount": 5,
|
||||
"backupFields": ["email","firstname","lastname","phone"],
|
||||
"targetFields": ["email","firstname","lastname","phone","department"],
|
||||
"warnings": [
|
||||
"target has new fields not in backup: [department]"
|
||||
],
|
||||
"exists": true
|
||||
},
|
||||
"legacy_old": {
|
||||
"documentCount": 7,
|
||||
"mediaFileCount": 0,
|
||||
"backupFields": ["data"],
|
||||
"targetFields": null,
|
||||
"warnings": ["collection \"legacy_old\" does not exist in target project"],
|
||||
"exists": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**In dieser Phase werden KEINE Daten geschrieben.** Das ZIP wird nur entpackt und analysiert.
|
||||
Der `restoreToken` referenziert das Temp-Verzeichnis für die nächste Phase.
|
||||
|
||||
### SSE-Modus (`sse=1`)
|
||||
|
||||
Response-Type: `text/event-stream` mit folgenden Events:
|
||||
|
||||
```
|
||||
event: progress
|
||||
data: {"phase":"init","restoreToken":"a1b2c3d4e5f6..."}
|
||||
|
||||
event: progress
|
||||
data: {"phase":"save"}
|
||||
|
||||
event: progress
|
||||
data: {"phase":"extract"}
|
||||
|
||||
event: progress
|
||||
data: {"phase":"validate"}
|
||||
|
||||
event: progress
|
||||
data: {"phase":"analyze","totalCollections":2}
|
||||
|
||||
event: result
|
||||
data: {"phase":"done","restoreToken":"a1b2c3d4e5f6...","sourceNamespace":"quelle-projekt","createdAt":"2026-06-24T12:00:00Z","expiresAt":"2026-06-24T13:00:00Z","collections":{...}}
|
||||
```
|
||||
|
||||
### Schema-Kompatibilitäts-Prüfung
|
||||
|
||||
Die Prüfung ist **tolerant** — sie warnt, blockiert aber nicht:
|
||||
|
||||
| Situation | Verhalten |
|
||||
|-----------|-----------|
|
||||
| Backup & Ziel identisch | Kein Warning |
|
||||
| Backup hat Felder, Ziel nicht | Warning (MongoDB speichert sie trotzdem) |
|
||||
| Ziel hat Felder, Backup nicht | Warning (Dokumente bekommen null/leer) |
|
||||
| Collection existiert nicht im Ziel | `exists: false`, wird beim Restore geskippt |
|
||||
|
||||
---
|
||||
|
||||
## 3. Restore ausführen (Phase 2)
|
||||
|
||||
```text
|
||||
POST /_/:project/_/admin/backup/:restoreToken?collections=contacts&merge=true&sse=1
|
||||
```
|
||||
|
||||
| Query-Param | Default | Beschreibung |
|
||||
|-------------|---------|-------------|
|
||||
| `collections` | _(alle)_ | Welche Collections aus dem Backup importieren |
|
||||
| `merge` | `false` | `true` = Dokumente updaten, Orphans behalten; `false` = exakter 1:1-Abgleich (Orphans löschen) |
|
||||
| `sse` | `0` | `1` = Fortschritt via SSE streamen |
|
||||
|
||||
### Direkter Restore (`sse=0`)
|
||||
|
||||
**Response (200):**
|
||||
|
||||
```json
|
||||
{
|
||||
"restoredCollections": ["contacts"],
|
||||
"skippedCollections": ["legacy_old"],
|
||||
"totalDocuments": 42,
|
||||
"mediaFiles": 5
|
||||
}
|
||||
```
|
||||
|
||||
### Restore-Modi
|
||||
|
||||
**`merge=false` (Default) — Exakter 1:1-Abgleich:**
|
||||
- Jedes Dokument aus dem Backup wird via atomarem Upsert (`ReplaceOne`) eingefügt oder ersetzt
|
||||
- Nach dem Import werden alle Dokumente gelöscht, die vor dem Restore existierten, aber **nicht** im Backup enthalten sind (Orphans)
|
||||
- Ergebnis: Die Collection ist eine exakte 1:1-Kopie des Backups — jedes Feld, jeder BSON-Typ
|
||||
- Kein Zeitfenster, in dem die Collection leer ist (kein Pre-Delete mehr)
|
||||
- Bricht der Import ab, sind bereits verarbeitete Docs korrekt, unverarbeitete unangetastet
|
||||
|
||||
**`merge=true` — Überschreiben, Orphans behalten:**
|
||||
- Existierende Dokumente mit gleicher `_id` werden **ersetzt** (atomares Upsert)
|
||||
- Neue `_id`s werden **angelegt**
|
||||
- Dokumente im Ziel, die nicht im Backup sind, bleiben **erhalten**
|
||||
- Geeignet für partielle Updates / Migration einzelner Collections
|
||||
|
||||
### SSE-Modus (`sse=1`)
|
||||
|
||||
```
|
||||
event: progress
|
||||
data: {"phase":"init"}
|
||||
|
||||
event: progress
|
||||
data: {"phase":"collection","collection":"contacts","action":"importing"}
|
||||
|
||||
event: result
|
||||
data: {"restoredCollections":["contacts"],"skippedCollections":[],"totalDocuments":42,"mediaFiles":5,"phase":"done"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Temp-Daten aufräumen
|
||||
|
||||
```text
|
||||
DELETE /_/:project/_/admin/backup/:restoreToken
|
||||
```
|
||||
|
||||
Explizites Cleanup des Temp-Verzeichnisses. Antwort: `{"message":"ok"}`.
|
||||
|
||||
**Automatisches Cleanup:** Ein Background-Job (5-Minuten-Takt) löscht Temp-Verzeichnisse,
|
||||
deren TTL abgelaufen ist:
|
||||
- **Restore-Temp-Dirs**: 1 Stunde TTL
|
||||
- **Download-ZIPs**: 5 Minuten TTL
|
||||
|
||||
---
|
||||
|
||||
## Typischer Workflow
|
||||
|
||||
### Szenario A: Projekt von Staging nach Production kopieren
|
||||
|
||||
```bash
|
||||
# 1. Auf Staging: Backup herunterladen
|
||||
curl -H "X-Admin-Token: $STAGING_TOKEN" \
|
||||
-o staging-backup.zip \
|
||||
"https://staging.example.com/api/v1/_/myproject/_/admin/backup"
|
||||
|
||||
# 2. Auf Production: Backup hochladen & analysieren
|
||||
curl -H "X-Admin-Token: $PROD_TOKEN" \
|
||||
-F "file=@staging-backup.zip" \
|
||||
"https://prod.example.com/api/v1/_/myproject/_/admin/backup"
|
||||
# → {"restoreToken":"abc...","collections":{...}}
|
||||
|
||||
# 3. Prüfung der Analyse-Warnings, dann Restore ausführen
|
||||
curl -H "X-Admin-Token: $PROD_TOKEN" \
|
||||
-X POST \
|
||||
"https://prod.example.com/api/v1/_/myproject/_/admin/backup/abc..."
|
||||
# → {"restoredCollections":["contacts","pages"],"totalDocuments":84,"mediaFiles":12}
|
||||
```
|
||||
|
||||
### Szenario B: Nur eine Collection aktualisieren (Selective Restore)
|
||||
|
||||
```bash
|
||||
# 1. Full Backup herunterladen (enthält alle Collections)
|
||||
curl -H "X-Admin-Token: $TOKEN" -o full-backup.zip "..."
|
||||
|
||||
# 2. Upload & Analyse
|
||||
RESTORE_TOKEN=$(curl -H "X-Admin-Token: $TOKEN" -F "file=@full-backup.zip" "..." | jq -r .restoreToken)
|
||||
|
||||
# 3. Nur "contacts" wiederherstellen
|
||||
curl -H "X-Admin-Token: $TOKEN" -X POST \
|
||||
"http://.../_/myproject/_/admin/backup/$RESTORE_TOKEN?collections=contacts"
|
||||
```
|
||||
|
||||
### Szenario C: Mit Merge updaten (bestehende Daten erhalten)
|
||||
|
||||
```bash
|
||||
# merge=true: Backup-Docs ersetzen bestehende, neue hinzufügen,
|
||||
# aber Docs die nur im Ziel existieren bleiben erhalten
|
||||
curl -H "X-Admin-Token: $TOKEN" -X POST \
|
||||
"http://.../_/myproject/_/admin/backup/$TOKEN?merge=true"
|
||||
```
|
||||
|
||||
### Szenario D: Mit SSE-Fortschritt (große Projekte)
|
||||
|
||||
```bash
|
||||
# Download mit Fortschrittsanzeige
|
||||
curl -H "X-Admin-Token: $TOKEN" -N \
|
||||
"http://.../_/myproject/_/admin/backup?sse=1"
|
||||
# → event:progress + event:result mit downloadUrl
|
||||
|
||||
# Restore mit Fortschrittsanzeige
|
||||
curl -H "X-Admin-Token: $TOKEN" -N -X POST \
|
||||
"http://.../_/myproject/_/admin/backup/$TOKEN?sse=1"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ZIP-Struktur (Referenz)
|
||||
|
||||
```
|
||||
backup-<namespace>-<timestamp>.zip
|
||||
├── backup-info.json # Metadaten + Schema-Fingerprint
|
||||
├── collections/
|
||||
│ ├── contacts.json # Array aller Dokumente (mit _id)
|
||||
│ └── pages.json
|
||||
└── media/ # Nur Dateien im uploadPath
|
||||
└── contacts/
|
||||
└── <documentId>/
|
||||
└── <fieldName>/
|
||||
└── <filename>
|
||||
```
|
||||
|
||||
Kein `project.json` oder `config.json` — das Projekt muss auf dem Zielsystem bereits existieren.
|
||||
|
||||
---
|
||||
|
||||
## Media-Erkennung
|
||||
|
||||
Beim Export werden **nur** Dateien berücksichtigt, die:
|
||||
1. Über ein `file`- oder `file[]`-Feld im Dokument referenziert sind
|
||||
2. **Tatsächlich** im konfigurierten `uploadPath` existieren (`os.Stat` → OK)
|
||||
3. Innerhalb des `uploadPath` liegen (Path-Traversal-Schutz)
|
||||
|
||||
Algorithmus:
|
||||
```
|
||||
Für jedes Dokument einer Collection:
|
||||
Für jedes Feld vom Typ "file" oder "file[]":
|
||||
Relativer Pfad aus Feldwert extrahieren
|
||||
Absoluter Pfad = <uploadPath> / <relativer Pfad>
|
||||
os.Stat() → existiert → in ZIP aufnehmen
|
||||
Existiert nicht → skip, Log-Warnung
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sicherheit
|
||||
|
||||
| Maßnahme | Beschreibung |
|
||||
|----------|-------------|
|
||||
| **Auth** | Server-Token, Projekt-Token, oder JWT AdminRole erforderlich |
|
||||
| **Path Traversal** | `filepath.Clean` + `strings.HasPrefix` bei ZIP-Extraktion und Media-Kopie |
|
||||
| **Temp-Files** | Automatischer Cleanup nach TTL, keine Akkumulation |
|
||||
| **One-Time Download** | SSE-Download-Links sind nach einmaligem Abruf ungültig |
|
||||
| **Media außerhalb uploadPath** | Pfade außerhalb des konfigurierten `uploadPath` werden ignoriert |
|
||||
|
||||
---
|
||||
|
||||
## Fehlerbehandlung
|
||||
|
||||
| HTTP-Status | Ursache |
|
||||
|-------------|---------|
|
||||
| **400** | Keine Datei hochgeladen, ungültiges ZIP, fehlende `backup-info.json`, falscher Typ/Version |
|
||||
| **403** | Fehlende Berechtigung (`project.backup` oder `allowBackup`) |
|
||||
| **404** | `restoreToken` nicht gefunden oder abgelaufen, Projekt nicht gefunden |
|
||||
| **410** | Download-Link abgelaufen (nur bei `/download/:file`) |
|
||||
| **500** | Interner Fehler (DB-Lesefehler, ZIP-Erstellung fehlgeschlagen) |
|
||||
|
||||
---
|
||||
|
||||
## Temp-Datei-Management
|
||||
|
||||
```
|
||||
Upload (Phase 1) → /tmp/tibi-backup/<restoreToken>/ (TTL: 1h)
|
||||
Restore (Phase 2) → importiert, dann gelöscht
|
||||
DELETE /:restoreToken → sofort gelöscht
|
||||
TTL-Ablauf → Background-Cleanup (alle 5 Min)
|
||||
|
||||
SSE Download → /tmp/tibi-backup-download/<token>.zip (TTL: 5 Min)
|
||||
GET /download/:file → ausgeliefert + sofort gelöscht
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Konfiguration
|
||||
|
||||
### Server-Level Token (config.yml)
|
||||
|
||||
```yaml
|
||||
api:
|
||||
adminTokens:
|
||||
- token: "backup-token"
|
||||
label: "backup-agent"
|
||||
permissions:
|
||||
- project.backup
|
||||
```
|
||||
|
||||
`project.*` (Wildcard) inkludiert automatisch `project.backup`.
|
||||
|
||||
### Projekt-Level Token
|
||||
|
||||
```yaml
|
||||
admin:
|
||||
tokens:
|
||||
- token: "PROJEKT-TOKEN"
|
||||
allowBackup: true
|
||||
```
|
||||
|
||||
### Keine Projekt-Config-Änderung nötig
|
||||
|
||||
Der Backup/Restore-Vorgang benötigt keine Änderungen an der Projekt-Config.
|
||||
Collections, Felder, uploadPath etc. werden aus der bestehenden Konfiguration gelesen.
|
||||
|
||||
---
|
||||
|
||||
## Implementierung
|
||||
|
||||
### Dateien
|
||||
|
||||
| Datei | Inhalt |
|
||||
|-------|--------|
|
||||
| `internal/controller/admin_backup.go` | Alle Handler (Download, Analyze, Restore, Cleanup, SSE, Temp-Stores) |
|
||||
| `internal/controller/admin_backup_auth_test.go` | 12 Auth-Tests |
|
||||
| `internal/controller/admin_backup_analyze_test.go` | 8 Analyze- & Download-Tests |
|
||||
| `internal/controller/admin_backup_restore_test.go` | 9 Restore-Tests |
|
||||
| `internal/controller/admin_backup_integration_test.go` | 3 SSE/E2E-Tests |
|
||||
| `internal/controller/setup.go` | Routen-Registrierung |
|
||||
| `internal/models/project.go` | `AdminToken.AllowBackup` Feld |
|
||||
| `cmd/server/main.go` | `StartBackupCleanupJob()` Aufruf |
|
||||
|
||||
### Test-Statistik
|
||||
|
||||
**33 Tests** (alle grün):
|
||||
- 12 Auth-Tests (Server-Token, Projekt-Token, JWT, Berechtigungen)
|
||||
- 6 Analyze-Tests (ZIP-Validierung, Schema-Diffs, unbekannte Collections)
|
||||
- 2 Download-Tests (direkt, selektiv)
|
||||
- 9 Restore-Tests (Import, Merge, Selective, Media-Copy, Cleanup)
|
||||
- 3 SSE/E2E-Tests (SSE-Download, DownloadFile, SSE-Restore)
|
||||
- 1 E2E-Test (skipped — Mock-Projekt-API-Limitierung)
|
||||
|
||||
Keine Regression auf bestehenden Admin-Tests (`TestAdminReload`, `TestAdminAuthReload` etc.).
|
||||
|
||||
---
|
||||
|
||||
## Betrieb: Speicherverhalten seit Streaming-Umbau (2026-10)
|
||||
|
||||
Erstellung und Wiederherstellung von Snapshots und Backups laufen in
|
||||
**O(Batch)** statt O(Collection): Keyset-Pagination (`_id $gt`, Sort `_id`),
|
||||
Datei-/ZIP-Streaming, sequenziell pro Collection. RAM ist keine Grenze mehr —
|
||||
auch 10-GB-Collections (z. B. Outdoorworks: `productVariant` 91k × ~117 KB mit
|
||||
`_search`-Bloat) laufen mit konstantem Heap.
|
||||
|
||||
### Batch-Konstanten (`internal/controller/snapshot_stream.go`)
|
||||
|
||||
| Konstante | Wert | Wirkung |
|
||||
|---|---|---|
|
||||
| `SnapshotBatchDocs` | 500 | Docs pro DB-Batch und Restore-Batch |
|
||||
| `SnapshotBatchBytes` | 8 MiB | reserviert (Byte-Cap, greift pro großem Einzeldoc) |
|
||||
| `SnapshotBufSize` | 64 KiB | `bufio`-Puffer für Snapshot-Dateien |
|
||||
| `OrphanMapThreshold` | 200.000 | darunter In-RAM-Orphan-Set, darüber Temp-Collection in Ziel-DB |
|
||||
| `orphanChunkSize` | 500 | `$in`-Chunks für Orphan-Deletes |
|
||||
|
||||
### Empfehlungen
|
||||
|
||||
- `GOMEMLIMIT` setzen (z. B. `GOMEMLIMIT=512MiB` im Container): begrenzt GC-Druck,
|
||||
Streaming braucht es nicht — aber es schützt vor Nachbar-Last.
|
||||
- Jeder Snapshot-Run loggt `snapshot <id>: done documents=… heapGrowthMB=…`
|
||||
(`heapGrowthMB` = Heap-Wachstum über den Lauf, kein Peak; Peak ist per
|
||||
Konstruktion batch-begrenzt).
|
||||
- Retention `maxCount` behält die **neuesten** Einträge (Bugfix 2026-10-15:
|
||||
zuvor wurden fälschlich die neuesten gelöscht).
|
||||
|
||||
### Nachweise (Tests)
|
||||
|
||||
- `TestExportStreaming_PeakScalesFlat` / `TestStreamRestore_PeakScalesFlat`:
|
||||
10× Docs ⇒ kein 10× Peak (Kontrollmessung Alt-Pfad liegt ein Mehrfaches darüber).
|
||||
- E2E Roundtrips (Mock + Real-Mongo): Snapshot NonMerge/Merge, Backup
|
||||
Restore-Matrix, Download-Pipelines.
|
||||
- OOM-Fixture: `testdata/outdoorworks-oom/` (echte `_search`-Form, Stats,
|
||||
`dump-full.sh` für den 8-GB-Voll-Dump nach `/WM_Dev/.cortex/mcp-tmp/`).
|
||||
Reference in new issue
Block a user