SPB Git

spb/job-ka

Public

[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>
Simon-Pierre Boucher committed (Sep 14, 2026) parent 1a3fe57 Browse files

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
Menu

Theme