SQLite-WAL-Wiederherstellung nach einem iOS-Absturz testen

SQLite-WAL-Wiederherstellung nach einem iOS-Absturz testen

Eine iOS-App speichert Offline-Entwürfe in SQLite. Nachdem die App „Erfolgreich gespeichert“ angezeigt hat, stürzt der Prozess beim nächsten Schreibvorgang ab. Ist der Entwurf nach dem Neustart noch vorhanden? Auf einem Cloud-Mac mit einem eigens dafür vorgesehenen Simulator lässt sich daraus ein reproduzierbarer Test machen: Zuerst wird ein Referenzeintrag festgeschrieben. Dann wird während einer zweiten, noch offenen Transaktion gezielt ein Absturz ausgelöst. Anschließend wird die Datenbank direkt geprüft. Getestet wird die Transaktionsgrenze, nicht die Build-Geschwindigkeit. Dass sich die App wieder öffnen lässt, ersetzt keine Datenprüfung.

Erfolgskriterien festlegen

Legen Sie eine Testtabelle mit nur zwei Spalten an: id und phase. Der Eintrag 1|committed muss vor dem Absturz festgeschrieben sein. 2|pending muss innerhalb einer neu begonnenen Transaktion geschrieben werden, darf aber nicht festgeschrieben werden. Nach dem Absturz muss die Integritätsprüfung ok zurückgeben; die Abfrage darf nur den ersten Eintrag liefern. Ist auch der zweite vorhanden, prüfen Sie zuerst, ob der Absturz versehentlich erst nach dem Commit ausgelöst wurde. Fehlt der erste, prüfen Sie, ob er tatsächlich in dieselbe Datenbankdatei geschrieben wurde.

WAL ist keine Zusage, dass niemals Schreibvorgänge verloren gehen. Dieser Test verlangt lediglich, dass erfolgreich festgeschriebene Transaktionen nach diesem Prozessabsturz lesbar bleiben und nicht festgeschriebene Transaktionen nicht als gespeicherte Daten erscheinen. Tests für Stromausfälle, Speicherschäden oder die Synchronisierung auf Anwendungsebene ersetzt er nicht.

Verwenden Sie für den Test einen eigens angelegten Simulator und eine Test-Bundle-ID. Legen Sie vorab die Xcode-Version und die Simulator-Runtime fest und protokollieren Sie die Ausgabe von xcodebuild -version sowie die Simulator-ID. Führen Sie jeden Durchlauf unter denselben Bedingungen aus. Injizieren Sie den Fehler weder in einem persönlich genutzten Entwicklungssimulator noch in einer Datenbank mit echten Nutzerdaten.

Absturz an der Commit-Grenze auslösen

Beschränken Sie den Einstiegspunkt für die Fehlerinjektion auf Debug-Builds und verlangen Sie einen expliziten Startparameter, damit er beim normalen Start nicht versehentlich ausgeführt wird. Die folgende Funktion setzt voraus, dass der Aufrufer Documents/Recovery.sqlite bereits geöffnet hat und der Rückgabewert jedes SQL-Aufrufs geprüft wird. Im Beispiel macht precondition Fehler bei der Vorbereitung sofort sichtbar, statt anschließend irreführende Abfrageergebnisse zu erzeugen.

#if DEBUG
import Darwin
import SQLite3

func runRecoveryProbe(_ db: OpaquePointer) {
    func execute(_ sql: String) {
        precondition(sqlite3_exec(db, sql, nil, nil, nil) == SQLITE_OK)
    }

    execute("PRAGMA journal_mode=WAL")
    execute("CREATE TABLE IF NOT EXISTS probe (id INTEGER PRIMARY KEY, phase TEXT NOT NULL)")
    execute("INSERT OR REPLACE INTO probe VALUES (1, 'committed')")
    execute("BEGIN IMMEDIATE")
    execute("INSERT OR REPLACE INTO probe VALUES (2, 'pending')")
    abort()
}
#endif

Rufen Sie die Funktion nur auf, wenn der Startparameter die eigens dafür vorgesehene Testmarkierung enthält. Der Testeinstiegspunkt muss zuvor bestätigen, dass die Testdatenbank verwendet wird und keine frühere Transaktion mehr offen ist. PRAGMA journal_mode=WAL muss außerhalb einer Transaktion ausgeführt werden. abort() beendet den Prozess in diesem Durchlauf mit einem Absturz; deshalb steht danach bewusst kein COMMIT. Nutzt das reale Projekt eine Datenbank-Abstraktionsschicht, stellen Sie dieselbe Commit-Grenze über diese Schicht her. Wer sie umgeht, kann nicht behaupten, damit den produktiven Schreibpfad geprüft zu haben.

Verunreinigung durch alte Einträge vermeiden

Setzen Sie vor jedem Durchlauf die Daten der Test-App im eigens angelegten Simulator zurück oder installieren Sie die App auf einem neuen, ausschließlich für den Test vorgesehenen Simulator. Führen Sie nicht bloß DELETE FROM probe aus, während andere Anwendungstabellen und Dateien erhalten bleiben. Prüfen Sie vor dem ersten Start, ob die Datenbank tatsächlich im Datencontainer der App liegt. Protokollieren Sie vor und nach der Fehlerinjektion den Datenbankpfad, die Build-Version und die Simulator-ID. Kopieren Sie keine echten Projektdaten in den Testcontainer.

