Konsistente SQLite-Online-Backups für iOS auf dem Cloud-Mac prüfen

Konsistente SQLite-Online-Backups für iOS auf dem Cloud-Mac prüfen

Konsistente SQLite-Online-Backups für iOS auf dem Cloud-Mac prüfen

Ein Kunde möchte die auszuliefernde App um einen Datenbankexport erweitern, ohne dafür die Auftragserfassung unterbrechen zu müssen. Das Entwicklerteam prüft auf einem Cloud-Mac, ob sich die Datei herunterladen lässt. Das funktioniert – doch nach der Wiederherstellung fehlen einige Datensätze. Die Ursache liegt nicht zwangsläufig in der Übertragung: Möglicherweise wurde einfach die Hauptdatei einer laufenden SQLite-Datenbank kopiert. Zuerst muss ein konsistentes Sicherungsverfahren festgelegt und anschließend die Wiederherstellung geprüft werden. „Erfolgreich kopiert“ reicht als Abnahmekriterium nicht aus.

Die Backup-Schnittstelle wählen, statt aktive Dateien zu kopieren

Für einen Export im laufenden Betrieb eignet sich sqlite3_backup: Die Schnittstelle liest die Quelldatenbank über eine Datenbankverbindung, schreibt den Inhalt in die Zieldatenbank und unterstützt das schrittweise Kopieren von Seiten. Eine gewöhnliche Dateikopie einer aktiven Datenbank ist dagegen ungeeignet. Insbesondere im WAL-Modus können bereits bestätigte Änderungen noch in der Protokolldatei liegen.

Auch wenn Hauptdatei, WAL und SHM nacheinander kopiert werden, ist nicht gewährleistet, dass sie denselben Zeitpunkt abbilden. Die Kommandozeilenversuche auf dem Cloud-Mac dienen dazu, das Verhalten der Datenbank zu prüfen. Bei der Integration in iOS müssen die vorhandenen Datenbankpfade und die Verbindungsverwaltung der App verwendet werden.

Ein Online-Backup gewährleistet Datenbankkonsistenz, nicht den Abschluss eines Geschäftsprozesses. Werden ein Auftrag und seine Positionen über mehrere Transaktionen hinweg angelegt, kann das Backup weiterhin zwischen zwei dieser Transaktionen entstehen.

Deshalb muss zunächst die Anforderung an die Lieferung eindeutig sein: Genügt ein beliebiger konsistenter Zustand, oder wird der Zustand nach Abschluss eines bestimmten Batches benötigt? Im zweiten Fall sind eine Synchronisationsbarriere auf Anwendungsebene und eine klar definierte Snapshot-Strategie erforderlich – nicht die Hoffnung, den richtigen Kopierzeitpunkt zu treffen.

Einen zeitlich begrenzten Backup-Prozess in Swift implementieren

Das folgende Beispiel sichert ausschließlich die Datenbank main. Die Zieldatei liegt in einem eigenen Verzeichnis für den jeweiligen Durchlauf und darf noch nicht existieren; die Quelldatei muss bereits vorhanden sein. Den Code als backup.swift speichern und auf einem Cloud-Mac mit installierten Kommandozeilenwerkzeugen ausführen.

import Foundation
import SQLite3

enum BackupFailure: Error {
    case invalidArguments
    case sqlite(Int32)
    case timeout
}

func openDB(_ path: String, flags: Int32) throws -> OpaquePointer {
    var db: OpaquePointer?
    let rc = sqlite3_open_v2(path, &db, flags, nil)
    guard rc == SQLITE_OK, let handle = db else {
        if let db = db { sqlite3_close(db) }
        throw BackupFailure.sqlite(rc)
    }
    return handle
}

