[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 | 4 | # Contact : contact@spboucher.ai |
| 5 | 5 | # Fichier : .gitignore |
| 6 | 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 | 10 | # Secrets — jamais commités |
@@ -29,6 +29,7 @@ data/*.db | ||
| 29 | 29 | data/*.sqlite* |
| 30 | 30 | data/cache/ |
| 31 | 31 | data/jobka.db |
| 32 | +data/jobka.db-* | |
| 32 | 33 | data/candidats-*.jsonl |
| 33 | 34 | data/confirmed-*.json |
| 34 | 35 | data/terms-*.txt |
modified
jobka/db.py
+6 −1
@@ -5,7 +5,7 @@ | ||
| 5 | 5 | # Fichier : jobka/db.py |
| 6 | 6 | # Rôle : Persistance SQLite — upsert avec détection de changements, cycle de |
| 7 | 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 | 10 | from __future__ import annotations |
| 11 | 11 | |
@@ -153,6 +153,11 @@ def connect() -> sqlite3.Connection: | ||
| 153 | 153 | con = sqlite3.connect(DB_PATH, timeout=30.0) |
| 154 | 154 | con.row_factory = sqlite3.Row |
| 155 | 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 | 161 | con.executescript(_SCHEMA) |
| 157 | 162 | for table, cols in _MIGRATIONS.items(): |
| 158 | 163 | existing = {r["name"] for r in con.execute(f"PRAGMA table_info({table})")} |
| 159 | 164 | |