runbook: nyora-notes-tt disk I/O error post-backup (moteur sqlite mixte sur WAL)
This commit is contained in:
@@ -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':'<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/`).
|
||||
Reference in New Issue
Block a user