Datenbank nach dem Absturz unverändert prüfen

Bestätigen Sie nach dem Start der App zunächst, dass sie aufgrund des vorgesehenen Fehlers abgestürzt ist. Erst danach folgt die Abfrage. Dass der Startbefehl zurückkehrt, beweist noch keinen Absturz: Möglicherweise hat die App den Testeinstiegspunkt gar nicht erreicht. Gleichen Sie dazu den Prozessstatus im Simulator mit dem Absturzbericht ab. Ein Durchlauf ohne Absturznachweis gilt als „Test nicht abgeschlossen“, nicht als bestanden.

Sobald der Prozess beendet ist, lesen Sie den App-Datencontainer im Terminal des Cloud-Mac aus. Ersetzen Sie die Variablen durch die Simulator-ID dieses Durchlaufs und die Test-Bundle-ID:

UDID="your-simulator-udid"
BUNDLE_ID="com.example.RecoveryProbe"
DATA="$(xcrun simctl get_app_container "$UDID" "$BUNDLE_ID" data)"
sqlite3 "$DATA/Documents/Recovery.sqlite" \
  'PRAGMA integrity_check; SELECT id, phase FROM probe ORDER BY id;'

Die erwartete Ausgabe lautet nacheinander ok und 1|committed. Löschen oder verschieben Sie vor der Prüfung die Dateien -wal und -shm neben der Datenbank nicht einzeln. Lassen Sie SQLite die Datenbank im vorgefundenen Zustand öffnen. Starten Sie während der Prüfung auch keinen App-Prozess, der erneut in die Datenbank schreibt. Falls das Projekt mehrere Datenbanken verwendet, gleichen Sie zunächst Dateinamen und Containerpfad ab, damit eine Abfrage der falschen Datei nicht als fehlgeschlagene Wiederherstellung gilt.

Fehler nach Ursache eingrenzen

Enthält die Ausgabe 2|pending, prüfen Sie zuerst, ob ein automatischer Commit stattgefunden hat, die Abstraktionsschicht die Transaktion vorzeitig beendet hat oder die Fehlermarkierung einen anderen Codepfad aktiviert hat. Fehlt 1|committed, prüfen Sie, ob der erste Schreibvorgang erfolgreich war und ob der Test dieselbe Verbindung und dieselbe Datei verwendet hat. Meldet die Abfrage, dass die Tabelle nicht existiert, prüfen Sie zuerst, ob der installierte Build den Testeinstiegspunkt enthält und ob der Containerpfad nicht auf den Simulator des vorigen Durchlaufs verweist.

integrity_check ist keine Prüfung der fachlich erwarteten Daten: Selbst bei ok müssen die Einträge einzeln abgeglichen werden. Umgekehrt belegt ein Absturzbericht nur das anormale Ende des Prozesses, nicht die korrekte Wiederherstellung der Transaktion. Bewahren Sie beide Arten von Nachweisen zusammen mit der Xcode-Version, der Simulator-ID und der Build-Kennung der Test-App auf. Nur so lassen sich reproduzierte Fehler unter vergleichbaren Bedingungen untersuchen.

Testgrenzen in der CI-Pipeline wahren

Reservieren Sie für diesen Test einen eigenen Simulator. Führen Sie die schreibgeschützte Prüfung erst aus, nachdem die Fehlerinjektion beendet und das Ende des Prozesses bestätigt wurde. Teilen Sie den App-Datencontainer nicht mit regulären UI-Tests: Parallele Schreibvorgänge verändern das beobachtete Ergebnis. Der Einstiegspunkt für die Fehlerinjektion darf nur in Debug-Builds vorhanden sein, nicht im Release-Build. Die Pipeline sollte dies ausdrücklich prüfen.

Führen Sie zum Schluss einen Kontrolldurchlauf ohne injizierten Absturz aus: Schreiben Sie den zweiten Eintrag über den normalen Pfad fest und prüfen Sie, ob er lesbar ist. So bestätigen Sie sowohl, dass nicht festgeschriebene Schreibvorgänge zurückgerollt werden, als auch, dass der Test nicht sämtliche Schreibvorgänge verhindert. Erst mit beiden Ergebnissen wird aus einem zufällig beobachteten Neustart eine überprüfbare Abnahme der Datenpersistenz.

Häufig gestellte Fragen

Reicht das reguläre Beenden der App für diesen Test?

Nein. Beim regulären Beenden können Transaktionen abgeschlossen werden. Der Test muss vor dem Commit abbrechen und den tatsächlichen Prozessabsturz bestätigen.

Dürfen die Dateien -wal und -shm vor der Prüfung gelöscht werden?

Nein. Lassen Sie die Dateien unverändert und öffnen Sie die Datenbank mit SQLite, um Integrität und Datensätze zu prüfen.

UDPMac Cloud-Mac

Führen Sie Ihren nächsten Xcode-Build auf einer dedizierten physischen Maschine aus.

Wählen Sie Apple-Silicon-Modell, Standort und Laufzeit und führen Sie Ihre eigenen Build-Prozesse über eine Remote-Verbindung aus. Die tatsächliche Verfügbarkeit sehen Sie beim Bestellen in der Konsole.

Cloud-Mac-Mietoptionen ansehen