diff --git a/hermes-tt/nyora-notes-tt-backup-wal-moteur-mixte.md b/hermes-tt/nyora-notes-tt-backup-wal-moteur-mixte.md new file mode 100644 index 0000000..27967e0 --- /dev/null +++ b/hermes-tt/nyora-notes-tt-backup-wal-moteur-mixte.md @@ -0,0 +1,136 @@ +# 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)` : + ```python + 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 : + ```python + 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 : + ```bash + cd /volume1/docker/nyora-notes-tt + docker compose build + docker compose up -d --force-recreate + ``` + +--- + +## Verification + +```bash +# 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':''}))" +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/`).