func backup(source: String, target: String) throws {
    guard !FileManager.default.fileExists(atPath: target) else {
        throw BackupFailure.invalidArguments
    }
    let src = try openDB(source, flags: SQLITE_OPEN_READONLY)
    defer { sqlite3_close(src) }
    let dst = try openDB(
        target,
        flags: SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE
    )
    var complete = false
    defer {
        sqlite3_close(dst)
        if !complete {
            try? FileManager.default.removeItem(atPath: target)
        }
    }
    guard let job = sqlite3_backup_init(dst, "main", src, "main") else {
        throw BackupFailure.sqlite(sqlite3_errcode(dst))
    }
    let deadline = ProcessInfo.processInfo.systemUptime + 10
    var rc: Int32 = SQLITE_OK
    repeat {
        if ProcessInfo.processInfo.systemUptime >= deadline {
            rc = SQLITE_INTERRUPT
            break
        }
        rc = sqlite3_backup_step(job, 64)
        if rc == SQLITE_OK || rc == SQLITE_BUSY || rc == SQLITE_LOCKED {
            Thread.sleep(forTimeInterval: 0.02)
        }
    } while rc == SQLITE_OK || rc == SQLITE_BUSY || rc == SQLITE_LOCKED
    let finish = sqlite3_backup_finish(job)
    guard rc == SQLITE_DONE else {
        if rc == SQLITE_INTERRUPT { throw BackupFailure.timeout }
        throw BackupFailure.sqlite(rc)
    }
    guard finish == SQLITE_OK else {
        throw BackupFailure.sqlite(finish)
    }
    complete = true
}

guard CommandLine.arguments.count == 3 else {
    throw BackupFailure.invalidArguments
}
try backup(source: CommandLine.arguments[1], target: CommandLine.arguments[2])
xcrun swiftc backup.swift -o backup -lsqlite3
mkdir -m 700 backup-run
./backup source.sqlite backup-run/snapshot.sqlite
sqlite3 backup-run/snapshot.sqlite \
  'PRAGMA integrity_check; PRAGMA foreign_key_check;'

Die zehn Sekunden sind das Gesamtzeitbudget für diesen Versuch, kein allgemein gültiger Servicestandard. Die Zielverbindung darf nicht gleichzeitig von anderen Threads verwendet werden. Im Beispiel ist kein Busy-Handler eingerichtet. Konfiguriert die App einen eigenen Handler, müssen auch dessen interne Wartezeiten in das Budget einfließen.

Transaktionsgrenzen unter parallelen Schreibzugriffen prüfen

Für den Test werden zwei Tabellen angelegt: Eine Batch-Tabelle enthält die Batch-ID und die erwartete Anzahl der Positionen; eine Positionstabelle verweist auf die Batch-ID. Der Schreibprozess fügt in jedem Durchlauf innerhalb einer einzigen Transaktion einen Batch mit einer festen Anzahl von Positionen ein und bestätigt anschließend die Transaktion. Über eine weitere Verbindung werden wiederholt Backups erstellt, jeweils unter einem neuen Zielpfad.

Für jedes Backup wird geprüft, ob die im Batch angegebene Anzahl mit der tatsächlichen Anzahl der Positionen übereinstimmt, ob verwaiste Positionen fehlen und ob jede Batch-ID eindeutig ist. Die Snapshots dürfen unterschiedlich viele vollständige Batches enthalten. Eine nur teilweise enthaltene Transaktion ist dagegen unzulässig.

Fortschrittsanzeigen sind keine eingefrorenen Snapshots

Während eines schrittweisen Backups können weitere Schreibzugriffe über andere Verbindungen dazu führen, dass der Sicherungsvorgang von vorn beginnt. Der Fortschritt anhand der Seitenzahl muss deshalb nicht monoton steigen. Ebenso wenig lässt sich daraus ableiten, dass das Ergebnis den Zustand zum Zeitpunkt des Aufrufs abbildet.

Ist ein fester Lesezeitpunkt erforderlich, kann auf der Quellverbindung ausdrücklich eine Lesetransaktion begonnen und durch einen Lesezugriff ein Snapshot etabliert werden. Anschließend wird das Backup erstellt und die Transaktion zeitnah beendet. Im WAL-Modus können lange Lesetransaktionen den Checkpoint-Fortschritt behindern; daher muss auch das Wachstum der Protokolldatei beobachtet werden. Die Auswirkungen auf Sperren sind außerdem für die verschiedenen Journal-Modi separat zu testen.

Fehlertests müssen eine irrtümliche Auslieferung ausschließen

In einem eigens angelegten, anschließend verwerfbaren Testverzeichnis werden drei Fehlerfälle geprüft: ein nicht beschreibbares Zielverzeichnis, eine dauerhaft von einer anderen Verbindung gehaltene Schreibsperre und ein während des Backups beendeter Prozess. Tests mit zu wenig Speicherplatz gehören ausschließlich auf ein kontrolliertes Testvolume. Eine Festplatte, auf der auch Aufträge anderer Kunden laufen, darf dafür nicht gefüllt werden.

