Enterprise-Backup-, Recovery-, Verification-, Security- und Monitoring-Plattform fuer Proxmox VE, Windows, Linux und Dateisysteme. Der Leitsatz, der fast jede Entscheidung erklaert: Ein Backup gilt erst als vertrauenswuerdig, wenn Integritaet geprueft und Wiederherstellbarkeit nachgewiesen wurde. Deshalb steigt ein Wiederherstellungspunkt erst nach einem tatsaechlich durchgefuehrten Restore-Test auf "recoverable", und Unbekanntes geht in keine Bewertung als "gut" ein. Umfang (Phasen 0-23): - Repository Engine: inhaltsadressierte Bloecke, atomares Commit-Protokoll, Katalogaufbau allein aus den Manifesten — ohne Datenbank - Backup Engine: inhaltsabhaengiges Chunking, Deduplizierung trotz Verschluesselung, zstd, AES-256-GCM, Streaming mit Gegendruck - Agenten fuer Windows und Linux mit Auftragsabholung (Pull-Modell) - Proxmox-Provider mit beiden Zugriffswegen auf die Sicherungsarchive - Scheduler, Recovery Engine mit Pruefpunkt, Verification, Unveraenderlichkeit - Weboberflaeche, Kennzahlen, Meldungen, Berichte, Security Center, Ransomware-Heuristik (meldet, handelt nie) - Disaster Recovery, Haertung, Leistungsmessung, Chaos Testing - Eingefrorene Vertraege fuer API, Migrationen, Backup-Format und Repository - Auslieferungspaket fuer linux/amd64, linux/arm64 und windows/amd64 Nicht enthalten und als solches gekennzeichnet: Kapazitaetsprognose, Backup Copy, Changed Block Tracking bei Proxmox, erweiterte Attribute und ACLs. Gebaut, aber nie auf echter Hardware gefahren: der Windows-Dienst, die systemd-Einheit und der verpflichtende Proxmox-Meilenstein — ob eine wiederhergestellte VM startet, ist ungeprueft. Einzelheiten in CHANGELOG.md und docs/release-candidate.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
380 lines
28 KiB
Markdown
380 lines
28 KiB
Markdown
# Scheduler und Auftragsverwaltung
|
|
|
|
## Status
|
|
|
|
Die Kette ist geschlossen: **Auftrag → Scheduler → Backup Engine → Repository → Wiederherstellung.** Der Scheduler sichert selbstständig.
|
|
|
|
Real nachgewiesen mit 38 MiB in 24 Objekten: Auftrag über die API angelegt, vom Scheduler ausgeführt, Integritätsprüfung sauber (44 Chunks), Quelle gelöscht, aus der Zusatzsicherung wiederhergestellt — **bitgenau identisch** inklusive Rechten, Symlink und leerem Verzeichnis. Der zweite Lauf las 0,0 MiB in 0,06 s statt 38,1 MiB in 0,57 s.
|
|
|
|
Es fehlen noch Quellen außer Dateisystemen (siehe „Offene Punkte").
|
|
|
|
## Die Ausführungsschleife
|
|
|
|
Ein Durchgang (Standardtakt 10 s):
|
|
|
|
1. **Verwaiste Läufe freigeben** — Selbstheilung nach dem Absturz eines Control-Servers.
|
|
2. **Fällige Aufträge übernehmen** und ihre Läufe anlegen.
|
|
3. **Anstehende Läufe übernehmen** und ausführen.
|
|
4. **Ergebnis festschreiben**, gegebenenfalls Wiederholung einreihen.
|
|
|
|
### Die Executor-Schnittstelle
|
|
|
|
```go
|
|
type Executor interface {
|
|
Execute(ctx context.Context, req ExecutionRequest) (ExecutionResult, error)
|
|
}
|
|
```
|
|
|
|
Sie hält die Schleife frei von Backup Engine, Repository und Providern. Das ist nicht nur Ordnung: Ohne sie liesse sich das Zusammenspiel von Zeitplan, Wartungsfenster, Nebenläufigkeit und Wiederholung nur mit einem echten Repository prüfen — also praktisch gar nicht. Alle zwölf Schleifentests laufen gegen einen aufzeichnenden Ersatz.
|
|
|
|
Umgesetzt ist sie in `packages/backupexecutor`. Ohne eingerichteten Executor greift `NotImplementedExecutor` und meldet `EXECUTOR_NOT_CONFIGURED` — ein Executor, der stillschweigend Erfolg meldete, wäre das gefährlichste Fake-Feature der Anlage: grüne Läufe in der Oberfläche, leeres Repository.
|
|
|
|
## Der Backup-Executor
|
|
|
|
Ablauf je Lauf: Repository auflösen und öffnen → jede Quelle sichern → Backup in der Control Plane vermerken → Kennzahlen zurückgeben.
|
|
|
|
**Das Repository wird einmal für den ganzen Lauf geöffnet** und hält dabei die Schreibsperre. Es je Quelle zu öffnen und zu schließen liefe auf ein Wechselspiel um dieselbe Sperre hinaus.
|
|
|
|
### Verschlüsselung ist keine Option
|
|
|
|
Ein Executor ohne Schlüsselmaterial wird **beim Einrichten abgelehnt**, nicht erst beim ersten Lauf um zwei Uhr nachts. Wer unverschlüsselt sichern will, muss `AllowUnencrypted` setzen — und bekommt bei jedem Start eine Warnung ins Protokoll. Ein Executor, der ohne Schlüssel trotzdem sicherte, legte unverschlüsselte Backups an, ohne dass es jemandem auffiele.
|
|
|
|
### Eine gescheiterte Quelle bricht den Lauf nicht ab
|
|
|
|
Die übrigen sollen gesichert werden. Verschwiegen wird sie trotzdem nicht: Sie zählt als übergangenes Objekt, und damit ist der Lauf ein **Teilfehler** — niemals ein Erfolg. Scheitern *alle* Quellen, ist der Lauf gescheitert (`ALL_SOURCES_FAILED`).
|
|
|
|
Real geprüft: Ein Auftrag mit einer guten und einer fehlenden Quelle endet mit `partial_failure`, 24 gesicherten Objekten und einer übergangenen — und die gute Quelle liegt vollständig im Repository.
|
|
|
|
### Zusatzsicherung ist der Standard
|
|
|
|
Liegt ein Elternbackup in der Kette, wird inkrementell gesichert. Das ist die richtige Voreinstellung: Der Gewinn ist Lesezeit, und die ist bei jedem Lauf nach dem ersten der begrenzende Faktor.
|
|
|
|
Je Quelle und Repository gibt es genau **eine** offene Kette. Zwei Quellen in einer Kette zu führen machte die eigenständige Wiederherstellung einer einzelnen Quelle unmöglich.
|
|
|
|
### Die Backup-Kennung enthält die Laufkennung
|
|
|
|
```
|
|
run-<lauf-uuid>-<bereinigte-quelle>
|
|
```
|
|
|
|
Damit lässt sich von einem Backup im Repository aus zurückverfolgen, welcher Lauf es erzeugt hat — **auch dann, wenn die Datenbank verloren ging**. Der Quellanteil ist nur eine Lesehilfe und wird gekürzt; das Repository begrenzt Kennungen auf 64 Zeichen, weil sie zu Dateinamen werden. Pfadtrenner, führende Punkte und `..` werden ersetzt.
|
|
|
|
### Der Datenbankeintrag ist ein Verweis, keine Kopie
|
|
|
|
`RecordBackup` hält fest, **wo** das Manifest liegt, nicht seinen Inhalt. Das Repository bleibt ohne die Datenbank rekonstruierbar (SYNCOVA_ARCHITECTURE.md §10); der Eintrag beschleunigt nur die Suche.
|
|
|
|
Scheitert er, wird das protokolliert und **nicht** geworfen: Das Backup liegt bereits vollständig im Repository und ist von dort wiederherstellbar. Den Lauf als gescheitert zu melden, obwohl die Daten sicher sind, führte zu einer sinnlosen Wiederholung.
|
|
|
|
### Ausgetauschtes Repository
|
|
|
|
Beim ersten Öffnen wird die im Repository hinterlegte Kennung übernommen. Weicht sie später ab, wurde das Verzeichnis ausgetauscht. Der Lauf wird deswegen **nicht** abgebrochen — das Repository ist in sich stimmig —, aber die Angaben der Control Plane zu Backups und Belegung beziehen sich dann auf einen fremden Bestand. Das gehört ins Protokoll.
|
|
|
|
### Übergangene Objekte sind ein Teilfehler
|
|
|
|
Die Auswertung eines Laufs folgt einer festen Reihenfolge, und die ist nicht beliebig:
|
|
|
|
1. **Abbruch** wiegt am schwersten. Wer abbricht, will kein Ergebnis mehr; ein „gescheitert" löste eine sinnlose Wiederholung aus.
|
|
2. **Fehler** des Executors — mit seiner Fehlerklasse, die über die Wiederholung entscheidet.
|
|
3. **Übergangene Objekte** — auch **ohne** gemeldeten Fehler wird der Lauf zum Teilfehler.
|
|
|
|
Punkt 3 ist der Kern. Ein Executor kann fehlerfrei durchlaufen und trotzdem 17 Dateien nicht gelesen haben. Dieser Lauf ist kein Erfolg. Die Begründung kommt mit: „17 Objekte übergangen" ist keine Auskunft, „17 Objekte wegen fehlender Leseberechtigung" schon.
|
|
|
|
### Was nicht wiederholt wird
|
|
|
|
**Ein Teilfehler wird nicht wiederholt.** Die übergangenen Objekte wären beim nächsten Versuch mit hoher Wahrscheinlichkeit dieselben, und der Lauf kostete die volle Zeit für dasselbe Ergebnis. Er verlangt einen Blick, keine Wiederholung.
|
|
|
|
Gescheiterte Läufe werden nach der Wiederholungsstrategie des Auftrags erneut eingereiht — als eigener Lauf mit `trigger=retry` und erhöhter Versuchsnummer, geplant für den Zeitpunkt nach dem Backoff. Der neue Lauf entsteht erst, **nachdem** der gescheiterte abgeschlossen wurde; sonst verhinderte ihn der Teilindex, der nur einen aktiven Lauf je Auftrag zulässt.
|
|
|
|
### Versäumte Läufe werden übersprungen, nicht nachgeholt
|
|
|
|
War der Server drei Tage aus, liegt der geplante Zeitpunkt drei Tage zurück. Der Auftrag läuft **einmal**, nicht dreimal: Drei Sicherungen desselben Bestands hintereinander kosten Zeit und Platz, ohne einen einzigen zusätzlichen Wiederherstellungspunkt zu schaffen — die Daten von vorgestern gibt es nicht mehr. Die Zahl der übersprungenen Läufe wird protokolliert, nicht verschwiegen.
|
|
|
|
**Kein Drift:** Gerechnet wird vom geplanten Zeitpunkt aus, nicht vom tatsächlichen Beginn. Sonst schöbe sich ein Auftrag mit jeder Verspätung weiter nach hinten. Und der nächste Zeitpunkt wird **beim Übernehmen** fortgeschrieben, nicht nach dem Lauf: Sonst bliebe der Auftrag bei einem langen Backup fällig und würde im nächsten Durchgang erneut übernommen.
|
|
|
|
### Selbstheilung nach einem Absturz
|
|
|
|
Das ist der wichtigste Mechanismus der ganzen Ausführung.
|
|
|
|
Stirbt ein Control-Server mitten im Lauf, bleibt dessen Zeile auf `running` stehen. Der Teilindex lässt dann keinen weiteren Lauf dieses Auftrags zu — die Sicherung fiele **dauerhaft** aus, ohne dass jemand einen Fehler sähe. Es gäbe nur einen Auftrag, der nie wieder läuft.
|
|
|
|
Deshalb meldet jeder laufende Vorgang sich alle 30 Sekunden lebendig. Bleibt die Meldung fünf Minuten aus, gilt der Lauf als verwaist und wird freigegeben — als **gescheitert** vermerkt, nicht gelöscht: Der Abbruch ist ein Ereignis, das ins Protokoll gehört. Die Fehlerklasse `transient` sorgt dafür, dass die Wiederholungsstrategie greift.
|
|
|
|
**Die Frist muss deutlich über dem Meldeabstand liegen.** Andernfalls gäbe ein kurzer Aussetzer — eine langsame Datenbank, ein überlasteter Server — einen noch laufenden Lauf frei, und derselbe Auftrag liefe zweimal. Die Schleife lehnt eine Frist unterhalb des Meldeabstands beim Start ab; der Fehler wäre im Betrieb kaum zu finden.
|
|
|
|
### Geordnetes Beenden
|
|
|
|
Bei SIGTERM werden die laufenden Vorgänge **abgebrochen**, nicht zu Ende geführt. Sie laufen zu lassen hiesse, das Herunterfahren an das längste Backup zu hängen — bei einem mehrstündigen Lauf ein Neustart, der nie endet.
|
|
|
|
Das Ergebnis wird trotzdem geschrieben, mit eigenem Kontext: Sonst bliebe die Zeile auf `running` stehen und blockierte den Auftrag bis zum Ablauf der Frist. Verstreicht die Frist von 30 Sekunden, während noch Läufe aktiv sind, wird das als Warnung gemeldet — sie werden dann von der Freigabe verwaister Läufe eingesammelt. Das ist der ehrliche Ausgang: Wir wissen nicht, ob sie noch etwas schreiben.
|
|
|
|
## Der Backup-Wizard
|
|
|
|
Zehn Schritte von Name bis Anlegen (`apps/web/src/features/jobs`). Der Aufbau folgt drei Entscheidungen:
|
|
|
|
**Die Logik liegt getrennt von der Darstellung** (`wizardModel.ts`). Welcher Schritt vollständig ist und was in die Anfrage wandert, ist reine Berechnung — und damit ohne gerenderte Maske prüfbar. 23 der 34 Frontend-Tests laufen gegen das Modell.
|
|
|
|
**Der Entwurf lebt in einem Zustand, nicht in den Eingabefeldern.** Sonst wäre jeder Blick zurück ein Datenverlust. Vorwärts geht es nur über einen vollständigen Schritt; ein noch nicht erreichter Schritt ist in der Leiste nicht anklickbar — er würde eine Prüfung überspringen, die der Assistent gerade führen soll.
|
|
|
|
**Fehler erscheinen erst beim Versuch weiterzugehen.** Sie von Anfang an zu zeigen hieße, ein leeres Formular als fehlerhaft zu markieren.
|
|
|
|
### Drei Schritte fragen nichts ab
|
|
|
|
Aufbewahrung, Prüfung und Benachrichtigung haben keine Umsetzung im Backend. Sie erscheinen trotzdem — der Plan nennt zehn Schritte —, aber **ohne Eingabefelder**. Eine Maske, die Werte sammelt, die niemand auswertet, ist ein vorgetäuschtes Funktionsversprechen (PROMPT.md §138).
|
|
|
|
Stattdessen steht dort, was ohne diese Einstellung tatsächlich geschieht:
|
|
|
|
> **Noch nicht verfügbar** — Aufbewahrungsregeln sind noch nicht umgesetzt. Backups bleiben bis auf Weiteres vollständig erhalten und werden nicht automatisch gelöscht — der Speicherbedarf wächst also mit jedem Lauf.
|
|
|
|
Die Übersicht in Schritt 9 wiederholt das: „Aufbewahrung: unbegrenzt (Regeln noch nicht umgesetzt)". Eine leere Zeile ließe offen, ob nichts eingestellt oder nichts möglich ist.
|
|
|
|
Ebenso im Quellschritt: Proxmox-, Windows- und Linux-Quellen stehen in der Auswahl, sind aber deaktiviert und als „noch nicht verfügbar" gekennzeichnet — statt sie wegzulassen und den Anwender rätseln zu lassen, ob sie fehlen oder nur versteckt sind.
|
|
|
|
### Was der Wizard nicht entscheidet
|
|
|
|
**Verschlüsselung ist keine Wahl.** Der Executor verweigert den Dienst ohne Schlüsselmaterial; eine Schaltfläche zum Abschalten wäre eine Einstellung, die es nicht gibt. Der Schritt „Sicherheit" sagt das und fragt stattdessen nach Dringlichkeit und Bandbreitengrenze.
|
|
|
|
**Ob ein Repository Sicherungen annimmt, entscheidet der Server.** Die API liefert `accepts_backups`; die Oberfläche müsste sonst wissen, welche Zustände schreibend sind. Gesperrte Ziele erscheinen in der Liste, sind aber nicht wählbar und tragen ihren Grund — sie wegzulassen ließe den Anwender ein Repository suchen, das er nicht findet.
|
|
|
|
### Die Zeitzone kommt aus dem Browser
|
|
|
|
Voreingestellt ist `Intl.DateTimeFormat().resolvedOptions().timeZone`. Ohne Angabe rechnet der Server in UTC — derselbe Auftrag liefe dann je nach Standort zu einer anderen Uhrzeit. Bei einem Intervallplan wird die Zeitzone weggelassen: Er zählt Abstände, keine Uhrzeiten.
|
|
|
|
### Der Repository-Endpunkt
|
|
|
|
`GET /api/v1/repositories` (Recht `repositories.read`), rein lesend und ohne Pagination — ein Betrieb hat eine Handvoll Ziele, nicht tausende. Das **Anlegen** bleibt bei `syncova-repo create`: Ein Repository entsteht auf einem Datenträger, nicht in einer Datenbankzeile.
|
|
|
|
### Nachgewiesen
|
|
|
|
Genau die Anfrage, die der Wizard baut, gegen den laufenden Dienst: Auftrag angelegt (`täglich um 02:00 Uhr (Europe/Berlin)`, nächster Lauf berechnet), ausgeführt (4,8 MiB, 5 Objekte, 0,16 s), wiederhergestellt — und die im Wizard eingetragene Ausschlussregel `*.tmp` hat gewirkt: Die Datei fehlt im Backup.
|
|
|
|
## Die Zeitumstellung
|
|
|
|
Das ist der Punkt, an dem die meisten Scheduler stillschweigend danebenliegen, und er kostet genau einen Backup-Lauf im Jahr — den niemand vermisst, bis er gebraucht wird.
|
|
|
|
**Frühjahr, die Uhr springt von 02:00 auf 03:00.** Ein Auftrag „täglich um 02:30" hat an diesem Tag keinen Zeitpunkt. Wer den Tag überspringt, lässt die Sicherung ausfallen. Syncova nimmt den nächsten gültigen Zeitpunkt (03:30) — der Lauf findet statt.
|
|
|
|
**Herbst, 02:00 bis 03:00 wiederholt sich.** „Täglich um 02:30" gäbe es zweimal. Es läuft nur einmal, und zwar beim ersten Vorkommen: Ein Backup lieber eine Stunde früher als zwei Backups, die zwei Ketten anlegen.
|
|
|
|
Beides ist mit `Europe/Berlin` gegen die echten Umstellungstermine 2026 geprüft (`TestDailyScheduleSurvivesSpringForward`, `TestDailyScheduleRunsOnceOnFallBack`).
|
|
|
|
Daraus folgt auch die Suchweise: Kalenderzeitpläne werden **tageweise mit `AddDate`** gesucht, nicht mit `Add(24h)`. An Umstellungstagen hat ein Tag 23 oder 25 Stunden; mit einer festen Stundenzahl verschöbe sich die Suche zweimal im Jahr.
|
|
|
|
**Ohne Zeitzone gilt UTC**, nicht die Ortszeit des Servers. Andernfalls liefe dieselbe Konfiguration auf zwei Servern zu verschiedenen Zeiten — der häufigste Grund für ein Backup, das „mal um zwei und mal um vier" läuft.
|
|
|
|
## Zeitplanarten
|
|
|
|
| Art | Beispiel |
|
|
| --- | --- |
|
|
| `manual` | nur auf Anforderung |
|
|
| `interval` | alle 6 Stunden (mindestens 1 Minute) |
|
|
| `hourly` | stündlich zur Minute 30 |
|
|
| `daily` | täglich um 02:00 |
|
|
| `weekly` | samstags und sonntags um 22:00 |
|
|
| `monthly` | am 1. und am **letzten** Tag des Monats |
|
|
| `cron` | `*/15 9-17 * * mon-fri` |
|
|
|
|
**Der Monatsletzte (`-1`) ist kein Luxus.** Wer „am 31." einträgt, bekommt in vier Monaten des Jahres keine Sicherung. Wer den Monatsabschluss sichern will, meint den letzten Tag.
|
|
|
|
### Der eigene Cron-Parser
|
|
|
|
Cron ist klein; die Fallstricke liegen nicht im Zerlegen, sondern in Zeitzonen und einer Sonderregel, die ohnehin selbst gelöst werden muss:
|
|
|
|
**Sind Tag-des-Monats **und** Wochentag eingeschränkt, gilt ODER statt UND.** `0 0 13 * 5` bedeutet „am 13. **oder** freitags", nicht „an Freitagen, die der 13. sind". Wer das als UND umsetzt, baut einen Zeitplan, der fast nie läuft — und es fällt erst nach Monaten auf.
|
|
|
|
Sekunden gibt es bewusst nicht: Ein Sicherungsauftrag im Sekundentakt ist kein Zeitplan, sondern ein Tippfehler. Sonntag darf `0` oder `7` heißen, Namen (`mon`, `jan`) sind zugelassen, `@daily` und Verwandte auch.
|
|
|
|
## Wartungsfenster
|
|
|
|
Zwei Wirkungen: `blackout` unterdrückt Läufe im Fenster, `allowed` lässt sie **nur** im Fenster zu.
|
|
|
|
**Ein durch ein Fenster verhinderter Lauf wird verschoben, nicht übergangen.** Ihn ausfallen zu lassen erzeugte eine Lücke in der Sicherungskette, von der niemand erführe: Dass ein Lauf verspätet ist, sieht man; dass er fehlt, nicht.
|
|
|
|
Zwei Feinheiten:
|
|
|
|
- **Ein wiederkehrendes Fenster hat eine Länge, keine Endzeit.** Sonst bräuchte „22:00 bis 02:00" eine Sonderregel, die gern vergessen wird. Die Auswertung schaut acht Tage zurück: Ein Fenster, das samstags um 22:00 beginnt und zwölf Stunden dauert, reicht bis Sonntag 10:00 — wer nur den Tag des Zeitpunkts prüft, hielte Sonntag 09:00 für frei.
|
|
- **Ein Erlaubnisfenster für Auftrag A sperrt Auftrag B nicht.** Ohne diese Einschränkung fielen sämtliche Sicherungen aus, sobald irgendwo ein Erlaubnisfenster existiert.
|
|
|
|
Findet sich binnen eines Monats kein freier Zeitpunkt, ist das ein **Fehler** und kein stiller Ausfall — fast immer eine Fehlkonfiguration.
|
|
|
|
## Warteschlange
|
|
|
|
### Verhungerungsschutz
|
|
|
|
Ein wartender Auftrag gewinnt 50 Punkte je Stunde, gedeckelt bei 100. Ohne Alterung verhungert ein Auftrag niedriger Priorität in einer Anlage mit genug dringenden Aufträgen **für immer** — und niemand bemerkt es, weil er technisch „wartet" statt zu scheitern. Der Deckel entspricht dem Abstand zweier Stufen: Ein wartender Auftrag steigt höchstens um eine Stufe auf und überholt damit nie eine kritische Sicherung.
|
|
|
|
Eine unbekannte Prioritätsstufe gilt als `normal`, nicht als höchste: Ein Tippfehler soll keinen Auftrag an die Spitze befördern.
|
|
|
|
### Nebenläufigkeitsgrenzen
|
|
|
|
Global, je Repository und je Quelle. Die letzten beiden sind keine Feinheit:
|
|
|
|
- **Je Repository:** Das Repository hält beim Schreiben eine Sperre. Mehrere gleichzeitige Läufe warten ohnehin aufeinander und belegen dabei nur Arbeitsspeicher.
|
|
- **Je Quelle:** Zwei gleichzeitige Sicherungen derselben VM lasten den Wirt aus, ohne zusätzlichen Nutzen.
|
|
|
|
Jeder zurückgehaltene Auftrag trägt eine **Begründung**. Ein wartender Auftrag ohne Begründung ist für den Betrieb wertlos: Man sieht, dass nichts geschieht, aber nicht warum.
|
|
|
|
### Abhängigkeiten
|
|
|
|
Eine Abhängigkeit ist erfüllt, wenn der Vorgänger zuletzt **vollständig** gelungen ist. **Ein Teilfehler genügt nicht:** Wer eine Datenbank sichert und danach das Anwendungsverzeichnis, will nicht das Verzeichnis zu einer halben Datenbank.
|
|
|
|
Ringschlüsse werden beim Anlegen erkannt, nicht beim Ausführen. Ein Ring bliebe sonst unbemerkt: Jeder Auftrag wartete auf einen anderen, keiner liefe je an, und die Oberfläche zeigte lauter „wartende" Aufträge ohne erkennbaren Grund.
|
|
|
|
## Wiederholungen
|
|
|
|
Wiederholt wird **nur** bei `transient`, `network`, `repository` und `source`. Ein Anmeldefehler behebt sich nicht durch Warten; ein Integritätsfehler wird durch Wiederholen nicht besser, sondern nur später bemerkt; ein sicherheitsrelevanter Fehler gehört gemeldet, nicht verschluckt.
|
|
|
|
Voreinstellung: drei Versuche, 30 s → 60 s → 120 s, gedeckelt bei 15 Minuten. **Der Deckel ist wesentlich:** Ohne ihn wären es beim achten Versuch über eine Stunde und beim zehnten mehr als vier — ein nächtliches Sicherungsfenster wäre längst vorbei. Mehr als zehn Versuche sind nicht zugelassen; sie verschleiern ein dauerhaftes Problem, statt es zu melden.
|
|
|
|
**Die Streuung wirkt nur nach unten.** Nach oben verlängerte sie die Wartezeit über die vereinbarte Grenze — der Deckel wäre dann keiner. Ihr Zweck ist der Gleichlauf: Fallen zwanzig Aufträge derselben Störung zum Opfer, liefen sie ohne Streuung alle gleichzeitig wieder an und überlasteten die eben erholte Gegenstelle sofort erneut.
|
|
|
|
Gerechnet wird in Gleitkomma statt mit Schiebeoperationen: Ein Schieben um mehr als 62 Stellen liefe über und machte aus einer langen Wartezeit eine negative.
|
|
|
|
Eine unbekannte Fehlerart gilt als **dauerhaft**, also „nicht wiederholen". Der umgekehrte Standard wäre gefährlicher: Ein falsch als vorübergehend eingeordneter Fehler liesse einen aussichtslosen Auftrag immer wieder anlaufen und verdeckte dabei die Ursache.
|
|
|
|
## Bandbreitengrenze
|
|
|
|
Token-Bucket, kein festes Zeitfenster: Ein Fenster erlaubt an seiner Grenze die doppelte Rate — 10 MB am Ende des einen und 10 MB am Anfang des nächsten ergeben 20 MB in kurzer Folge.
|
|
|
|
### Was begrenzt wird
|
|
|
|
**Das Lesen von der Quelle**, nicht das Schreiben ins Repository. Weil die Pipeline mit Gegendruck arbeitet, bremst das den gesamten Ablauf: Was nicht gelesen wird, wird auch nicht gehasht, komprimiert, verschlüsselt und abgelegt. Ein Begrenzer an dieser einen Stelle bindet damit die gesamte Last.
|
|
|
|
Das ist auch die Größe, die ein Betreiber meint: „Lies höchstens 50 MB/s vom Dateiserver." Die Last auf dem produktiven System entsteht beim Lesen.
|
|
|
|
**Nicht begrenzt wird der Schreibweg zum Repository.** Bei einem lokalen Repository fällt das zusammen; bei einem entfernten (NFS, später Objektspeicher) wäre es eine eigene Grenze — und wegen der Deduplizierung eine ganz andere Zahl: Von 19 MiB gelesener Daten gehen bei einem zweiten Lauf 0 MiB über die Leitung.
|
|
|
|
### Ein Begrenzer je Lauf
|
|
|
|
Er gilt für den **gesamten** Lauf und wird über alle Quellen und alle Arbeiter geteilt. Je Quelle oder je Arbeiter einen eigenen zu führen ergäbe ein Vielfaches der vereinbarten Rate — genau der Fehler, der eine Bandbreitengrenze wirkungslos macht. Ein Test prüft das mit drei Quellen: Sie brauchen zusammen die Zeit, die eine einzelne Quelle dreifacher Größe bräuchte.
|
|
|
|
Beim Lesen wird **nach** der Übertragung gewartet (am Dateiende zahlte man sonst für Bytes, die es nicht mehr gibt), beim Schreiben **davor** (die Menge steht fest, und die Leitung wäre sonst schon belegt).
|
|
|
|
### Gemessen
|
|
|
|
19,1 MiB in einem Repository, derselbe Bestand zweimal über den Scheduler:
|
|
|
|
| | Dauer | Durchsatz |
|
|
| --- | --- | --- |
|
|
| ohne Grenze | 0,37 s | 51,7 MiB/s |
|
|
| `bandwidth_limit_bps: 1048576` | 18,17 s | **1,05 MiB/s** |
|
|
|
|
Der Eimer liefert eine Sekunde Vorrat, deshalb liegt der gemessene Durchschnitt bei kurzen Läufen etwas über der Grenze — bei 2 MiB gegen 1 MiB/s sind es 1,79 MiB/s. Über längere Läufe verschwindet der Effekt, wie die Messung oben zeigt.
|
|
|
|
### Bit und Byte
|
|
|
|
`100Mbit` ist ein Achtel von `100MB`. Netzwerkleute rechnen in Bit, Speicherleute in Byte; wer das verwechselt, vergibt das Achtfache. Beide Schreibweisen werden angenommen, ebenso `50MB/s`, `1,5MB` und `1.5MB`.
|
|
|
|
```bash
|
|
syncova-agent backup --repository /backup/repo --path /daten --bandwidth 50MB
|
|
syncova-agent backup --repository /backup/repo --path /daten --bandwidth 400Mbit
|
|
```
|
|
|
|
Im Auftrag: `"bandwidth_limit_bps": 52428800`. Die Angabe wird vor dem Öffnen des Repositorys geprüft — ein Tippfehler soll nicht erst nach dem Sperren auffallen.
|
|
|
|
## Datenbank
|
|
|
|
Migration `000004_jobs`. Zehn Tabellen; drei Entscheidungen lohnen die Erwähnung:
|
|
|
|
**Ein Lauf ist eine eigene Zeile, kein Zustandsfeld am Auftrag.** Ohne Historie ist nicht feststellbar, ob eine Sicherung regelmäßig gelingt.
|
|
|
|
**Ein Teilindex erzwingt einen aktiven Lauf je Auftrag:**
|
|
|
|
```sql
|
|
CREATE UNIQUE INDEX backup_job_runs_single_active_idx
|
|
ON backup_job_runs (job_id) WHERE status IN ('queued','running');
|
|
```
|
|
|
|
Zwei Control-Server, die gleichzeitig denselben fälligen Auftrag sehen, prüfen beide erfolgreich und starten beide — die Datenbank lässt nur einen durch. Der Übernahmemechanismus nutzt zusätzlich `FOR UPDATE SKIP LOCKED`: Der erste Server sperrt die Zeilen, der zweite überspringt sie, statt zu warten. Geprüft mit vier gleichzeitigen Servern auf zehn Aufträge (`TestClaimDueJobsNeverHandsOutSameJobTwice`).
|
|
|
|
**Regeln stehen als CHECK in der Datenbank, nicht nur im Code:**
|
|
|
|
```sql
|
|
CONSTRAINT backup_job_runs_skipped_is_not_success CHECK (
|
|
files_skipped = 0 OR status <> 'succeeded'
|
|
)
|
|
```
|
|
|
|
Ein Lauf mit übergangenen Objekten kann nicht als Erfolg dastehen. Im Code müsste jede Stelle die Regel einhalten — und eine davon vergisst es. Ebenso abgesichert: Zusatzsicherung ohne Elternbackup, Selbstabhängigkeit, Bandbreitengrenze 0, Wartungsfenster mit einmaliger *und* wiederkehrender Angabe. Alle acht Regeln sind gegen die echte Datenbank geprüft.
|
|
|
|
**Aufträge werden weich gelöscht.** Ein hart gelöschter Auftrag risse seine Läufe mit — und damit den Nachweis, dass gesichert wurde.
|
|
|
|
## API
|
|
|
|
```
|
|
GET /api/v1/jobs jobs.read Filter: status, repository, search; Pagination
|
|
POST /api/v1/jobs jobs.write
|
|
GET /api/v1/jobs/{id} jobs.read
|
|
DELETE /api/v1/jobs/{id} jobs.write weich, immer auditiert
|
|
POST /api/v1/jobs/{id}/pause jobs.run auditiert
|
|
POST /api/v1/jobs/{id}/resume jobs.run auditiert, berechnet next_run_at neu
|
|
POST /api/v1/jobs/{id}/run jobs.run 202: reiht einen Lauf ein, auditiert
|
|
GET /api/v1/jobs/{id}/runs jobs.read Historie mit Pagination
|
|
POST /api/v1/backup-runs/{id}/cancel jobs.run bricht einen Lauf ab, auditiert
|
|
```
|
|
|
|
Beispiel:
|
|
|
|
```json
|
|
{
|
|
"name": "Nächtliche Sicherung",
|
|
"priority": "high",
|
|
"schedule": {"type": "daily", "time": "02:00", "time_zone": "Europe/Berlin"},
|
|
"sources": [
|
|
{"type": "filesystem", "id": "/daten", "exclude_patterns": ["*.tmp"]},
|
|
{"type": "proxmox_vm", "id": "qemu/100"}
|
|
],
|
|
"repository_id": "…",
|
|
"bandwidth_limit_bps": 52428800
|
|
}
|
|
```
|
|
|
|
Die Antwort enthält `schedule_description` („täglich um 02:00 Uhr (Europe/Berlin)"). Ein Cron-Ausdruck sagt einem Anwender wenig; die Erklärung entsteht auf dem Server, damit sie in Oberfläche und Benachrichtigung gleich lautet.
|
|
|
|
**Der nächste Zeitpunkt wird beim Anlegen sofort berechnet.** Ohne ihn stünde der Auftrag als aktiv da, ohne je zu laufen. Beim Fortsetzen wird er neu berechnet: Ein Auftrag, der eine Woche ausgesetzt war, liefe sonst sofort los und danach zur falschen Zeit weiter.
|
|
|
|
**Das Aussetzen wird auditiert wie eine Löschung.** Es lässt den Schutz still auslaufen, ohne dass etwas kaputtgeht — genau deshalb gehört es ins Protokoll.
|
|
|
|
### Widersprüchliche Schutzziele werden abgelehnt
|
|
|
|
```
|
|
VALIDATION_FAILED: der zeitplan läuft nur alle 1 Tage und kann den geforderten
|
|
wiederherstellungspunkt von 4 Stunden nicht einhalten
|
|
```
|
|
|
|
Ein RPO, den der Zeitplan nicht einhalten kann, ist keine Kleinigkeit: Der Betreiber glaubt, höchstens vier Stunden zu verlieren, während der Auftrag nur täglich läuft. Das gehört beim Anlegen gesagt, nicht nach dem Ausfall.
|
|
|
|
## Rettung nach abgebrochener Migration
|
|
|
|
Bricht eine Migration mitten in der Datei ab, merkt golang-migrate die Datenbank als „dirty" vor und verweigert jeden weiteren Schritt. Bisher gab es dafür keinen Weg zurück außer von Hand in der Datenbank — was ein Migrationswerkzeug gerade ersparen soll.
|
|
|
|
```bash
|
|
syncova-migrate status # zeigt den abgebrochenen Zustand
|
|
# Schema von Hand auf einen bekannten Stand bringen
|
|
SYNCOVA_MIGRATE_CONFIRM_FORCE=yes syncova-migrate force 3
|
|
```
|
|
|
|
**`force` führt kein SQL aus. Es behauptet nur einen Stand.** Wer es falsch anwendet, lässt die Anwendung gegen ein Schema arbeiten, das sie für ein anderes hält. Deshalb: ausdrückliche Bestätigung über die Umgebungsvariable, Ausgabe des Vorher/Nachher, und eine Ablehnung auf sauberer Datenbank — dort gibt es nichts zu retten, und die Folge wäre ein übersprungenes Schema.
|
|
|
|
### Warum 202 und nicht 201
|
|
|
|
`POST /jobs/{id}/run` reiht den Lauf ein; die Schleife holt ihn im nächsten Durchgang. Die Sicherung hat also noch nicht begonnen — 201 („angelegt") behauptete mehr, als geschehen ist. Ebenso beim Abbruch: Er wird vermerkt, ein bereits laufender Vorgang endet erst beim nächsten Durchgang. Auch das wird gesagt, statt einen beendeten Lauf vorzutäuschen.
|
|
|
|
Ein **ausgesetzter** Auftrag wird nicht heimlich reaktiviert: Wer ihn ausführen will, setzt ihn zuerst fort. Sonst liefe er einmal und schwiege danach wieder, ohne dass es jemandem auffiele.
|
|
|
|
Ein zweiter Anstoß bei laufendem Auftrag ergibt **409**, keinen Serverfehler: Der Aufrufer hat nichts falsch gemacht, der Auftrag läuft nur bereits.
|
|
|
|
Die Laufhistorie weist `delay_seconds` aus — die Abweichung zwischen geplantem und tatsächlichem Beginn. Ein Lauf, der regelmäßig eine Stunde zu spät beginnt, hat ein Problem, das man ohne diesen Vergleich nicht sieht.
|
|
|
|
## Offene Punkte
|
|
|
|
- **Aufbewahrung, Prüfung und Benachrichtigung** — im Wizard als „noch nicht verfügbar" gekennzeichnet, weil dahinter nichts liegt.
|
|
- **Nur Dateisystemquellen.** `proxmox_vm`, `windows_system` und `linux_system` werden abgelehnt, nicht stillschweigend übergangen — eine übergangene Quelle ergäbe ein Backup, das vollständig aussieht und es nicht ist. Für Proxmox fehlt der `ArchiveTransport` (siehe `proxmox.md`), für die Systemquellen die Auftragsübermittlung an den Agent.
|
|
- **Der Schreibweg zum Repository ist nicht begrenzt.** Bei einem lokalen Repository fällt das mit dem Lesen zusammen; für ein entferntes Ziel braucht es eine eigene Grenze.
|
|
- **Die Wiederherstellung ist nicht begrenzt.** Ein Restore kann die Leitung voll belegen.
|
|
- **`logical_bytes` einer Zusatzsicherung ist die gelesene, nicht die gesicherte Menge.** Bei einem Lauf ohne Änderungen steht dort 0, obwohl das Backup den vollen Bestand beschreibt. Für die Belegungsrechnung ist das irreführend und gehört korrigiert.
|
|
- **Keine Wiederherstellung über die API.** Sie geht nur über `syncova-agent restore`.
|
|
- **`PATCH /jobs/{id}`** — Ändern ist noch nicht umgesetzt; ein Auftrag muss gelöscht und neu angelegt werden.
|
|
- **Wartungsfenster nur im Code**, ohne API und ohne Anbindung an die Auftragsübernahme.
|
|
- **Kein `Idempotency-Key`** bei `POST /jobs` (SYNCOVA_API.md verlangt ihn).
|
|
- **Retention-Regeln** sind als Tabelle angelegt, aber ohne Auswertung.
|