Files
nas-runbooks/hermes-tt/nyora-notes-tt-backup-wal-moteur-mixte.md
T

5.8 KiB

nyora-notes-tt : disk I/O error sur toutes les requetes apres chaque backup (moteur SQLite mixte sur DB WAL live)

Instance auteur : hermes-tt Date : 2026-08-02 Tags : nyora-notes-tt, sqlite, wal, sqlean, backup Statut : valide


Probleme

Apres la fin propre de l'ingestion Phase 1 (205 dossiers, 393 notes, cron watchdog retire), tous les tools MCP RAG mail (search_mail_memory, get_checkpoint_status, /pending-folders) tombaient en erreur des le premier appel :

sqlean.dbapi2.OperationalError: disk I/O error
  File "/app/mcp_server.py", line 336, in pending_folders
    conn = get_db_connection()
  File "/app/setup_db.py", line 22, in get_db_connection
    wal_res = conn.execute("PRAGMA journal_mode = WAL;").fetchone()

Le conteneur restait Up ... (healthy) (le healthcheck ne touche pas la DB en profondeur), /health repondait 200 en continu -- seules les routes qui ouvrent une vraie connexion DB echouaient. Pas de piste disque materielle : dmesg propre, /volume1 a 70% (large marge).


Contexte et contraintes

  • DB SQLite en mode WAL, moteur sqlean + extension sqlite_vec (embeddings Gemini 768-dim).
  • get_db_connection() (setup_db.py) ouvre une connexion neuve a chaque appel de route, sans pooling, et sans PRAGMA busy_timeout.
  • Route /backup (mcp_server.py, _run_backup()) declenchee par n8n toutes les 2h, utilisait import sqlite3 as std_sqlite3 -- le module stdlib, different du moteur sqlean utilise par le reste de l'app sur la meme DB live.

Ce qui NE fonctionne PAS

Tentative Erreur obtenue Raison de l'echec
Redemarrer le conteneur sans toucher au code Fonctionne temporairement, revient a la prochaine sauvegarde n8n (cycle 2h) Le vrai declencheur (moteur mixte sur backup) n'est pas traite, juste les fd/connexions purges
Ignorer et considerer que c'est un probleme disque NAS Aucune erreur dmesg, espace disque large Le diagnostic initial (panne materielle) etait errone -- l'erreur est applicative

Diagnostic (preuve)

ls -la /proc/1/fd dans le conteneur montrait plusieurs descripteurs vers nyora-notes-tt.db-wal et nyora-notes-tt.db-shm marques (deleted), horodates pres des cycles de backup (12:00 / 14:00) :

lrwx------ 1 appuser appuser 64 Aug  2 14:16 10 -> /app/nyora-notes-tt.db-shm (deleted)
lrwx------ 1 appuser appuser 64 Aug  2 14:16 12 -> /app/nyora-notes-tt.db-wal (deleted)
lrwx------ 1 appuser appuser 64 Aug  2 14:16 13 -> /app/nyora-notes-tt.db

Le close() de la connexion stdlib sqlite3 de _run_backup() (checkpoint automatique en mode WAL) recree le -wal/-shm pendant que les connexions sqlean deja ouvertes dans le process principal tiennent encore les anciens descripteurs -> les deux moteurs n'ont plus la meme vue de l'index WAL partage (shared memory) -> disk I/O error sur toute connexion sqlean ouverte juste apres. L'absence de PRAGMA busy_timeout transformait la moindre contention en echec dur au lieu d'une attente/retry.

Reproduction fiable : declencher /backup manuellement puis enchainer des appels /pending-folders -- echec systematique avant le fix, 8/8 OK apres.


Solution validee

  1. setup_db.py, dans get_db_connection(), juste apres conn = sqlite3.connect(db_path) :
    conn.execute("PRAGMA busy_timeout = 5000;")
    
  2. mcp_server.py, dans _run_backup() : remplacer le moteur stdlib par le meme moteur que le reste de l'app, et poser aussi le busy_timeout sur la connexion source :
    import sqlean as std_sqlite3   # etait: import sqlite3 as std_sqlite3
    ...
    src = std_sqlite3.connect(DB_PATH)
    src.execute("PRAGMA busy_timeout = 5000;")
    
  3. Rebuild + recreation du conteneur :
    cd /volume1/docker/nyora-notes-tt
    docker compose build
    docker compose up -d --force-recreate
    

Verification

# Declencher un backup manuel puis enchainer des requetes -- scenario exact qui plantait
docker exec nyora-notes-tt python3 -c "import urllib.request; urllib.request.urlopen(urllib.request.Request('http://localhost:8000/backup', method='POST', headers={'x-ingest-key':'<cle>'}))"
for i in 1 2 3 4 5 6 7 8; do
  docker exec nyora-notes-tt python3 -c "import urllib.request; print(urllib.request.urlopen('http://localhost:8000/pending-folders?limit=1').status)"
done
# Resultat attendu : 200 x8, y compris immediatement apres le backup

Pieges specifiques DSM / NAS

  • docker absent du PATH en shell SSH non-interactif Synology -- utiliser le chemin complet /usr/local/bin/docker (symlink vers /var/packages/ContainerManager/target/usr/bin/docker).
  • ls -la sur les fichiers host peut afficher des bits POSIX larges (777) alors qu'une ACL Synology (synoacltool -get) restreint reellement l'acces -- ne jamais se fier aux seuls bits POSIX affiches pour diagnostiquer un probleme de permission sur ce NAS.
  • Depuis l'interieur d'un conteneur (mcp-nas), joindre le service NyoraNotes hermes-perso (8787) et Gitea (3232) via 172.17.0.1, jamais l'IP LAN 192.168.100.33 (timeout).

Piege generique (tout futur service Hermes/nyora-notes-* sur sqlean + WAL)

Ne jamais ouvrir une DB SQLite en mode WAL activement utilisee par un process long-vivant avec un deuxieme moteur/binding SQLite different (stdlib vs sqlean vs autre), meme pour une operation aussi anodine qu'une sauvegarde en lecture via Connection.backup(). Le checkpoint automatique au close() peut recreer le -wal/-shm et desynchroniser les connexions deja ouvertes dans l'autre moteur. Regle : un seul moteur SQLite par DB WAL live, et toujours PRAGMA busy_timeout des l'ouverture de toute connexion.


References

  • Repo nyora-notes-tt : aucun repo Gitea existant au moment du fix (jamais initialise) -- le correctif ne vit que sur le filesystem NAS (/volume1/docker/nyora-notes-tt/).