Vérifier la cohérence des sauvegardes SQLite à chaud sur un Mac cloud
Un client demande d’ajouter une fonction « Exporter la base de données » à la version livrée de son application, sans interrompre la saisie des commandes. Sur un Mac cloud, le studio vérifie que le fichier peut être téléchargé, mais constate qu’il manque quelques enregistrements après restauration. Le transfert n’est pas forcément en cause : le problème peut venir de la copie directe du fichier principal d’une base SQLite en cours d’utilisation. Il faut d’abord choisir une méthode de sauvegarde cohérente, puis vérifier la restauration. Une copie réussie ne suffit pas à valider la livraison.
Choisir une API plutôt que copier les fichiers actifs
L’API adaptée à l’export à chaud est sqlite3_backup : elle lit la base source par l’intermédiaire d’une connexion, écrit son contenu dans une base cible et permet de copier les pages par lots. Une simple copie de fichiers ne convient pas à une base active, surtout en mode WAL, où des données déjà validées peuvent encore se trouver dans le journal.
Copier successivement le fichier principal et les fichiers WAL et SHM qui l’accompagnent ne garantit pas non plus qu’ils correspondent au même instant. Les essais en ligne de commande sur le Mac cloud servent à vérifier le comportement de la base. Pour l’intégration à iOS, il faut reprendre le chemin de base de données et la gestion des connexions déjà utilisés par l’application.
Une sauvegarde à chaud garantit la cohérence de la base de données, pas l’achèvement des opérations métier. Si l’ajout d’une commande et de ses lignes s’effectue en plusieurs transactions, la sauvegarde peut encore intervenir entre deux transactions.
Il faut donc définir d’abord le contrat de livraison : accepte-t-on n’importe quel état cohérent, ou faut-il l’état obtenu après la fin d’un lot précis ? Dans le second cas, une barrière de synchronisation métier doit être associée à une stratégie explicite de création d’instantanés, plutôt que de compter sur le moment supposé de la copie.
Créer en Swift un outil de sauvegarde à durée limitée
L’exemple suivant ne sauvegarde que la base main. La destination doit se trouver dans un répertoire propre à la tâche et ne doit pas encore exister ; le fichier source, lui, doit déjà exister. Enregistrez le code dans backup.swift, puis exécutez-le sur un Mac cloud équipé des outils en ligne de commande.
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;'
Le budget total de dix secondes est destiné à cet essai ; ce n’est pas une norme de service universelle. Aucun autre thread ne doit utiliser simultanément la connexion cible. L’exemple n’installe pas de gestionnaire d’attente en cas de base occupée ; si l’application en configure un, les attentes effectuées dans ce gestionnaire doivent elles aussi être prises en compte dans le budget.
Valider les limites transactionnelles avec des écritures concurrentes
Préparez deux tables : une table de lots contenant le numéro de lot et le nombre de lignes attendu, et une table de détails faisant référence au numéro de lot. À chaque itération, le processus d’écriture insère un lot et un nombre fixe de lignes de détail dans une seule transaction, puis la valide. Une autre connexion lance des sauvegardes répétées, chacune vers un nouveau chemin cible.
Pour chaque sauvegarde, vérifiez que le nombre annoncé par chaque lot correspond au nombre réel de lignes de détail, qu’aucune ligne de détail n’est orpheline et qu’aucun numéro de lot n’est dupliqué. Les instantanés peuvent contenir des nombres différents de lots complets, mais jamais une transaction partielle.
Ne pas confondre progression et instantané figé
Pendant une sauvegarde par étapes, les écritures effectuées par d’autres connexions peuvent faire repartir la sauvegarde de zéro. Le nombre de pages copiées ne progresse donc pas nécessairement de façon monotone, et rien ne permet d’affirmer que le résultat correspond à l’état de la base au début de l’appel.
Si un point de lecture fixe est requis, ouvrez explicitement une transaction de lecture sur la connexion source et effectuez une lecture pour établir l’instantané, puis lancez la sauvegarde. Libérez rapidement la transaction une fois l’opération terminée. En mode WAL, une transaction de lecture prolongée peut empêcher le point de contrôle d’avancer : surveillez aussi la croissance du journal. Les effets des verrous doivent également être testés séparément pour les différents modes de journalisation.
Vérifier qu’un échec ne peut pas conduire à une livraison invalide
Dans un répertoire de test jetable, testez trois types d’échecs : un répertoire cible non accessible en écriture, un verrou d’écriture maintenu longtemps par une autre connexion et l’arrêt du processus de sauvegarde en cours d’exécution. Les essais de manque d’espace doivent être réalisés uniquement sur un volume de test contrôlé ; ne saturez pas un disque qui héberge les tâches d’autres clients.
Dans chaque cas, le processus doit se terminer avec un statut d’échec, et le répertoire de livraison ne doit contenir aucun fichier susceptible d’être pris pour une sauvegarde complète. L’exemple ne supprime la cible que lorsqu’une erreur est renvoyée par le déroulement normal du programme. Un arrêt forcé du processus empêche ce nettoyage : dans l’intégration réelle, il faut donc écrire d’abord dans un fichier temporaire, puis ne le publier qu’après validation.
integrity_check doit renvoyer uniquement ok ; foreign_key_check ne doit renvoyer aucune ligne. Même si les deux contrôles réussissent, il reste à vérifier les totaux métier et user_version : une structure valide ne garantit pas la justesse des données métier.
Terminer par une restauration depuis un chemin indépendant
Fermez la connexion de sauvegarde, copiez le résultat dans un nouveau répertoire propre à la tâche et rouvrez-le sans les fichiers WAL et SHM de la base source. Commencez par des requêtes en lecture seule, puis effectuez des ajouts, des modifications, des suppressions et une nouvelle ouverture sur une copie jetable, afin de confirmer que l’application peut réellement continuer à utiliser la base.
Conservez dans le compte rendu de livraison l’identifiant du commit source, la version de la base, la durée de sauvegarde, l’empreinte du fichier et les résultats des contrôles, mais aucune donnée client en clair. Lors de la restauration dans l’application, fermez également toutes les anciennes connexions au préalable, pour éviter que certaines continuent à manipuler l’ancienne base après la remise en place du nouveau fichier.
Enfin, intégrez aux tests de non-régression les deux situations : « sans écritures concurrentes » et « avec écritures continues ». L’objectif n’est pas d’obtenir le même fichier à chaque exécution, mais une base qui s’ouvre de manière indépendante et respecte les contraintes transactionnelles et métier à chaque fois. En cas d’échec, la publication doit être explicitement refusée.
Questions fréquentes
Peut-on copier uniquement le fichier principal en mode WAL ?
Pas pour une sauvegarde à chaud fiable. Des données validées peuvent encore se trouver dans le WAL. Utilisez l'API de sauvegarde SQLite ou une autre méthode dont la cohérence a été vérifiée.
Un résultat positif à integrity_check suffit-il ?
Non. Exécutez aussi foreign_key_check, vérifiez les invariants métier et la version du schéma, puis testez l'ouverture et la restauration depuis un emplacement indépendant.
La sauvegarde représente-t-elle toujours l'instant de son lancement ?
Non. Les écritures d'autres connexions peuvent relancer la progression d'une sauvegarde incrémentale. Pour un instantané fixe, concevez explicitement la transaction de lecture et évaluez son effet sur la taille du WAL.
Adaptez la durée de location au calendrier de votre projet
Idéal pour les builds ponctuels, les pics d’activité avant livraison et les projets menés pour plusieurs clients. Si votre équipe compile à plein régime toute l’année et possède déjà son matériel, comparez d’abord l’achat et la location avant de choisir.
VMSSD Mac cloud propose des Mac mini physiques dédiés, et non des machines virtuelles. Choisissez une durée à la journée, à la semaine, au mois ou au trimestre. La location s’arrête à l’échéance sans renouvellement ; les modèles et les nœuds disponibles sont indiqués en temps réel dans la console.