Aller au contenu

Chaos : harnais déterministe pour scénarios de résilience

Le module Chaos est un harnais léger pour les tests de résilience. Tu enregistres des scénarios d’injection de fautes qui implémentent une interface à une seule méthode, tu les exécutes et tu collectes un rapport pass/fail structuré. Il est délibérément minimal — cinq classes — et destiné aux suites de résilience et aux exercices de chaos-day, pas au chemin de production des documents.

Stabilité : expérimentale. Il s’agit d’une surface d’outillage de test et de résilience, pas d’une API PDF du core. Le SPI est réduit et stable dans sa forme, mais la portée du module et les scénarios fournis évoluent. Ne construis pas de flux de contrôle de production sur cette base.

Fenêtre de terminal
composer require nextpdf/core:^3

Un test de résilience pose la question : quand une dépendance tombe en panne, le moteur se dégrade-t-il correctement ? Le module Chaos structure ce test. ChaosScenarioInterface est le contrat qu’un scénario implémente — un name() et un simulate() qui retourne un ChaosOutcome. Un scénario encapsule une faute (une partition réseau, une rafale de réponses 5xx du backend de récupération) et rapporte ce qui s’est passé.

ChaosScenarioRunner est l’orchestrateur. Tu enregistres des scénarios avec register(), tu appelles run() pour les exécuter séquentiellement dans l’ordre d’enregistrement, puis tu consultes l’agrégat : outcomes(), allPassed(), passCount(), failCount(). Le runner ne lève jamais d’exception sur un échec de scénario — un échec est une donnée capturée dans un ChaosOutcome, pas une exception. Il ne lève une exception que lorsque sa propre infrastructure est défaillante : un enregistrement de scénario invalide ou l’impossibilité d’écrire le fichier de rapport (ChaosReportWriteException). Un scénario qui ne peut pas atteindre la ressource qu’il teste remonte une RetrievalUnavailableException. L’ensemble du module est @since 3.2.0.

ChaosOutcome est le résultat par scénario : pass/fail, une durée et un toArray() pour le rapport structuré. Comme un outcome enregistre la durée d’horloge murale, le profil de reproductibilité du rapport est structural, pas bitwise.

TypeMembres clésRôle
ChaosScenarioInterfacename(): string, simulate(): ChaosOutcomeLe contrat de scénario (@since 3.2.0)
ChaosScenarioRunnerregister(), run(), outcomes(), allPassed(), passCount(), failCount()Orchestrateur séquentiel de scénarios (@since 3.2.0)
ChaosOutcomedurationSeconds(), toArray()Résultat pass/fail par scénario (@since 3.2.0)
RetrievalUnavailableExceptionUne ressource testée était inaccessible
ChaosReportWriteExceptionLe fichier de rapport n’a pas pu être écrit

Exécute composer docs:generate-api-php -- --module=Chaos pour obtenir la table PHPDoc complète.

Enregistre un scénario, puis exécute la suite.

<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Chaos\ChaosOutcome;
use NextPDF\Chaos\ChaosScenarioInterface;
use NextPDF\Chaos\ChaosScenarioRunner;
final class TimeoutScenario implements ChaosScenarioInterface
{
public function name(): string
{
return 'dependency-timeout';
}
public function simulate(): ChaosOutcome
{
// Inject the fault, observe the engine's degradation, return the verdict.
return new ChaosOutcome(/* name, passed, durationSeconds, details */);
}
}
$runner = new ChaosScenarioRunner();
$runner->register(new TimeoutScenario());
$runner->run();
echo $runner->allPassed() ? "Resilient.\n" : "{$runner->failCount()} scenario(s) failed.\n";

Pilote le harnais depuis un job de résilience et traite tout échec comme un code de sortie non nul, sans laisser un échec de scénario s’échapper sous forme d’exception.

<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Chaos\ChaosScenarioRunner;
use NextPDF\Chaos\Exception\ChaosReportWriteException;
use Psr\Log\LoggerInterface;
final readonly class ChaosJob
{
/** @param list<\NextPDF\Chaos\ChaosScenarioInterface> $scenarios */
public function __construct(
private array $scenarios,
private LoggerInterface $logger,
) {}
public function run(string $reportPath): int
{
$runner = new ChaosScenarioRunner();
foreach ($this->scenarios as $scenario) {
$runner->register($scenario);
}
$runner->run(); // never throws on scenario failure
try {
$runner->writeReport($reportPath);
} catch (ChaosReportWriteException $e) {
$this->logger->error('Chaos report could not be written.', ['error' => $e->getMessage()]);
return 2;
}
return $runner->allPassed() ? 0 : 1;
}
}
  • run() ne lève jamais d’exception parce qu’un scénario a échoué. Un échec vit dans ChaosOutcome. Si tu enveloppes run() dans un try/catch en t’attendant à y intercepter les échecs, tu ne les verras jamais — lis plutôt failCount() / allPassed().
  • Le runner ne lève une exception que sur des défaillances d’infrastructure : un enregistrement invalide ou une ChaosReportWriteException quand le chemin du rapport n’est pas accessible en écriture. Traite-les distinctement des résultats de scénarios.
  • Les scénarios s’exécutent séquentiellement dans l’ordre d’enregistrement. Il n’y a aucun parallélisme. L’ordre peut avoir son importance si les scénarios partagent un état externe.
  • Ce module sert aux tests de résilience. N’importe pas le runner dans le chemin de production de documents comme mécanisme de contrôle.

La surcharge du runner est négligeable ; le coût vient de ce que font les scénarios. Comme les scénarios injectent des fautes et peuvent attendre l’expiration de timeouts, une exécution de chaos peut être lente par conception. Ici, le performance_budget correspond au chiffre de référence du moteur, pas à une borne sur la durée des scénarios. Le profil de reproductibilité est structural : le rapport enregistre des durées d’horloge murale, donc deux exécutions diffèrent sur ces champs.

Les scénarios injectent des fautes et peuvent exercer les chemins d’échec des dépendances. N’exécute le harnais que dans un environnement de test ou de pré-production, avec des identifiants et des points de terminaison limités à cet environnement — jamais contre des systèmes de production. Le rapport peut contenir des détails de diagnostic sur les modes d’échec. Traite-le comme interne et respecte l’obligation de nettoyage des journaux du projet avant de le partager. Consulte le modèle de menace du moteur dans /modules/core/security/.

Ce module n’affirme aucune revendication normative relative à la spécification PDF. C’est de l’outillage de résilience. Il n’implémente aucun protocole standardisé dont les clauses devraient être citées. La conformité du moteur est validée par l’oracle et les suites golden décrits dans /modules/core/conformance/.