# 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 rm -rf ``` **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/.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 |