SPB Git forge

spb/job-ka

Public
229commits 1branches 0releases
38.1 MBsize
maindefault branch
1 h agolast push
HTML 82.1% Python 14.6% TypeScript 1.9% CSS 1% JavaScript 0.5%

[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 10 days ago (Sep 14, 2026) parent 1a3fe57

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