[ka6] fix site down jobka: convoi de verrous SQLite (journal DELETE) — bascule en WAL — le site était muet (ReadTimeout public+local depuis 03:38) alors que pm2 affichait online : la BD data/jobka.db (740 Mo, 64 340 offres) tournait en journal_mode=delete, où lecteurs et écrivains se bloquent mutuellement. Un cycle de sync long (agropur 92 s, airbus suspendu ~03:36) a tenu le verrou d'écriture ; chaque requête web (SSR d'accueil = plusieurs agrégats sur la table jobs) restait coincée dans sqlite3_step jusqu'au busy_timeout de 30 s, les 40 slots du threadpool AnyIO de FastAPI se sont remplis (sample : ~24 threads dans sqlite3_step, /emploi statique répondait mais /, /health et toute route sync-def étaient muets), et job-ka-sync crashait en boucle sur « database is locked » (18 restarts, journal chaud data/jobka.db-journal). Le restart pm2 automatique ne réglait rien : le sync relançait aussitôt son cycle d'écritures et le convoi se reformait. Mesure à froid : COUNT(*) sur 64 k lignes = 60 s sous contention, 0,03 s BD libérée — volumétrie saine (freelist 6 pages), quick_check ok, 0 offre perdue. Correctif racine : PRAGMA journal_mode=WAL (persistant, appliqué à la BD après arrêt propre + sauvegarde data/backup-jobka-20260914.db) + synchronous=NORMAL, inscrits dans db.connect() ; .gitignore couvre désormais data/jobka.db-* (journal/wal/shm). Post-fix : /health ok (24 719 offres actives, sync live), / local 200 en 0,1 s, public 200 en 0,55 s pendant que le cycle de sync écrit — plus aucun « database is locked ». 103 tests pytest verts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2 changed files +8 −2
modified
.gitignore
+2 −1
@@ -4,7 +4,7 @@ | |||||
| 4 | # Contact : contact@spboucher.ai | 4 | # Contact : contact@spboucher.ai | ||
| 5 | # Fichier : .gitignore | 5 | # Fichier : .gitignore | ||
| 6 | # Rôle : Exclusions git (secrets, caches, artefacts de build) | 6 | # Rôle : Exclusions git (secrets, caches, artefacts de build) | ||
| 7 | −# Créé : 2026-08-17 Modifié : 2026-08-17 | 7 | +# Créé : 2026-08-17 Modifié : 2026-09-14 | ||
| 8 | # ============================================================================= | 8 | # ============================================================================= | ||
| 9 | 9 | ||||
| 10 | # Secrets — jamais commités | 10 | # Secrets — jamais commités | ||
@@ -29,6 +29,7 @@ data/*.db | |||||
| 29 | data/*.sqlite* | 29 | data/*.sqlite* | ||
| 30 | data/cache/ | 30 | data/cache/ | ||
| 31 | data/jobka.db | 31 | data/jobka.db | ||
| 32 | +data/jobka.db-* | ||||
| 32 | data/candidats-*.jsonl | 33 | data/candidats-*.jsonl | ||
| 33 | data/confirmed-*.json | 34 | data/confirmed-*.json | ||
| 34 | data/terms-*.txt | 35 | data/terms-*.txt | ||
modified
jobka/db.py
+6 −1
@@ -5,7 +5,7 @@ | |||||
| 5 | # Fichier : jobka/db.py | 5 | # Fichier : jobka/db.py | ||
| 6 | # Rôle : Persistance SQLite — upsert avec détection de changements, cycle de | 6 | # Rôle : Persistance SQLite — upsert avec détection de changements, cycle de | ||
| 7 | # vie avec délai de grâce, anti-dérive, expiration sur date limite | 7 | # vie avec délai de grâce, anti-dérive, expiration sur date limite | ||
| 8 | −# Créé : 2026-08-17 Modifié : 2026-08-25 | 8 | +# Créé : 2026-08-17 Modifié : 2026-09-14 | ||
| 9 | # ============================================================================= | 9 | # ============================================================================= | ||
| 10 | from __future__ import annotations | 10 | from __future__ import annotations | ||
| 11 | 11 | ||||
@@ -153,6 +153,11 @@ def connect() -> sqlite3.Connection: | |||||
| 153 | con = sqlite3.connect(DB_PATH, timeout=30.0) | 153 | con = sqlite3.connect(DB_PATH, timeout=30.0) | ||
| 154 | con.row_factory = sqlite3.Row | 154 | con.row_factory = sqlite3.Row | ||
| 155 | con.execute("PRAGMA busy_timeout=30000") # BD vivante (sync horaire) | 155 | con.execute("PRAGMA busy_timeout=30000") # BD vivante (sync horaire) | ||
| 156 | + # WAL : lecteurs et écrivains ne se bloquent plus mutuellement — en mode | ||||
| 157 | + # rollback (delete), un sync long verrouillait toutes les lectures web, | ||||
| 158 | + # saturait le threadpool FastAPI et rendait le site muet (panne 2026-09-14). | ||||
| 159 | + con.execute("PRAGMA journal_mode=WAL") | ||||
| 160 | + con.execute("PRAGMA synchronous=NORMAL") # durable en WAL, commits rapides | ||||
| 156 | con.executescript(_SCHEMA) | 161 | con.executescript(_SCHEMA) | ||
| 157 | for table, cols in _MIGRATIONS.items(): | 162 | for table, cols in _MIGRATIONS.items(): | ||
| 158 | existing = {r["name"] for r in con.execute(f"PRAGMA table_info({table})")} | 163 | existing = {r["name"] for r in con.execute(f"PRAGMA table_info({table})")} | ||
| 159 | 164 | ||||