In allen drei Fällen muss der Prozess mit einem Fehlerstatus enden. Im Auslieferungsverzeichnis darf keine Datei verbleiben, die irrtümlich als vollständiges Backup gelten könnte. Das Beispiel löscht die Zieldatei nur bei einer regulären Fehlerrückgabe. Wird der Prozess zwangsweise beendet, findet diese Bereinigung nicht statt. In der tatsächlichen Integration sollte daher zunächst in eine temporäre Datei geschrieben und das Ergebnis erst nach erfolgreicher Prüfung bereitgestellt werden.

integrity_check darf ausschließlich ok zurückgeben; foreign_key_check darf keine Ergebniszeilen liefern. Auch wenn beide Prüfungen erfolgreich sind, müssen die fachlichen Summen und user_version geprüft werden. Eine intakte Datenbankstruktur bedeutet noch keine fachlich korrekten Daten.

Erst die Wiederherstellung unter einem unabhängigen Pfad schließt den Test ab

Nach dem Schließen der Backup-Verbindung wird das Ergebnis in ein neues Verzeichnis für den Wiederherstellungstest kopiert und dort ohne WAL- und SHM-Dateien der Quelldatenbank erneut geöffnet. Zunächst werden ausschließlich lesende Abfragen ausgeführt. Anschließend werden auf einer verwerfbaren Kopie Datensätze angelegt, geändert und gelöscht sowie die Datenbank erneut geöffnet. So lässt sich bestätigen, dass die App tatsächlich damit weiterarbeiten kann.

Im Auslieferungsprotokoll werden die Commit-ID des Quellcodes, die Datenbankversion, die Backup-Dauer, der Datei-Hash und die Prüfergebnisse festgehalten – nicht jedoch unverschlüsselte Kundendaten. Bei der Wiederherstellung durch die App müssen außerdem zuerst alle alten Verbindungen geschlossen werden. Andernfalls könnten Verbindungen nach dem Austausch der Datei weiterhin auf die alte Datenbank zugreifen.

Zum Abschluss gehören sowohl der Betrieb ohne parallele Schreibzugriffe als auch der Betrieb mit kontinuierlichen Schreibzugriffen in die Regressionstests. Das Abnahmeziel ist nicht, jedes Mal dieselbe Datei zu erhalten. Entscheidend ist, dass jede Sicherung unabhängig geöffnet werden kann und die Transaktionsgrenzen sowie die fachlichen Bedingungen einhält. Bei einem Fehler muss die Bereitstellung ausdrücklich verweigert werden.

Häufig gestellte Fragen

Reicht es im WAL-Modus, die Hauptdatei zu kopieren?

Nicht für ein zuverlässiges Online-Backup. Bestätigte Änderungen können noch im WAL liegen. Verwenden Sie die SQLite-Backup-API oder ein anderes Verfahren mit nachgewiesener Konsistenz.

Genügt ein erfolgreicher integrity_check?

Nein. Prüfen Sie zusätzlich foreign_key_check, fachliche Invarianten und die erwartete Schemaversion. Testen Sie außerdem das Öffnen und Wiederherstellen aus einem unabhängigen Verzeichnis.

Entspricht das Backup immer dem Zustand beim Aufruf?

Nein. Schreibzugriffe anderer Verbindungen können den Fortschritt eines schrittweisen Backups zurücksetzen. Für einen festen Snapshot müssen Sie die Lesetransaktion der Quelle planen und das WAL-Wachstum berücksichtigen.

Mietdauer passend zum Projektplan wählen

Ideal für kurzfristige Builds, zusätzliche Kapazität in der Release-Woche und getrennte Umgebungen für mehrere Kundenprojekte. Wenn Ihr Team dauerhaft ausgelastet ist und bereits eigene Geräte nutzt, vergleichen Sie Kauf und Miete, bevor Sie sich entscheiden.

VMSSD bietet Ihnen dedizierte physische Mac mini – keine virtuellen Maschinen. Wählen Sie eine Mietdauer von einem Tag, einer Woche, einem Monat oder einem Quartal. Ohne Verlängerung endet die Miete automatisch. Verfügbarkeit von Modellen und Standorten sehen Sie aktuell in der Konsole.

Aus dem Lesen den nächsten Schritt machen

Prüfen Sie zunächst Projektdauer und Build-Auslastung. Wählen Sie anschließend Modell und Mietdauer. Wenn Sie zum ersten Mal mieten, starten Sie mit der Bestell-Checkliste.