# 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/`).