Journal

Trois pannes qui ne disaient rien

En 2026, les trois pannes qui m'ont pris le plus de temps sur les sites de Jewely avaient un point commun. Aucune n'a affiché d'erreur. Tout semblait fonctionner, et le résultat ét...

Article4 fils étiquetés

En 2026, les trois pannes qui m'ont pris le plus de temps sur les sites de Jewely avaient un point commun. Aucune n'a affiché d'erreur. Tout semblait fonctionner, et le résultat était faux.

Je les raconte dans l'ordre, puis la règle que j'en ai tirée.

1. Les prix qui ne bougeaient plus

Février 2026, sur la boutique PrestaShop d'Auberi. Les prix viennent de l'ERP Odeis : il dépose un fichier, et la boutique l'importe sept fois par jour. Depuis plusieurs semaines, les prix ne changeaient plus sur le site.

J'ai vérifié du plus proche au plus lointain. Le code en production était bien le code versionné. Le module d'import calculait et enregistrait les prix correctement. La tâche planifiée tournait aux heures prévues. Restait le fichier lui-même, et c'est le journal du transfert qui a répondu :

articles.txt    133 bytes received
dispo.txt       245723 bytes received

Le fichier des articles pesait 133 octets : une ligne d'en-tête, un marqueur de fin, aucun produit. L'import le lisait, ne trouvait rien à mettre à jour, et s'arrêtait sans rien signaler. Le fichier des stocks, lui, arrivait normalement, ce qui cachait le problème.

La cause était dans l'export de l'ERP, côté client. Ce n'était pas à moi de la corriger. De mon côté, j'ai intégré les prix à la main à partir du fichier du client, le temps que l'export soit rétabli. Puis j'ai ajouté le contrôle qui manquait, dans le tableau de bord de l'import :

$smallFiles = array_filter($articleFiles, function ($f) { return filesize($f) < 200; });
if (count($smallFiles) === count($articleFiles)) {
    $stats['health'][] = ['level' => 'danger', 'message' => 'Tous les fichiers articles sont vides (< 200 octets) — export ODEIS probablement en erreur'];
}

Un import qui reçoit zéro ligne n'est pas un import réussi. C'est une question à poser à quelqu'un.

2. Le déploiement annoncé réussi

Le 2 mars 2026, sur le cluster Docker Swarm de Jewely. Les disques de deux serveurs étaient remplis à plus de 95 %. Ils n'ont pas pu télécharger la nouvelle image, qui pesait environ 786 Mo. Swarm est revenu à l'ancienne version, et la commande de déploiement a quand même rendu un code de succès.

L'incident est raconté en détail dans Quand un déploiement réussi ne l'est pas.

Ce que j'ai écrit ensuite est un agent de déploiement. Il ne croit pas la commande. Il compare l'empreinte de l'image attendue avec celle qui tourne réellement, et il regarde les disques avant de commencer :

Ce que l'agent constate Ce qu'il fait
Disque au-dessus de 80 % Nettoyage ciblé des anciennes images, puis déploiement
Disque au-dessus de 90 % Déploiement bloqué
L'image qui tourne n'est pas l'image attendue Nouvelle tentative, trois au maximum

Cet agent vérifie à côté du pipeline. Mettre la même vérification dans le déclencheur automatique relevait de l'administrateur des serveurs AWS, et c'est noté comme tel dans la documentation.

3. Le cache qu'on ne pouvait plus vider

Mars 2026, dans le code commun. Le cache du site est rangé par étiquettes, ce qui permet de vider d'un coup tout ce qui concerne un produit ou une page. Les étiquettes n'existent qu'avec Redis. En développement et dans les tests, le cache est un simple fichier, sans étiquettes.

Une première version contournait le manque : sans étiquettes, elle écrivait quand même en cache, sous une autre clé. L'écriture marchait. Le vidage par étiquette, lui, ne faisait rien. Une valeur mise en cache ne pouvait donc plus jamais être invalidée, et elle restait fausse sans aucun message.

La décision, écrite dans une note d'architecture : sans étiquettes, pas de cache du tout.

public static function rememberWithTags(array $tags, string $key, int $ttl, callable $callback): mixed
{
    if (self::cacheSupportsTagging()) {
        return Cache::tags($tags)->remember($key, $ttl, $callback);
    }
    return $callback(); // bypass cache entirely
}

Le coût : en développement, chaque appel va en base de données. Je l'ai accepté. À cet endroit, une réponse lente et juste vaut mieux qu'une réponse rapide et périmée.

Le même défaut, trois fois

Ce que le système affichait Ce qui se passait Le contrôle ajouté
Prix Import terminé Aucun article lu Alerte si tous les fichiers sont vides
Déploiement Succès Ancienne version en production Comparaison entre l'image attendue et celle qui tourne
Cache Valeur servie Valeur jamais invalidée Pas de cache quand on ne peut pas le vider

Dans les trois cas, quelqu'un avait prévu une sortie discrète pour un cas gênant. Zéro ligne : rien à faire. Image impossible à démarrer : on revient à l'ancienne. Pas d'étiquettes : on écrit ailleurs. Chaque sortie était raisonnable seule. Aucune ne prévenait personne.

La règle

Une panne qui se voit vaut mieux qu'un repli qui se tait.

Je l'ai appliquée ensuite aux templates du CMS : quand une maison n'a pas un template, le site lève une erreur. Il ne va plus chercher celui d'une autre maison. Un template manquant se voit en préproduction. Le template d'une autre maison, lui, serait parti en production.

Avant de mettre un flux automatique en service, je pose maintenant quatre questions :

  1. Que voit-on quand rien n'arrive ? Un fichier vide, une liste vide, zéro ligne traitée.
  2. Le « succès » décrit-il la commande, ou l'état obtenu ? Une commande qui rend 0 ne dit pas quelle version tourne.
  3. Y a-t-il un repli ? Si oui, que sert-il à la place, et qui l'apprend ?
  4. Que coûte le bruit ? Une alerte de trop coûte une minute. Des prix faux pendant des semaines coûtent autre chose.

Le pipeline et son durcissement sont décrits dans l'étude de cas Vérifier ce qui tourne vraiment après un déploiement e-commerce.