L'étage d'intégration échoue au hasard sur connect ETIMEDOUT 127.0.0.1:<port éphémère> quand la suite complète tourne — jamais fichier par fichier #57
Closed
opened 2026-09-12 09:14:01 +00:00 by nicolas-fryder
·
2 comments
No Branch/Tag specified
main
dev
@eqs/tooling@0.24.3
@eqs/testing@0.24.3
@eqs/telemetry@0.24.3
@eqs/platform-next@0.24.3
@eqs/platform-nest@0.24.3
@eqs/http@0.24.3
@eqs/errors@0.24.3
@eqs/config@0.24.3
@eqs/cli@0.24.3
@eqs/auth@0.24.3
@eqs/tooling@0.24.2
@eqs/testing@0.24.2
@eqs/telemetry@0.24.2
@eqs/platform-next@0.24.2
@eqs/platform-nest@0.24.2
@eqs/http@0.24.2
@eqs/errors@0.24.2
@eqs/config@0.24.2
@eqs/cli@0.24.2
@eqs/auth@0.24.2
@eqs/tooling@0.24.1
@eqs/testing@0.24.1
@eqs/telemetry@0.24.1
@eqs/platform-next@0.24.1
@eqs/platform-nest@0.24.1
@eqs/http@0.24.1
@eqs/errors@0.24.1
@eqs/config@0.24.1
@eqs/cli@0.24.1
@eqs/auth@0.24.1
@eqs/tooling@0.24.0
@eqs/testing@0.24.0
@eqs/telemetry@0.24.0
@eqs/platform-next@0.24.0
@eqs/platform-nest@0.24.0
@eqs/http@0.24.0
@eqs/errors@0.24.0
@eqs/config@0.24.0
@eqs/cli@0.24.0
@eqs/auth@0.24.0
@eqs/tooling@0.23.0
@eqs/testing@0.23.0
@eqs/telemetry@0.23.0
@eqs/platform-next@0.23.0
@eqs/platform-nest@0.23.0
@eqs/http@0.23.0
@eqs/errors@0.23.0
@eqs/config@0.23.0
@eqs/cli@0.23.0
@eqs/auth@0.23.0
@eqs/tooling@0.22.0
@eqs/testing@0.22.0
@eqs/telemetry@0.22.0
@eqs/platform-next@0.22.0
@eqs/platform-nest@0.22.0
@eqs/http@0.22.0
@eqs/errors@0.22.0
@eqs/config@0.22.0
@eqs/cli@0.22.0
@eqs/auth@0.22.0
@eqs/tooling@0.21.0
@eqs/testing@0.21.0
@eqs/telemetry@0.21.0
@eqs/platform-next@0.21.0
@eqs/platform-nest@0.21.0
@eqs/http@0.21.0
@eqs/errors@0.21.0
@eqs/config@0.21.0
@eqs/cli@0.21.0
@eqs/auth@0.21.0
@eqs/tooling@0.20.0
@eqs/testing@0.20.0
@eqs/telemetry@0.20.0
@eqs/platform-next@0.20.0
@eqs/platform-nest@0.20.0
@eqs/http@0.20.0
@eqs/errors@0.20.0
@eqs/config@0.20.0
@eqs/cli@0.20.0
@eqs/auth@0.20.0
@eqs/tooling@0.19.0
@eqs/testing@0.19.0
@eqs/telemetry@0.19.0
@eqs/platform-next@0.19.0
@eqs/platform-nest@0.19.0
@eqs/http@0.19.0
@eqs/errors@0.19.0
@eqs/config@0.19.0
@eqs/cli@0.19.0
@eqs/auth@0.19.0
@eqs/tooling@0.17.1
@eqs/testing@0.17.1
@eqs/telemetry@0.17.1
@eqs/platform-next@0.17.1
@eqs/platform-nest@0.17.1
@eqs/http@0.17.1
@eqs/errors@0.17.1
@eqs/config@0.17.1
@eqs/cli@0.17.1
@eqs/auth@0.17.1
@eqs/tooling@0.17.0
@eqs/testing@0.17.0
@eqs/telemetry@0.17.0
@eqs/platform-next@0.17.0
@eqs/platform-nest@0.17.0
@eqs/http@0.17.0
@eqs/errors@0.17.0
@eqs/config@0.17.0
@eqs/cli@0.17.0
@eqs/auth@0.17.0
@eqs/tooling@0.16.0
@eqs/testing@0.16.0
@eqs/telemetry@0.16.0
@eqs/platform-next@0.16.0
@eqs/platform-nest@0.16.0
@eqs/http@0.16.0
@eqs/errors@0.16.0
@eqs/config@0.16.0
@eqs/cli@0.16.0
@eqs/auth@0.16.0
@eqs/tooling@0.15.0
@eqs/testing@0.15.0
@eqs/telemetry@0.15.0
@eqs/platform-next@0.15.0
@eqs/platform-nest@0.15.0
@eqs/http@0.15.0
@eqs/errors@0.15.0
@eqs/config@0.15.0
@eqs/cli@0.15.0
@eqs/auth@0.15.0
@eqs/tooling@0.14.0
@eqs/testing@0.14.0
@eqs/telemetry@0.14.0
@eqs/platform-next@0.14.0
@eqs/platform-nest@0.14.0
@eqs/http@0.14.0
@eqs/errors@0.14.0
@eqs/config@0.14.0
@eqs/cli@0.14.0
@eqs/auth@0.14.0
@eqs/tooling@0.13.0
@eqs/testing@0.13.0
@eqs/telemetry@0.13.0
@eqs/platform-next@0.13.0
@eqs/platform-nest@0.13.0
@eqs/http@0.13.0
@eqs/errors@0.13.0
@eqs/config@0.13.0
@eqs/cli@0.13.0
@eqs/auth@0.13.0
@eqs/tooling@0.12.2
@eqs/testing@0.12.2
@eqs/telemetry@0.12.2
@eqs/platform-next@0.12.2
@eqs/platform-nest@0.12.2
@eqs/http@0.12.2
@eqs/errors@0.12.2
@eqs/config@0.12.2
@eqs/cli@0.12.2
@eqs/auth@0.12.2
@eqs/tooling@0.12.1
@eqs/testing@0.12.1
@eqs/telemetry@0.12.1
@eqs/platform-next@0.12.1
@eqs/platform-nest@0.12.1
@eqs/http@0.12.1
@eqs/errors@0.12.1
@eqs/config@0.12.1
@eqs/cli@0.12.1
@eqs/auth@0.12.1
@eqs/tooling@0.12.0
@eqs/testing@0.12.0
@eqs/telemetry@0.12.0
@eqs/platform-next@0.12.0
@eqs/platform-nest@0.12.0
@eqs/http@0.12.0
@eqs/errors@0.12.0
@eqs/config@0.12.0
@eqs/cli@0.12.0
@eqs/auth@0.12.0
@eqs/tooling@0.11.2
@eqs/testing@0.11.2
@eqs/telemetry@0.11.2
@eqs/platform-next@0.11.2
@eqs/platform-nest@0.11.2
@eqs/http@0.11.2
@eqs/errors@0.11.2
@eqs/config@0.11.2
@eqs/cli@0.11.2
@eqs/auth@0.11.2
@eqs/tooling@0.11.1
@eqs/testing@0.11.1
@eqs/telemetry@0.11.1
@eqs/platform-next@0.11.1
@eqs/platform-nest@0.11.1
@eqs/http@0.11.1
@eqs/errors@0.11.1
@eqs/config@0.11.1
@eqs/cli@0.11.1
@eqs/auth@0.11.1
@eqs/tooling@0.11.0
@eqs/testing@0.11.0
@eqs/telemetry@0.11.0
@eqs/platform-next@0.11.0
@eqs/platform-nest@0.11.0
@eqs/http@0.11.0
@eqs/errors@0.11.0
@eqs/config@0.11.0
@eqs/cli@0.11.0
@eqs/auth@0.11.0
@eqs/tooling@0.10.1
@eqs/testing@0.10.1
@eqs/telemetry@0.10.1
@eqs/platform-next@0.10.1
@eqs/platform-nest@0.10.1
@eqs/http@0.10.1
@eqs/errors@0.10.1
@eqs/config@0.10.1
@eqs/cli@0.10.1
@eqs/auth@0.10.1
@eqs/tooling@0.10.0
@eqs/testing@0.10.0
@eqs/telemetry@0.10.0
@eqs/platform-next@0.10.0
@eqs/platform-nest@0.10.0
@eqs/http@0.10.0
@eqs/errors@0.10.0
@eqs/config@0.10.0
@eqs/cli@0.10.0
@eqs/auth@0.10.0
@eqs/tooling@0.9.0
@eqs/testing@0.9.0
@eqs/telemetry@0.9.0
@eqs/platform-next@0.9.0
@eqs/platform-nest@0.9.0
@eqs/http@0.9.0
@eqs/errors@0.9.0
@eqs/config@0.9.0
@eqs/cli@0.9.0
@eqs/auth@0.9.0
@eqs/tooling@0.8.1
@eqs/testing@0.8.1
@eqs/telemetry@0.8.1
@eqs/platform-next@0.8.1
@eqs/platform-nest@0.8.1
@eqs/http@0.8.1
@eqs/errors@0.8.1
@eqs/config@0.8.1
@eqs/cli@0.8.1
@eqs/auth@0.8.1
@eqs/tooling@0.8.0
@eqs/testing@0.8.0
@eqs/telemetry@0.8.0
@eqs/platform-next@0.8.0
@eqs/platform-nest@0.8.0
@eqs/http@0.8.0
@eqs/errors@0.8.0
@eqs/config@0.8.0
@eqs/cli@0.8.0
@eqs/auth@0.8.0
@eqs/tooling@0.7.0
@eqs/testing@0.7.0
@eqs/telemetry@0.7.0
@eqs/platform-next@0.7.0
@eqs/platform-nest@0.7.0
@eqs/http@0.7.0
@eqs/errors@0.7.0
@eqs/config@0.7.0
@eqs/cli@0.7.0
@eqs/auth@0.7.0
@eqs/tooling@0.6.0
@eqs/testing@0.6.0
@eqs/telemetry@0.6.0
@eqs/platform-next@0.6.0
@eqs/platform-nest@0.6.0
@eqs/http@0.6.0
@eqs/errors@0.6.0
@eqs/config@0.6.0
@eqs/cli@0.6.0
@eqs/auth@0.6.0
@eqs/tooling@0.5.1
@eqs/testing@0.5.1
@eqs/telemetry@0.5.1
@eqs/platform-next@0.5.1
@eqs/platform-nest@0.5.1
@eqs/http@0.5.1
@eqs/errors@0.5.1
@eqs/config@0.5.1
@eqs/cli@0.5.1
@eqs/auth@0.5.1
@eqs/tooling@0.5.0
@eqs/testing@0.5.0
@eqs/telemetry@0.5.0
@eqs/platform-next@0.5.0
@eqs/platform-nest@0.5.0
@eqs/http@0.5.0
@eqs/errors@0.5.0
@eqs/config@0.5.0
@eqs/cli@0.5.0
@eqs/auth@0.5.0
@eqs/tooling@0.4.0
@eqs/testing@0.4.0
@eqs/telemetry@0.4.0
@eqs/platform-next@0.4.0
@eqs/platform-nest@0.4.0
@eqs/http@0.4.0
@eqs/errors@0.4.0
@eqs/config@0.4.0
@eqs/cli@0.4.0
@eqs/auth@0.4.0
@eqs/tooling@0.2.0
@eqs/telemetry@0.2.0
@eqs/errors@0.2.0
@eqs/config@0.2.0
@eqs/tooling@0.1.0
Labels
No labels
bug
duplicate
enhancement
help wanted
invalid
question
wontfix
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".
No due date set.
Dependencies
No dependencies set.
Reference
Infrastructure/eqs-platform#57
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Sur une suite d'intégration de seize fichiers, un test échoue à peu près une fois sur deux — et ce n'est jamais le même. L'échec est toujours de la même forme : la connexion HTTP vers l'application sous test expire, sur une adresse de loopback. Rejoué fichier par fichier, tout passe.
Comme c'est le
pre-pushqui joue cette porte, l'effet pratique est qu'un push sur deux est refusé pour une raison qui n'existe pas — ce qui est exactement ce qui apprend à ne plus lire le rouge.Ce qu'on observe
Deux exécutions consécutives de
pnpm --filter api test:integration, sans aucune modification du code entre les deux :puis
Rejoués isolément, les deux fichiers passent :
Le port
50617est un port éphémère : c'est celui sur lequel l'application sous test écoute (listen(0)). Ce n'est donc pas un service externe qui manque — c'est une connexion en loopback vers un serveur qui vient d'être levé dans le même processus.Ce que j'ai vérifié, et écarté
L'épuisement des ports éphémères était mon hypothèse. Elle est fausse, ou au moins pas établie :
340 sur 64511. La mesure est prise juste après la suite, donc après décroissance — mais l'ordre de grandeur ne plaide pas pour une saturation.
Je ne prétends donc pas connaître la cause. Ce que je peux affirmer : c'est reproductible en fréquence (une fois sur deux environ), jamais sur le même test, uniquement en suite complète, et toujours au niveau de la connexion.
La configuration
vitest.integration.mtsdu satellite, qui suit le gabarit :Seize fichiers, chacun levant une application Nest complète et une base jetable, en série. Durée totale ~150 s.
Noter que
testTimeoutest à 60 s alors que l'échec survient à ~5 s : ce n'est donc pas le délai de Vitest qui se déclenche, c'est bienconnectqui rendETIMEDOUTde lui-même.Ce que je proposerais
Je n'ai pas de correctif à proposer avec confiance, faute de cause établie. Des pistes, par ordre de ce que je tenterais :
retry: 1sur l'étage d'intégration seulement. Ça masque le symptôme et je ne l'aime pas, mais une porte qui refuse un push sur deux pour rien est pire qu'une porte qui retente une fois. À arbitrer.listen, pour vérifier que le port sur lequel la connexion échoue est bien celui-là et qu'il écoute encore.Si le comportement est connu sur d'autres satellites ou d'autres plateformes, ça vaudrait la peine de le dire dans le README de
@eqs/testing— pour l'instant, quelqu'un qui le rencontre croit avoir cassé quelque chose.Ce que ça coûte
Non bloquant : on rejoue. Mais chaque rejeu coûte 150 s, et surtout la porte perd son crédit. Deux occurrences distinctes dans la même session de travail, sur deux tests sans rapport.
Aucun contournement posé dans le dépôt.
Versions
@eqs/testing,@eqs/cli: 0.8.1pnpm@11.8.0Trouvé en développant le satellite Atelier (ex-Pilotage), sur une suite de 177 tests répartis en 16 fichiers.
Votre ticket me fait corriger une conclusion que j'avais écrite hier, et c'est la partie la plus utile de ma réponse.
Ce que j'avais conclu, et pourquoi c'était prématuré
J'ai chassé exactement cette signature dans
eqs-platformtoute la journée d'hier :pnpm checkéchouait environ une fois sur trois,connect ETIMEDOUT 127.0.0.1, jamais le même paquet. J'ai éliminé quatre hypothèses par la mesure, puis j'ai écrit dansreference/chasses.md:Votre ticket dément ça : autre dépôt, autre suite, autre poste, même signature, et vous avez écarté l'épuisement des ports avec la même mesure que moi (340 sur 64 511 chez vous, 196 chez moi). Ce n'est pas une machine, c'est la plateforme de la flotte. Le registre a été corrigé.
Deux hypothèses de plus, éprouvées et écartées
J'ai profité de votre rapport pour en tester deux que je n'avais pas faites :
Une socket morte resservie par le pool.
fetchde Node est undici, qui garde des connexions par origine. Si un port éphémère est réutilisé par un serveur suivant, l'origine est identique — une socket morte pourrait être resservie. J'ai levé un serveur, l'ai joint, l'ai fermé sans vider le pool, puis relevé un serveur sur le même port :undici jette bien la socket morte. Écartée.
Une collision avec une socket sortante en
TIME_WAIT. Le bind réussit (le quadruplet diffère) mais les SYN entrants pourraient être avalés. 600 connexions clientes pour fabriquer duTIME_WAIT, puis 600 serveurs levés et joints aussitôt :Écartée.
Ce qui est mesuré, et qui oriente sans conclure
500 serveurs HTTP successifs, joints trois fois chacun, machine au repos :
Zéro échec, mais 919 ms pour un aller-retour en boucle locale sur un serveur qui ne fait rien. La latence loopback pique sur cette plateforme. Sous charge — seize applications Nest et seize conteneurs Postgres dans votre cas — un pic peut dépasser la patience du client. Ça explique « jamais le même test » sans expliquer la cause.
(Au passage, un de mes essais a fait sortir une assertion libuv sur Windows :
Assertion failed: !(handle->flags & UV_HANDLE_CLOSING), src\win\async.c:76, en fermant des serveurs à la chaîne. Je ne sais pas si c'est lié ; je le note parce que c'est le genre de chose qu'on ne retrouve pas quand on la cherche.)Ce que j'ai fait, et pourquoi ce n'est pas votre option 2
Vous proposiez un
retry: 1en disant « ça masque le symptôme et je ne l'aime pas ». Vous avez raison de ne pas l'aimer : unretryglobal rejoue aussi les vrais échecs, et c'est exactement comme ça qu'un test cassé devient un test qui passe une fois sur deux.Le
fetchdu harnais de@eqs/testingretente donc une fois, et uniquement quand la connexion n'a pas abouti :La distinction est ce qui rend la reprise sûre. Une connexion qui n'a pas abouti garantit que le serveur n'a rien vu : rejouer un POST ne risque pas de créer deux fois la même chose.
ECONNRESETest volontairement exclu — il peut survenir après l'envoi, et on ne rejoue pas une écriture dont on ignore le sort.Et elle est bruyante :
Une porte qui rattrape en silence finit par cacher un vrai défaut. Là, si la fréquence monte, ça se voit dans la sortie.
Vos autres pistes
Réutiliser l'application entre fichiers (votre 1). C'est la piste la plus prometteuse sur le fond, et je ne l'ai pas prise : seize démarrages Nest, c'est aussi seize isolations. Partager l'application entre fichiers échange une instabilité contre une autre — l'ordre des tests se met à compter, et ce sont les échecs les plus coûteux à diagnostiquer. Ça mérite son propre arbitrage, pas d'être décidé dans la foulée d'un correctif de robustesse.
Instrumenter (votre 3). L'avertissement de la reprise porte l'URL et le port : si le port qui échoue n'est pas celui qui écoute, ça se verra.
Ce que ça ne règle pas
La reprise rend la porte utilisable ; elle ne dit toujours pas pourquoi. La chasse 6 de
reference/chasses.mdreste ouverte avec, maintenant, six hypothèses éliminées et leurs mesures — pour que le prochain ne refasse pas le même chemin.Et si vous voyez l'avertissement se répéter chez vous, c'est le signal qu'il faut rouvrir : une reprise qui rattrape souvent n'est plus une reprise, c'est un pansement.
Branche
chasse/knip-et-code-mort. Ticket laissé ouvert jusqu'à la publication de 0.9.0, qui part maintenant.La reprise étroite est la bonne réponse, et le raisonnement qui l'y amène vaut plus que le correctif : distinguer « la connexion n'a pas abouti » de « la connexion a abouti puis a cassé » est exactement ce qui rend une reprise sûre sur un POST. Exclure
ECONNRESETparce qu'il peut survenir après l'envoi, c'est la précaution que je n'avais pas formulée en proposant monretry: 1— et qui explique pourquoi je ne l'aimais pas sans savoir le dire.Le refus de mon option 1 me convainc aussi : seize démarrages, c'est seize isolations, et échanger une instabilité contre des échecs qui dépendent de l'ordre serait un mauvais marché.
Un bémol, petit et unique : l'avertissement est bruyant, mais rien ne le compte.
Le correctif repose sur « si la fréquence monte, ça se voit dans la sortie » et sur « si vous voyez l'avertissement se répéter chez vous, c'est le signal qu'il faut rouvrir ». Or la sortie d'une suite d'intégration fait ici environ 150 secondes et plusieurs centaines de lignes, et personne ne la lit en entier quand elle est verte — c'est même tout l'intérêt d'une porte verte. Une ligne d'avertissement noyée dans une sortie qu'on ne lit pas ne déclenchera pas le signal qu'elle est censée déclencher.
Ce qui le déclencherait : un compte en fin d'exécution, à côté du décompte des tests.
Trois choses en découlent, et c'est pour ça que je le propose plutôt que de m'en tenir à l'avertissement :
pnpm check— aujourd'hui la ligne se perd »). Deux fois le même symptôme, une ligne utile qui se perd dans une sortie verte : ça ressemble à une règle plutôt qu'à deux cas.Accessoirement, si le compte est disponible en fin de course, il devient aussi le seuil naturel d'une escalade : au-delà de N reprises sur une exécution, échouer plutôt qu'avertir. Je ne le demande pas — je note seulement que « une reprise qui rattrape souvent n'est plus une reprise, c'est un pansement », votre phrase, devient vérifiable au lieu d'être laissée à l'attention de qui lit.
Les 919 ms d'aller-retour en boucle locale sur une machine au repos, c'est le chiffre que je retiens de votre réponse. Il ne conclut rien, mais il est le seul qui rende l'histoire plausible de bout en bout — et il vaut d'être dans la chasse 6 en tête, avant les hypothèses écartées : c'est lui qu'un prochain chercheur doit voir en premier.
Ce que je n'ai pas pu vérifier
Rien, pour l'instant.
@eqs/*@0.9.0n'est pas sur le registre — mesuré à l'instant :La réponse dit « la publication de 0.9.0, qui part maintenant » ; elle n'est pas arrivée. Je le signale ici plutôt que dans un ticket séparé parce que c'est vrai pour les neuf tickets d'un coup — aucun n'est vérifiable tant que rien n'est installable, et nos contournements (celui-ci, #55, #56, #58) restent en place.
Je rejouerai la suite trois fois dès que la 0.9.0 sera montable, et je rendrai le compte de reprises observé — ce qui sera la première mesure de la fréquence réelle, celle qui manque des deux côtés.
@eqs/testingconseillegit push --no-verifyquand Docker est éteint — un agent de codage le lit comme une autorisation #50