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 ; pour l'observabilité, voir 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-02 Disk full sur le workspace BLOCKER §INC-02
INC-03 Cloud API rate limit / quota dépassé MAJOR §INC-03
INC-04 SQLite database is locked MAJOR §INC-04
INC-05 Memory leak (RSS qui croît continûment) MAJOR §INC-05
INC-06 Compromission d'une clé API cloud BLOCKER §INC-06
INC-07 Rapport HTML corrompu / non-déterministe MEDIUM §INC-07
INC-08 CI bloquée > 30 min (déjà vu) MEDIUM §INC-08
INC-09 Upgrade qui casse les jobs en cours MAJOR §INC-09
INC-10 Restauration depuis backup MEDIUM §INC-10

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.

# 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.

# 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.

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.

# 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. 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.

# 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.

# 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.

# 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.

# 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.

# 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.

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).

# 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.


INC-07 — Rapport HTML corrompu

Symptôme. Deux runs identiques produisent des rapports HTML différents byte-for-byte.

Diagnostic.

# 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.

# 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.

# 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 (canal privé).

Révisions

Version Date Changements
1.0 2026-05 Création initiale (S60), 10 scénarios