healthChecks ne peut rien injecter : les sondes sont figées avant que le conteneur Nest existe #106

Closed
opened 2026-10-01 13:32:22 +00:00 by nicolas-fryder · 1 comment

Ce qui se passe

healthChecks est lu dans les options, donc avant que createEqsApp() construise l'application Nest. Une sonde ne peut donc pas injecter un service du conteneur.

Pour une sonde de base de données, cela ne se voit pas : le client Prisma vit hors du graphe de modules (c'est même ce que prescrit prisma.ts, ticket #13), et verifier le referme simplement.

Pour tout le reste, ça coince. Nuage sonde sa file de conversion, qui est un provider (ConversionQueue, dessus BullMQ et Redis). Il n'y a aucun moyen de l'obtenir au moment où les options se construisent.

Ce que nuage a dû écrire

let application: INestApplication | undefined

return {
  healthChecks: [
    { nom: 'base', vital: true, verifier: async () => { await client.$queryRaw`SELECT 1` } },
    {
      nom: 'file',
      vital: false,
      verifier: async () => {
        const file = application?.get(ConversionQueue, { strict: false })
        if (!file) return
        await file.getStats()
      },
    },
  ],
  beforeListen: (app) => { application = app },
}

Ça marche — /health n'est servi qu'après beforeListen, donc la référence est toujours posée quand une sonde tourne — mais c'est une variable mutable capturée par fermeture, et le if (!file) return rend la sonde silencieusement vacante si l'ordre changeait un jour. Ce n'est pas le genre de chose qu'on veut voir recopiée dans trente satellites.

Proposition

Donner à verifier l'application, comme beforeListen la donne :

verifier: (app: INestApplication) => Promise<void>

Les sondes existantes ignorent simplement l'argument ; celles qui ont besoin du conteneur l'obtiennent sans fermeture. C'est additif et rétrocompatible.

Une variante qui marcherait aussi : accepter healthChecks en fonction de l'application — healthChecks?: VerificationSante[] | ((app: INestApplication) => VerificationSante[]) — mais la première me semble plus simple à lire.

Trouvé en rétrofittant nuage (profil nest), socle 0.14.0.

## Ce qui se passe `healthChecks` est lu dans les options, donc **avant** que `createEqsApp()` construise l'application Nest. Une sonde ne peut donc pas injecter un service du conteneur. Pour une sonde de base de données, cela ne se voit pas : le client Prisma vit hors du graphe de modules (c'est même ce que prescrit `prisma.ts`, ticket #13), et `verifier` le referme simplement. Pour tout le reste, ça coince. Nuage sonde sa file de conversion, qui est un provider (`ConversionQueue`, dessus BullMQ et Redis). Il n'y a aucun moyen de l'obtenir au moment où les options se construisent. ## Ce que nuage a dû écrire ```ts let application: INestApplication | undefined return { healthChecks: [ { nom: 'base', vital: true, verifier: async () => { await client.$queryRaw`SELECT 1` } }, { nom: 'file', vital: false, verifier: async () => { const file = application?.get(ConversionQueue, { strict: false }) if (!file) return await file.getStats() }, }, ], beforeListen: (app) => { application = app }, } ``` Ça marche — `/health` n'est servi qu'après `beforeListen`, donc la référence est toujours posée quand une sonde tourne — mais c'est une variable mutable capturée par fermeture, et le `if (!file) return` rend la sonde silencieusement vacante si l'ordre changeait un jour. Ce n'est pas le genre de chose qu'on veut voir recopiée dans trente satellites. ## Proposition Donner à `verifier` l'application, comme `beforeListen` la donne : ```ts verifier: (app: INestApplication) => Promise<void> ``` Les sondes existantes ignorent simplement l'argument ; celles qui ont besoin du conteneur l'obtiennent sans fermeture. C'est additif et rétrocompatible. Une variante qui marcherait aussi : accepter `healthChecks` en **fonction** de l'application — `healthChecks?: VerificationSante[] | ((app: INestApplication) => VerificationSante[])` — mais la première me semble plus simple à lire. Trouvé en rétrofittant nuage (profil `nest`), socle 0.14.0.
Author
Owner

Corrigé par 29f181d, PR #111, changeset sonde-resout-un-provider. Fermeture à la fusion.

verifier: (conteneur) => conteneur.get(ConversionQueue, { strict: false }).getStats()

J'ai dévié de votre proposition, et voici pourquoi

Vous demandiez l'INestApplication, comme beforeListen la donne. C'est ModuleRef qui est passé.

La première raison est la capacité : une sonde a besoin de résoudre un provider, pas de piloter le cycle de vie d'une application. ModuleRef.get(X, { strict: false }) est exactement le geste de votre contournement, sans le reste.

La seconde est celle qui tranche : ModuleRef s'injecte dans SanteController, là où les sondes tournent. L'INestApplication, elle, devrait être capturée dans l'assemblage puis transmise aux sondes — c'est-à-dire réintroduire, dans le socle, exactement la fermeture que ce ticket veut retirer des satellites. Le correctif aurait porté le défaut qu'il corrige.

Votre variante healthChecks en fonction de l'application aurait le même problème, pour la même raison.

Rétrocompatible, et un test le fixe

Une fonction qui prend moins d'arguments reste assignable : verifier: () => … compile et tourne comme avant. Le test « pour celle qui ignore l'argument » est à côté de celui qui résout — sans lui, ce changement casserait tous les satellites sans que rien ne le dise.

Trois cas éprouvés :

cas ce qu'il fixe
la sonde qui résout un provider le geste que #106 rendait impossible
la sonde de l'ancienne forme la rétrocompatibilité
un provider introuvable 503 et le message — sans lui, une sonde qui n'attrape rien passerait les deux premiers

Éprouvé en ne passant pas le conteneur : le premier rougit.

Votre diagnostic du if (!file) return

c'est une variable mutable capturée par fermeture, et le if (!file) return rend la sonde silencieusement vacante si l'ordre changeait un jour. Ce n'est pas le genre de chose qu'on veut voir recopiée dans trente satellites.

C'est la phrase qui a décidé de la forme du correctif. Une sonde vacante qui rend up est de la même famille que #108 (un groupe à 0/n qui affiche ✓) et #100 (une porte qui ne lint rien) — trois fois le même jour, un dispositif qui rend vert sans avoir rien regardé.

Une erreur à moi, pour mémoire

Mon premier test attendait status: 'up' au niveau global. C'est 'ok' — seules les sondes individuelles sont up/down. Corrigé dans le test, pas dans le code : c'était mon assertion qui était fausse.

Sept changesets en attente ; 0.15.0 sortira quand le flux de tickets se tarira, pas avant.

Corrigé par `29f181d`, PR #111, changeset `sonde-resout-un-provider`. Fermeture à la fusion. ```ts verifier: (conteneur) => conteneur.get(ConversionQueue, { strict: false }).getStats() ``` ## J'ai dévié de votre proposition, et voici pourquoi Vous demandiez l'`INestApplication`, comme `beforeListen` la donne. C'est **`ModuleRef`** qui est passé. La première raison est la capacité : une sonde a besoin de *résoudre un provider*, pas de piloter le cycle de vie d'une application. `ModuleRef.get(X, { strict: false })` est exactement le geste de votre contournement, sans le reste. La seconde est celle qui tranche : **`ModuleRef` s'injecte dans `SanteController`**, là où les sondes tournent. L'`INestApplication`, elle, devrait être capturée dans l'assemblage puis transmise aux sondes — c'est-à-dire réintroduire, dans le socle, exactement la fermeture que ce ticket veut retirer des satellites. Le correctif aurait porté le défaut qu'il corrige. Votre variante `healthChecks` en fonction de l'application aurait le même problème, pour la même raison. ## Rétrocompatible, et un test le fixe Une fonction qui prend moins d'arguments reste assignable : `verifier: () => …` compile et tourne comme avant. Le test « pour celle qui ignore l'argument » est **à côté** de celui qui résout — sans lui, ce changement casserait tous les satellites sans que rien ne le dise. Trois cas éprouvés : | cas | ce qu'il fixe | | --- | --- | | la sonde qui résout un provider | le geste que #106 rendait impossible | | la sonde de l'ancienne forme | la rétrocompatibilité | | un provider introuvable | 503 et le message — sans lui, une sonde qui n'attrape rien passerait les deux premiers | Éprouvé en ne passant pas le conteneur : le premier rougit. ## Votre diagnostic du `if (!file) return` > c'est une variable mutable capturée par fermeture, et le `if (!file) return` rend la sonde silencieusement vacante si l'ordre changeait un jour. Ce n'est pas le genre de chose qu'on veut voir recopiée dans trente satellites. C'est la phrase qui a décidé de la forme du correctif. Une sonde vacante qui rend `up` est de la même famille que #108 (un groupe à 0/n qui affiche ✓) et #100 (une porte qui ne lint rien) — trois fois le même jour, un dispositif qui rend vert sans avoir rien regardé. ## Une erreur à moi, pour mémoire Mon premier test attendait `status: 'up'` au niveau global. C'est `'ok'` — seules les sondes individuelles sont `up`/`down`. Corrigé dans le test, pas dans le code : c'était mon assertion qui était fausse. Sept changesets en attente ; 0.15.0 sortira quand le flux de tickets se tarira, pas avant.
Sign in to join this conversation.
No milestone
No project
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#106
No description provided.