La synchronisation en arrière-plan a bien validé de nouveaux enregistrements, mais la liste affiche toujours les anciennes valeurs après actualisation. Reconstruire le cache fait disparaître le problème temporairement, puis il réapparaît à la prochaine lecture par lot. Dans ce cas, vérifiez d’abord si les lectures s’effectuent encore dans la même transaction SQLite. Reproduire le scénario avec deux connexions indépendantes sur un Mac cloud permet de distinguer plus facilement le comportement de la base d’un problème de cache d’interface qu’en multipliant les manipulations sur le bureau à distance.
Définir d’abord le contrat de lecture à respecter
Cet article traite uniquement du mode WAL d’une base locale stockée dans un fichier. Ses conclusions ne s’appliquent ni aux bases en mémoire ni aux configurations à cache partagé. Préparez un environnement Python 3 avec un module SQLite opérationnel, puis exécutez python3 -c 'import sqlite3; print(sqlite3.sqlite_version)'. Conservez la sortie avec les résultats du test.
Le critère d’acceptation est le suivant : après la première requête de la connexion de lecture, la connexion d’écriture peut valider sa transaction ; l’ancienne transaction de lecture voit toujours l’ancienne valeur ; une nouvelle requête ne voit la nouvelle valeur qu’une fois cette transaction de lecture terminée. Ici, « ancienne » désigne une vue cohérente de la transaction, pas une perte de données.
Ne validez pas arbitrairement une transaction pour rendre une liste « plus à jour » lorsqu’une lecture portant sur plusieurs tables exige une vue cohérente. Déterminez d’abord si le besoin métier impose un même snapshot ou si chaque requête doit lire les dernières données validées.
Le test doit utiliser un véritable fichier temporaire et deux connexions. Deux connexions :memory: ordinaires disposent chacune de leur propre base et ne permettent pas de vérifier ces interactions.
Construire une expérience minimale à deux connexions
Enregistrez le contenu ci-dessous dans snapshot_check.py, puis exécutez python3 snapshot_check.py. Le script ne se connecte pas à la base de l’application, et le répertoire temporaire est supprimé automatiquement à la sortie. Tous les curseurs sont immédiatement consommés jusqu’au bout puis fermés, afin de ne pas confondre une instruction encore active avec un problème de transaction explicite.
import sqlite3
import tempfile
from pathlib import Path
def one(db, sql):
cursor = db.execute(sql)
try:
return cursor.fetchone()
finally:
cursor.close()
with tempfile.TemporaryDirectory() as root:
path = Path(root) / "snapshot.sqlite"
writer = sqlite3.connect(path, isolation_level=None)
reader = sqlite3.connect(path, isolation_level=None)
try:
assert one(writer, "PRAGMA journal_mode=WAL")[0] == "wal"
writer.execute("PRAGMA wal_autocheckpoint=0")
writer.execute("CREATE TABLE item(id INTEGER PRIMARY KEY, value TEXT)")
writer.execute("INSERT INTO item VALUES(1, 'old')")
one(writer, "PRAGMA wal_checkpoint(TRUNCATE)")
reader.execute("BEGIN")
assert one(reader, "SELECT value FROM item WHERE id=1")[0] == "old"
writer.execute("BEGIN IMMEDIATE")
writer.execute("UPDATE item SET value='new' WHERE id=1")
writer.execute("COMMIT")
assert one(reader, "SELECT value FROM item WHERE id=1")[0] == "old"
held = one(writer, "PRAGMA wal_checkpoint(PASSIVE)")
assert held[1] > held[2], held
reader.execute("COMMIT")
assert one(reader, "SELECT value FROM item WHERE id=1")[0] == "new"
released = one(writer, "PRAGMA wal_checkpoint(PASSIVE)")
assert released[1] == released[2], released
print({"held": held, "released": released, "result": "passed"})
finally:
if reader.in_transaction:
reader.rollback()
if writer.in_transaction:
writer.rollback()
reader.close()
writer.close()
Le code désactive explicitement les checkpoints automatiques uniquement pour rendre le déroulement de l’expérience observable. Ce n’est pas une recommandation de configuration pour la production. De même, la troncature après l’initialisation ne concerne que cette base de test à usage exclusif : ne la reproduisez pas sur une base qui traite des tâches en cours.
Distinguer BEGIN de la première lecture
Un simple BEGIN ouvre une transaction différée. Il ne faut donc pas assimiler l’instant d’exécution de cette instruction à celui du snapshot. Dans l’exemple ci-dessus, c’est la première requête qui établit la vue de lecture.
Essayez ensuite une variante indépendante : déplacez l’écriture et sa validation avant la première requête de la connexion de lecture. Cette première requête, comme les suivantes au sein de la même transaction, doit alors renvoyer new. Adaptez les assertions en conséquence.
Cette variante permet de déceler une erreur d’interprétation fréquente : le journal indique « début de transaction », et l’on suppose que toutes les lectures suivantes reflètent cet instant. Pour le diagnostic, consignez au minimum l’identifiant de connexion, l’identifiant de transaction, le point de la première requête et celui de la fin de transaction, plutôt que la seule entrée dans une méthode.
Plusieurs requêtes doivent-elles partager une vue cohérente ?
Si vous interrogez d’abord le nombre total de commandes, puis leurs détails, et que ces deux résultats doivent appartenir au même snapshot métier, placez les requêtes dans une courte transaction de lecture. En revanche, si chaque chargement au fil du défilement doit récupérer les données les plus récentes, n’englobez pas tout le cycle de vie de la page dans une seule transaction. Les critères d’acceptation découlent du périmètre métier, pas de la solution qui semble afficher les mises à jour le plus vite.
Suivre la progression des checkpoints, pas seulement la taille du fichier
wal_checkpoint(PASSIVE) renvoie trois entiers : l’état d’occupation, le nombre de frames du journal et le nombre de frames déjà transférées par checkpoint. Dans la base de test, l’ancienne transaction de lecture a encore besoin de la vue antérieure ; certaines nouvelles frames ne peuvent donc pas être transférées par checkpoint. Une fois la transaction de lecture terminée, un nouvel appel permet de progresser.
N’écrivez pas d’assertion imposant un nombre exact de frames pour une opération donnée. La taille des pages, l’organisation de la base et la version de la bibliothèque utilisée à l’exécution influencent ce nombre. Le test doit vérifier des relations entre les valeurs.
De même, lorsque le nombre de frames transférées par checkpoint rejoint celui des frames du journal, cela ne signifie pas que le fichier WAL rétrécit immédiatement. Son espace peut être conservé pour être réutilisé. En production, relevez conjointement la durée des transactions de lecture, les résultats des checkpoints et l’évolution des écritures. Ne supprimez pas le journal au seul motif qu’un gros fichier apparaît dans le répertoire.
Si la deuxième assertion échoue, recherchez d’abord les transactions de lecture, curseurs ou autres connexions encore ouverts, avant d’analyser la stratégie de checkpoint. Ne commencez pas par ajouter une boucle qui force la troncature : cela masquerait la véritable cause, une transaction de lecture qui reste ouverte trop longtemps.
Intégrer le dispositif aux tests de la couche de données iOS
L’expérience exécutée sur l’hôte vérifie la version de SQLite à laquelle son Python est lié. Lors du passage à iOS, réexécutez le scénario sur le simulateur ou l’appareil cible en utilisant la véritable couche d’accès aux données, et consignez la valeur de sqlite3_libversion() dans cet environnement. Le résultat obtenu sur l’hôte ne constitue pas une preuve pour tous les environnements d’exécution.
Conservez deux connexions sous-jacentes réellement indépendantes. Utilisez des barrières de synchronisation explicites dans le test pour contrôler les trois étapes « première lecture terminée », « écriture validée » et « nouvelle lecture », sans vous appuyer sur des temporisations. Si le pool de connexions affecte les deux opérations à une même connexion, l’expérience perd tout son sens.
Vérifiez enfin quatre points : l’ancienne transaction lit l’ancienne valeur, une nouvelle transaction lit la nouvelle valeur, les transactions se terminent également en cas d’exception, et les checkpoints peuvent progresser après la libération des transactions de lecture longues. Pour les requêtes asynchrones, assurez-vous aussi que les curseurs sont fermés et les transactions terminées après une annulation. Conservez ces assertions dans les tests de non-régression. La prochaine fois qu’une liste affichera d’anciennes données, vous pourrez ainsi déterminer d’abord s’il s’agit d’un snapshot correct ou d’une transaction dont la durée de vie a dépassé le périmètre prévu.
Questions fréquentes
BEGIN fixe-t-il immédiatement le snapshot de lecture ?
Pas pour une transaction différée ordinaire. Dans ce test en mode WAL, le snapshot se forme lors de la première lecture effective.
Une lecture déjà ouverte doit-elle voir le dernier commit ?
Non. Elle conserve son ancien snapshot. Il faut terminer la transaction et relire pour voir les modifications validées.
Un checkpoint réduit-il toujours la taille du fichier WAL ?
Non. Réécrire les pages dans la base et tronquer le fichier sont deux opérations distinctes. La taille du fichier ne suffit pas au diagnostic.
Confiez vos tâches à un Mac cloud dédié
Louez un Mac mini physique dédié et choisissez la région et la durée de facturation adaptées à votre activité. Après activation, consultez les informations de connexion dans la console. En cas de problème, ouvrez un ticket depuis la console.
Choisir une offre Mac cloud