Ma position, annoncée d'entrée : toute migration de schéma exécutée contre une base de production sans lock_timeout est un bug, même si elle réussit. Pas une question de style, pas un bonus — un bug. Un billet Medium qui circule cette semaine décrit le mode de défaillance qui m'a convaincu de ça il y a des années, et ça vaut la peine de le redire à tous ceux qui lancent php artisan migrate ou doctrine:migrations:migrate comme étape de déploiement, parce que nos outils ne font pas grand-chose pour nous protéger ici.

La mécanique, en bref. Sur PostgreSQL, ajouter une colonne nullable à une table — même avec 50 millions de lignes — ne réécrit aucune donnée. Le catalogue est mis à jour, les valeurs manquantes sont traitées comme NULL, et l'opération elle-même ne coûte presque rien. Ce qu'elle coûte, c'est un verrou : ALTER TABLE a besoin d'un verrou ACCESS EXCLUSIVE, le plus strict de Postgres. Et l'acquisition des verrous dans Postgres est une file équitable. Si une requête de reporting lit cette table depuis vingt minutes, ton ALTER attend derrière elle — jusque-là, rien de grave — mais chaque requête qui arrive après ton ALTER attend derrière l'ALTER. Lectures comprises. Ton appli ne ralentit pas ; elle s'arrête. La migration qui prenait une milliseconde sur le staging devient le bouchon dans la bouteille en production.

La parade est ridiculement petite : SET lock_timeout = '2s' avant l'ALTER. Désormais, la migration obtient son verrou rapidement ou abandonne, le trafic accumulé s'écoule, et tes utilisateurs ont vu un accroc de deux secondes au lieu d'un canal d'incident qui se remplit. Ton déploiement échoue, certes — mais il échoue bruyamment, de façon rejouable, et sans dégâts collatéraux. Une migration ratée, c'est un mardi comme un autre. Une table users bloquée, c'est un post-mortem.

Alors pourquoi ce n'est pas le défaut partout ? C'est là que je veux pointer du doigt notre propre écosystème, gentiment. Le runner de migrations de Laravel enverra sans sourciller un ALTER TABLE sans le moindre lock_timeout. Doctrine Migrations te laisse pareillement te débrouiller. Les deux se comportent raisonnablement — ils sont agnostiques côté base, et la sémantique des verrous varie énormément entre Postgres, MySQL et compagnie — mais le résultat pratique, c'est que des milliers de boîtes PHP déploient des changements de schéma avec une attente illimitée intégrée, et la plupart ne le sauront jamais, jusqu'au jour où la requête d'un analyste et un déploiement se chevauchent par hasard. Le staging ne peut pas l'attraper, parce que le staging n'a pas cet analyste.

Prenons l'argument contraire au sérieux, parce qu'il est réel : un lock_timeout strict rend tes déploiements instables. La migration échoue, le pipeline passe au rouge, quelqu'un se fait réveiller pour un déploiement qui serait passé sans problème dix secondes plus tard. Si ton process traite un pipeline rouge comme une urgence, tu as troqué une panne catastrophique rare contre des échecs agaçants fréquents, et les équipes sous pression réagiront en supprimant le timeout. C'est une objection légitime — et elle te dit que le timeout ne peut pas se suffire à lui-même. Il lui faut une boucle de retry autour : tenter le verrou avec un timeout court, temporiser, réessayer, abandonner pour de bon après quelques minutes et alerter un humain. C'est peut-être trente lignes dans une classe de base de migration maison ou un wrapper autour de ton étape de déploiement. Une fois que ça existe, l'argument de l'instabilité s'évapore, parce que la contention transitoire sur les verrous se résout toute seule sans réveiller personne.

Il y a pourtant un changement plus profond caché sous la syntaxe. On a tendance à ranger les migrations dans la case « déploiement de code » — versionnées, relues, mergées, terminé. Mais une migration est aussi une opération en direct contre un système sous trafic, et les opérations en direct ont besoin de budgets : combien de temps j'ai le droit d'attendre, combien de temps j'ai le droit de tourner, que se passe-t-il si je dépasse. On pense déjà comme ça pour les appels HTTP — plus personne ne livre un client Guzzle sans timeout — et pour les jobs de queue avec leurs réglages $timeout et de retry. Les changements de schéma méritent la même discipline. Un fichier de migration qui dit ADD COLUMN mais ne dit pas combien de temps il peut bloquer n'est qu'une demi-spécification.

La demande concrète, donc : rends le budget de verrou structurel, pas du savoir tribal. Mets le SET lock_timeout dans une classe de base de migration partagée ou dans la config de connexion utilisée par ton runner de migrations, ajoute le wrapper de retry, et pose un check CI dessus si tu peux. Ne compte pas sur la présence en revue de la seule personne qui a lu le bon billet de blog. Tout l'intérêt du correctif, c'est qu'il fonctionne précisément le jour où personne n'y pense.

Et voilà ce que j'aimerais vraiment savoir de toi, parce que j'ai vu des équipes atterrir à des endroits très différents : où fixes-tu le chiffre ? Deux secondes, c'est défendable, mais 500ms sur une table OLTP très sollicitée aussi, et 30 secondes sur un système avec des transactions massives mais bornées aussi. Et qui possède le retry — l'outil de migration, le script de déploiement, ou l'orchestrateur ? Si tu as câblé tout ça dans un pipeline Laravel ou Symfony d'une manière qui a survécu au contact de la production, raconte-nous comment dans les commentaires. C'est la partie qu'aucun framework ne peut décider à ta place.