Picarones / docs /operations /runbook.md
Claude
docs: refonte Diataxis + 8 documents institutionnels (S60)
d0a3fab unverified
# Runbook — réponse aux incidents Picarones
> **Audience** : opérateur (DSI institutionnelle, SRE) en garde
> active. Ce document liste les incidents prévisibles et les
> procédures de mitigation. Pour le déploiement initial, voir
> [`deployment-institutional.md`](deployment-institutional.md) ;
> pour l'observabilité, voir [`observability.md`](observability.md).
>
> **Convention** : chaque scénario suit le format
> `Symptôme → Diagnostic → Mitigation → Suivi`.
## Index des scénarios
| ID | Scénario | Sévérité | Page |
|----|----------|----------|------|
| INC-01 | Job stuck en `running` | MAJOR | [§INC-01](#inc-01--job-stuck-en-running) |
| INC-02 | Disk full sur le workspace | BLOCKER | [§INC-02](#inc-02--disk-full-sur-le-workspace) |
| INC-03 | Cloud API rate limit / quota dépassé | MAJOR | [§INC-03](#inc-03--cloud-api-rate-limit) |
| INC-04 | SQLite `database is locked` | MAJOR | [§INC-04](#inc-04--sqlite-database-is-locked) |
| INC-05 | Memory leak (RSS qui croît continûment) | MAJOR | [§INC-05](#inc-05--memory-leak) |
| INC-06 | Compromission d'une clé API cloud | BLOCKER | [§INC-06](#inc-06--compromission-de-cl%C3%A9-api) |
| INC-07 | Rapport HTML corrompu / non-déterministe | MEDIUM | [§INC-07](#inc-07--rapport-html-corrompu) |
| INC-08 | CI bloquée > 30 min (déjà vu) | MEDIUM | [§INC-08](#inc-08--ci-bloqu%C3%A9e) |
| INC-09 | Upgrade qui casse les jobs en cours | MAJOR | [§INC-09](#inc-09--upgrade-casse-jobs) |
| INC-10 | Restauration depuis backup | MEDIUM | [§INC-10](#inc-10--restauration-backup) |
---
## INC-01 — Job stuck en `running`
**Symptôme**. `GET /api/jobs/{job_id}` retourne `status=running`
depuis > 1 heure alors que le corpus tient en quelques minutes.
**Diagnostic**.
```bash
# 1. Le thread daemon existe-t-il encore ?
curl -s http://localhost:7860/api/jobs/{job_id} | jq '.status, .progress'
# 2. Les logs montrent-ils une activité récente ?
journalctl -u picarones -n 200 | grep "{job_id}"
# 3. Y a-t-il un appel cloud bloqué ?
ss -tnp | grep :443 # connexions TLS sortantes
```
Causes typiques :
- Appel cloud qui hang sans timeout (anciens adapters).
- Workspace en read-only (impossible d'écrire le résultat).
- Process daemon mort sans avoir mis à jour le statut.
**Mitigation**.
```bash
# Forcer l'annulation (dégrade en cancelled, pas en error).
curl -X DELETE http://localhost:7860/api/jobs/{job_id}
# Si le service ne répond plus :
systemctl restart picarones
# Au boot, le lifespan hook ``mark_orphaned_jobs_interrupted`` bascule
# automatiquement les jobs ``running`` en ``interrupted``.
```
**Suivi**. Vérifier que le `JobRunner` n'a pas d'autres threads
zombies via `len(runner._threads)` (devrait redescendre). Si
récurrent, instrumenter avec un timeout de soft-cap par job.
---
## INC-02 — Disk full sur le workspace
**Symptôme**. Les jobs échouent en `error` avec
`OSError: [Errno 28] No space left on device`. L'API web peut
elle-même planter au boot (`JobStore` ne peut plus persister).
**Diagnostic**.
```bash
df -h /var/lib/picarones/workspaces # ou le path configuré
du -sh /var/lib/picarones/workspaces/*
```
Coupable typique : caches d'artefacts non purgés (`InMemoryArtifactStore`
n'a pas de TTL ; `FilesystemArtifactStore` non plus).
**Mitigation**.
```bash
# 1. Identifier les workspaces les plus gros.
du -sh /var/lib/picarones/workspaces/* | sort -rh | head -10
# 2. Purger les workspaces dont aucun job actif ne dépend (lookup
# via JobStore).
sqlite3 /var/lib/picarones/jobs.db \
"SELECT job_id, status, payload FROM jobs WHERE status NOT IN ('pending', 'running');" \
| jq -r '.payload | fromjson | .output_dir'
# 3. Pour chaque output_dir terminé, archiver puis supprimer.
tar czf /backup/picarones-archive-$(date +%F).tar.gz <output_dirs>
rm -rf <output_dirs>
```
**Suivi**. Établir une politique de rétention dans
[`data-retention-rgpd.md`](data-retention-rgpd.md). Recommandation :
purger les workspaces > 30 jours sans accès.
---
## INC-03 — Cloud API rate limit
**Symptôme**. Logs WARN : `[adapter] erreur retryable (tentative 3/4,
attente 8s) : 429 Too Many Requests`. Job se termine en error après
épuisement des retries.
**Diagnostic**.
```bash
# Compter les 429 dans la dernière heure.
journalctl -u picarones --since "1 hour ago" \
| grep "429" | wc -l
# Identifier les jobs concernés.
journalctl -u picarones --since "1 hour ago" \
| grep -B2 "429" | grep "job_runner"
```
Causes typiques : un benchmark de 5000 documents lance 5000 appels
en parallèle, dépasse la quota de l'organisation cloud.
**Mitigation immédiate**.
```bash
# 1. Réduire le parallélisme du runner (env var).
sed -i 's/PICARONES_RUNNER_MAX_WORKERS=8/PICARONES_RUNNER_MAX_WORKERS=2/' /etc/picarones/.env
systemctl restart picarones
# 2. Re-soumettre les jobs en error qui se sont arrêtés au milieu.
# (Picarones ne fait pas de resume automatique sur erreur cloud — le
# cache d'artefacts du PipelineExecutor évite de re-exécuter les
# steps déjà terminés au prochain run.)
```
**Mitigation long terme**. Demander une quota plus haute au
fournisseur cloud, ou ajouter un throttle au niveau adapter (token
bucket par adapter).
---
## INC-04 — SQLite `database is locked`
**Symptôme**. Logs ERROR : `sqlite3.OperationalError: database is
locked`. Touche typiquement le `JobStore`.
**Diagnostic**.
```bash
# 1. Compter les processes qui ont la DB ouverte.
lsof /var/lib/picarones/jobs.db
# 2. Vérifier le mode WAL.
sqlite3 /var/lib/picarones/jobs.db "PRAGMA journal_mode;"
# Devrait répondre "wal". Si "delete" ou "rollback", le WAL n'a pas
# pris.
```
Causes : un process autre que Picarones a ouvert la DB (backup
maladroit), ou le filesystem ne supporte pas WAL (FAT32, NFS sans
verrous).
**Mitigation**.
```bash
# 1. Stopper l'autre process si identifié.
# 2. Si NFS : remonter avec ``-o nolock`` côté serveur ne marche PAS
# (WAL exige des verrous). Solution : déplacer ``jobs.db`` sur un
# filesystem local et exporter le résultat via NFS read-only.
# 3. Si filesystem ne supporte vraiment pas WAL, le code retombe sur
# ``rollback journal`` (cf. job_store.py:185-189) — fonctionnel
# mais bloquant en lecture pendant les écritures.
# Test de santé.
sqlite3 /var/lib/picarones/jobs.db "PRAGMA integrity_check;"
```
**Suivi**. Configurer un monitoring du `journal_mode` au boot.
---
## INC-05 — Memory leak
**Symptôme**. RSS du process Picarones croît continûment au-delà
de 2 GB après plusieurs heures.
**Diagnostic**.
```bash
# Profiling minimal sans installer d'outil.
ps -o pid,rss,cmd -p $(pgrep picarones) | tail -1
# Si py-spy disponible :
py-spy dump --pid $(pgrep picarones)
```
Causes connues :
- `JobRunner._threads` non nettoyé (FIXÉ en S58).
- `RateLimitMiddleware._buckets` non borné (FIXÉ en S58 — eviction LRU).
- Caches d'artefacts in-memory accumulés (cf. INC-02).
**Mitigation**.
```bash
systemctl restart picarones
# Le lifespan hook nettoie les jobs orphelins ; les caches in-memory
# sont vidés par redémarrage.
```
**Suivi**. Si récurrent, exporter `picarones._mem_audit`
implémenter — backlog) et corréler avec les jobs actifs.
---
## INC-06 — Compromission de clé API
**Symptôme**. Facturation cloud anormale, ou notification du
fournisseur (« nous avons détecté une utilisation suspecte de votre
clé »).
**Mitigation immédiate** (dans l'ordre).
```bash
# 1. Révoquer la clé chez le fournisseur (console cloud).
# 2. Stopper Picarones pour éviter qu'il ne tente de relancer avec
# la clé invalidée.
systemctl stop picarones
# 3. Rotater la clé dans le secret store.
vault kv put secret/picarones OPENAI_API_KEY=sk-NEW...
# 4. Reload + redémarrage.
systemctl start picarones
# 5. Audit des jobs récents pour identifier les exfiltrations.
sqlite3 /var/lib/picarones/jobs.db \
"SELECT job_id, payload, created_at FROM jobs ORDER BY created_at DESC LIMIT 100;"
```
**Suivi**. Notifier le DPO institutionnel sous 24 h si des
documents avec PII (registres, état civil) ont été envoyés à l'API
compromise. Voir [`data-retention-rgpd.md`](data-retention-rgpd.md).
---
## INC-07 — Rapport HTML corrompu
**Symptôme**. Deux runs identiques produisent des rapports HTML
différents byte-for-byte.
**Diagnostic**.
```bash
# Comparer les hashes de manifests.
sha256sum run-A/run_manifest.json run-B/run_manifest.json
# Si différents : un des paramètres canoniques a divergé.
diff <(jq -S . run-A/run_manifest.json) <(jq -S . run-B/run_manifest.json)
```
Causes typiques : un adapter cloud (gpt-4o, claude) qui a une
température > 0 → non-déterminisme natif. Vérifier les
`adapter_kwargs` dans le manifest.
**Mitigation**. Forcer `temperature: 0.0` dans la `RunSpec` YAML.
Pour les benchmarks de reproductibilité, exclure les adapters
non-déterministes.
---
## INC-08 — CI bloquée
**Symptôme**. Un job GitHub Actions reste en `queued` ou
`in_progress` > 30 minutes pour ce qui devrait être un test < 5 min.
**Diagnostic**. Vérifier dans cet ordre :
1. **Codecov upload hang** (déjà vu — 50+ min) → couvert par
`timeout-minutes: 5` sur l'étape Codecov + `fail_ci_if_error: false`
depuis le S59.
2. **Live tests qui s'exécutent** au lieu d'être deselected → le
marker `live` doit être dans `addopts` de `pyproject.toml`
(vérifié par les tests dual-lang).
3. **Codespaces / runner épuisé** → annuler manuellement le job,
relancer.
**Mitigation**. Annuler le workflow run (UI GitHub Actions),
relancer. Si récurrent, élever un incident infra GitHub.
---
## INC-09 — Upgrade casse jobs
**Symptôme**. Après `git pull && pip install -e .`, les jobs
soumis avant l'upgrade échouent en `error`.
**Diagnostic**. Le `JobStore` utilise une table `schema_version` ;
une bump de SCHEMA_VERSION sans migration livre `JobStoreError` au
boot.
**Mitigation**.
```bash
# 1. Stopper le service AVANT l'upgrade.
systemctl stop picarones
# 2. Backup du JobStore.
cp /var/lib/picarones/jobs.db /var/lib/picarones/jobs.db.bak
# 3. Upgrade.
git pull && pip install -e ".[dev,web]"
# 4. Vérifier le schéma.
sqlite3 /var/lib/picarones/jobs.db "SELECT version FROM schema_version;"
# 5. Démarrer. Le dispatcher applique automatiquement les
# migrations enregistrées dans ``_MIGRATIONS``.
systemctl start picarones
```
**Suivi**. Tester chaque upgrade en staging avant prod.
---
## INC-10 — Restauration depuis backup
**Symptôme**. Corruption ou perte du workspace ou de la DB jobs.
**Pré-requis**. Backup récent (recommandé : snapshot quotidien du
volume `/var/lib/picarones/`).
**Mitigation**.
```bash
# 1. Stopper le service.
systemctl stop picarones
# 2. Restaurer.
rsync -av /backup/picarones-2026-05-XX/ /var/lib/picarones/
# 3. Vérifier l'intégrité SQLite.
sqlite3 /var/lib/picarones/jobs.db "PRAGMA integrity_check;"
# 4. Démarrer. Les jobs ``running`` au moment du backup seront
# automatiquement marqués ``interrupted`` par le lifespan hook.
systemctl start picarones
```
**Suivi**. Communiquer aux utilisateurs que les jobs en cours au
moment du backup sont à relancer.
---
## Escalade
Si un incident dépasse les procédures ci-dessus :
1. Documenter l'observation dans un fichier `incidents/<date>.md`
(snapshot du symptôme + commandes lancées + résultat).
2. Ouvrir une issue GitHub avec le label `incident`.
3. Pour une vulnérabilité de sécurité, suivre la procédure de
[`/SECURITY.md`](../../SECURITY.md) (canal privé).
## Révisions
| Version | Date | Changements |
|---------|------|-------------|
| 1.0 | 2026-05 | Création initiale (S60), 10 scénarios |