@nodefony/realtime 10.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +544 -0
- package/README.md +207 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
- package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
- package/dist/index.js +218 -0
- package/dist/nodefony/config/config.js +63 -0
- package/dist/nodefony/config/defineModuleConfig.js +60 -0
- package/dist/nodefony/decorators/realtimeDecorators.js +240 -0
- package/dist/nodefony/interfaces/IBackplane.js +1 -0
- package/dist/nodefony/interfaces/IChannelPolicy.js +1 -0
- package/dist/nodefony/interfaces/IClientLogsLimits.js +1 -0
- package/dist/nodefony/interfaces/IRealtimeAuthenticator.js +1 -0
- package/dist/nodefony/interfaces/IRealtimeAuthenticatorMatcher.js +1 -0
- package/dist/nodefony/interfaces/IRealtimeController.js +1 -0
- package/dist/nodefony/interfaces/IRealtimeHandshake.js +1 -0
- package/dist/nodefony/interfaces/IRealtimeProbe.js +1 -0
- package/dist/nodefony/interfaces/IRealtimeToken.js +1 -0
- package/dist/nodefony/interfaces/index.js +1 -0
- package/dist/nodefony/src/backplane/ClusterBackplane.js +105 -0
- package/dist/nodefony/src/backplane/LoopbackBackplane.js +40 -0
- package/dist/nodefony/src/backplane/RedisBackplane.js +176 -0
- package/dist/nodefony/src/backplane/backplaneRegistry.js +20 -0
- package/dist/nodefony/src/backplane/envelope.js +109 -0
- package/dist/nodefony/src/backplane/originId.js +35 -0
- package/dist/nodefony/src/backplane/publishQueue.js +96 -0
- package/dist/nodefony/src/cluster/ClusterProbeClient.js +284 -0
- package/dist/nodefony/src/server/AnonymousRealtimeToken.js +35 -0
- package/dist/nodefony/src/server/RealtimeAdminApi.js +87 -0
- package/dist/nodefony/src/server/RealtimeController.js +601 -0
- package/dist/nodefony/src/server/RealtimeHub.js +926 -0
- package/dist/nodefony/src/server/ServerRealtimeSocket.js +168 -0
- package/dist/nodefony/src/server/deniedDetail.js +29 -0
- package/dist/nodefony/src/server/syslogUplink.js +105 -0
- package/dist/nodefony/src/server/welcomeEnv.js +25 -0
- package/dist/nodefony/src/service/RealtimeService.js +240 -0
- package/dist/nodefony/src/transport/WsConnectionTransport.js +94 -0
- package/dist/nodefony/testing/index.js +163 -0
- package/dist/types/index.d.ts +99 -0
- package/dist/types/nodefony/config/config.d.ts +43 -0
- package/dist/types/nodefony/config/defineModuleConfig.d.ts +49 -0
- package/dist/types/nodefony/decorators/realtimeDecorators.d.ts +190 -0
- package/dist/types/nodefony/interfaces/IBackplane.d.ts +128 -0
- package/dist/types/nodefony/interfaces/IChannelPolicy.d.ts +28 -0
- package/dist/types/nodefony/interfaces/IClientLogsLimits.d.ts +22 -0
- package/dist/types/nodefony/interfaces/IRealtimeAuthenticator.d.ts +53 -0
- package/dist/types/nodefony/interfaces/IRealtimeAuthenticatorMatcher.d.ts +46 -0
- package/dist/types/nodefony/interfaces/IRealtimeController.d.ts +36 -0
- package/dist/types/nodefony/interfaces/IRealtimeHandshake.d.ts +27 -0
- package/dist/types/nodefony/interfaces/IRealtimeProbe.d.ts +206 -0
- package/dist/types/nodefony/interfaces/IRealtimeToken.d.ts +48 -0
- package/dist/types/nodefony/interfaces/index.d.ts +18 -0
- package/dist/types/nodefony/src/backplane/ClusterBackplane.d.ts +76 -0
- package/dist/types/nodefony/src/backplane/LoopbackBackplane.d.ts +29 -0
- package/dist/types/nodefony/src/backplane/RedisBackplane.d.ts +164 -0
- package/dist/types/nodefony/src/backplane/backplaneRegistry.d.ts +51 -0
- package/dist/types/nodefony/src/backplane/envelope.d.ts +27 -0
- package/dist/types/nodefony/src/backplane/originId.d.ts +22 -0
- package/dist/types/nodefony/src/backplane/publishQueue.d.ts +71 -0
- package/dist/types/nodefony/src/cluster/ClusterProbeClient.d.ts +90 -0
- package/dist/types/nodefony/src/server/AnonymousRealtimeToken.d.ts +3 -0
- package/dist/types/nodefony/src/server/RealtimeAdminApi.d.ts +18 -0
- package/dist/types/nodefony/src/server/RealtimeController.d.ts +165 -0
- package/dist/types/nodefony/src/server/RealtimeHub.d.ts +516 -0
- package/dist/types/nodefony/src/server/ServerRealtimeSocket.d.ts +63 -0
- package/dist/types/nodefony/src/server/deniedDetail.d.ts +25 -0
- package/dist/types/nodefony/src/server/syslogUplink.d.ts +58 -0
- package/dist/types/nodefony/src/server/welcomeEnv.d.ts +21 -0
- package/dist/types/nodefony/src/service/RealtimeService.d.ts +167 -0
- package/dist/types/nodefony/src/transport/WsConnectionTransport.d.ts +78 -0
- package/dist/types/nodefony/testing/index.d.ts +169 -0
- package/docs/actions.md +573 -0
- package/docs/architecture.md +889 -0
- package/docs/configuration.md +803 -0
- package/docs/cookbook-chat.md +1619 -0
- package/docs/couverture-bancs.md +139 -0
- package/docs/index.md +372 -0
- package/docs/observabilite.md +592 -0
- package/docs/protocole.md +518 -0
- package/docs/securite.md +845 -0
- package/docs/vocabulaire.md +845 -0
- package/package.json +83 -0
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Observabilité de la socket — la sonde, les canaux de santé, les écrans"
|
|
3
|
+
navTitle: Observabilité de la socket
|
|
4
|
+
lang: fr
|
|
5
|
+
module: "@nodefony/realtime"
|
|
6
|
+
topic: realtime
|
|
7
|
+
section: "Temps réel"
|
|
8
|
+
audience: [developer, devops, supervisor]
|
|
9
|
+
tags:
|
|
10
|
+
[realtime, observabilite, sonde, probe, backpressure, sante, studio, cluster]
|
|
11
|
+
version: "doc"
|
|
12
|
+
status: stable
|
|
13
|
+
updated: 2026-07-20
|
|
14
|
+
source: "src/packages/@nodefony/realtime/docs/observabilite.md"
|
|
15
|
+
coverageModule: realtime
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
# Observabilité de la socket — la sonde, les canaux de santé, les écrans
|
|
19
|
+
|
|
20
|
+
> Comment savoir que ta socket va bien. La socket Nodefony s'observe **à travers elle-même** : le hub
|
|
21
|
+
> tient ses propres constantes vitales, les sert en HTTP pour le premier affichage, et les rediffuse
|
|
22
|
+
> sur un canal temps réel — sur la socket qu'elles décrivent. Cette page dit ce que la sonde expose
|
|
23
|
+
> exactement, à quelle cadence, ce que Studio en montre, et comment publier ta propre sonde métier
|
|
24
|
+
> sans faire fuir la mémoire ni empêcher l'arrêt du processus.
|
|
25
|
+
|
|
26
|
+
📍 [Documentation](../../../../../docs/index.md) › [@nodefony/realtime](index.md) › **Observabilité**
|
|
27
|
+
|
|
28
|
+
```nodefony-livegraph
|
|
29
|
+
{
|
|
30
|
+
"graph": "sondes",
|
|
31
|
+
"height": 480,
|
|
32
|
+
"title": "La sonde, en direct",
|
|
33
|
+
"hint": "Sonde, agrégat, endpoint, minuteur, canal, écran. Le compteur de sondes vivantes est réel."
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Schéma général
|
|
38
|
+
|
|
39
|
+
Le patron tient en cinq pièces. Un **producteur** de mesures, un **agrégateur**, puis deux chemins de
|
|
40
|
+
lecture — un instantané et un flux — qui aboutissent au même écran.
|
|
41
|
+
|
|
42
|
+
```mermaid
|
|
43
|
+
flowchart LR
|
|
44
|
+
HUB["RealtimeHub<br/>compteurs vivants"] -->|"probe()"| PROBE["IRealtimeProbe<br/>snapshot pur"]
|
|
45
|
+
PROBE --> BUILD["buildRealtimeHealth()<br/>+ identité, process, ORM, erreurs"]
|
|
46
|
+
BUILD --> HTTP["GET /nodefony/realtime/api/health<br/>instantané, 1er affichage"]
|
|
47
|
+
BUILD --> TICK["createBrokerTicker<br/>minuteur détaché"]
|
|
48
|
+
TICK -->|"publish"| CH["canal nodefony:socket<br/>cadence 2 s par défaut"]
|
|
49
|
+
CH --> UI["Studio<br/>Temps réel · Cluster · Carte du serveur"]
|
|
50
|
+
HTTP --> UI
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Le détail qui rend le schéma vrai : le canal `nodefony:socket` **circule sur la socket elle-même**.
|
|
54
|
+
La mesure emprunte le transport qu'elle mesure — c'est ce qui rend l'observation gratuite en
|
|
55
|
+
infrastructure, et c'est aussi ce dont il faut se méfier quand le transport souffre.
|
|
56
|
+
|
|
57
|
+
## 📖 Lexique
|
|
58
|
+
|
|
59
|
+
| Terme | Développé | En une ligne |
|
|
60
|
+
| --------------------- | ----------------------------- | --------------------------------------------------------------------------------------------------- |
|
|
61
|
+
| **Sonde** (_probe_) | — | Un thermomètre branché sur un sous-système : il **lit** des compteurs, il n'en produit aucun. |
|
|
62
|
+
| **Snapshot** | instantané | Une photo des compteurs à un instant `ts`. Deux photos donnent un débit. |
|
|
63
|
+
| **Compteur monotone** | — | Un cumul qui ne redescend jamais depuis le démarrage. Le lecteur dérive le débit, le serveur non. |
|
|
64
|
+
| **Fan-out** | diffusion en éventail | Une publication livrée à N abonnés. `fanoutTotal / publishTotal` = taille moyenne d'un salon. |
|
|
65
|
+
| **Back-pressure** | contre-pression | La file d'envoi d'un client qui n'absorbe pas le flux. Le risque mémoire numéro un. |
|
|
66
|
+
| **`bufferedAmount`** | — | Octets déjà remis à la socket mais **pas encore partis** sur le réseau. Le signal de la souffrance. |
|
|
67
|
+
| **Slow consumer** | consommateur lent | Une connexion dont le `bufferedAmount` dépasse le seuil d'alerte. |
|
|
68
|
+
| **ELU** | _Event Loop Utilization_ | Fraction du temps où la boucle d'événements est active. ~1,0 = processeur saturé. |
|
|
69
|
+
| **Cadence** | granularité | L'intervalle de publication d'un canal d'état, inscrit dans son **nom** (`base:<ms>`). |
|
|
70
|
+
| **Per-instance** | par instance | Mesuré sur **ce** processus. En cluster, ce n'est pas la vérité du pod. |
|
|
71
|
+
| **Pod** | — | L'unité déployée : un maître et ses workers, vus comme un seul serveur. |
|
|
72
|
+
| **Drill-down** | forage | Demander à un worker précis d'enrichir sa remontée, le temps qu'on le regarde. |
|
|
73
|
+
| **IPC** | _Inter-Process Communication_ | Le canal de messages entre le maître d'un cluster et ses workers. |
|
|
74
|
+
|
|
75
|
+
## Qu'est-ce qu'observer une socket ?
|
|
76
|
+
|
|
77
|
+
Un serveur HTTP se surveille facilement : chaque requête a un début, une fin, un code. Une socket,
|
|
78
|
+
non — **elle n'a qu'un début**. Une connexion ouverte pendant six heures ne produit aucune ligne de
|
|
79
|
+
journal, et pourtant elle consomme, elle accumule, et parfois elle se dégrade.
|
|
80
|
+
|
|
81
|
+
L'analogie utile est celle d'un **standard téléphonique**. Ce qui compte n'est pas le nombre d'appels
|
|
82
|
+
passés, c'est : combien de lignes sont décrochées, combien de personnes écoutent chaque conférence, et
|
|
83
|
+
surtout **si l'une d'elles n'arrive plus à suivre**. Ce dernier point est le seul vrai danger.
|
|
84
|
+
|
|
85
|
+
**Le risque numéro un d'une socket n'est pas le processeur, c'est la mémoire.** Quand un client
|
|
86
|
+
n'absorbe pas ce qu'on lui envoie, les messages s'empilent dans une file d'envoi côté serveur. Cette
|
|
87
|
+
file n'a pas de borne naturelle : elle grossit jusqu'à ce que le client réagisse — ou jusqu'à ce que le
|
|
88
|
+
processus tombe. Et le multiplexage **concentre** ce risque : une seule connexion lente retient
|
|
89
|
+
l'ensemble des canaux qu'elle a demandés.
|
|
90
|
+
|
|
91
|
+
C'est pour cela que la sonde de la socket ne ressemble à aucune autre. Elle ne mesure pas d'abord un
|
|
92
|
+
temps de réponse — elle mesure **ce qui n'est pas encore parti**.
|
|
93
|
+
|
|
94
|
+
## La vision Nodefony — la socket s'observe à travers elle-même
|
|
95
|
+
|
|
96
|
+
Trois partis pris, tous lisibles dans le code.
|
|
97
|
+
|
|
98
|
+
**1. La sonde est une lecture pure, jamais un collecteur.** `RealtimeHub.probe()`
|
|
99
|
+
(`RealtimeHub.ts:775`) ne fait qu'additionner des primitives déjà tenues à jour par le chemin chaud.
|
|
100
|
+
Aucune entrée-sortie, aucune exception possible, aucun état de lecture conservé. Appeler la sonde
|
|
101
|
+
mille fois par seconde ne changerait rien à ce que le hub fait par ailleurs.
|
|
102
|
+
|
|
103
|
+
**2. Les cumuls sont monotones, le débit se dérive côté lecteur.** Le serveur ne calcule aucune
|
|
104
|
+
moyenne mobile, ne garde aucune fenêtre glissante. Il expose `publishTotal`, `bytesSentTotal`, `ts` —
|
|
105
|
+
et le lecteur soustrait deux photos. Conséquence : **zéro état de lecture côté serveur**, donc zéro
|
|
106
|
+
divergence entre deux consommateurs, et un rafraîchissement qui se choisit librement.
|
|
107
|
+
|
|
108
|
+
**3. La mesure voyage sur la socket qu'elle décrit.** Le même producteur alimente l'endpoint HTTP
|
|
109
|
+
(premier affichage, sans attendre un tick) et le canal temps réel (le suivi). Un seul code, deux
|
|
110
|
+
portes — `buildRealtimeHealth()` (`RealtimeAdminApi.ts:74`).
|
|
111
|
+
|
|
112
|
+
Le compromis assumé : cette sonde est **per-instance par nature**. Un pod à quatre workers rend quatre
|
|
113
|
+
vérités partielles. Nodefony ne les moyenne pas au hasard de la requête — il pousse une vue agrégée
|
|
114
|
+
explicite (voir plus bas), et le mode cluster reste **désactivable en bloc**.
|
|
115
|
+
|
|
116
|
+
## 🚀 Démarrage rapide
|
|
117
|
+
|
|
118
|
+
**Le besoin vécu** : ta boutique publie des commandes, et tu veux voir en direct — dans Studio, sans
|
|
119
|
+
écrire une ligne de frontend — combien de commandes passent et combien échouent. Tu ne veux ni agent
|
|
120
|
+
externe, ni base de séries temporelles, ni redémarrage.
|
|
121
|
+
|
|
122
|
+
Le principe : ton service tient déjà des compteurs ; la sonde ne fait que les **lire** et les
|
|
123
|
+
publier sur un canal.
|
|
124
|
+
|
|
125
|
+
### Le contrôleur — fichier créé
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
// modules/shop/nodefony/controllers/ShopProbeController.ts
|
|
129
|
+
import { controller, route } from "@nodefony/framework";
|
|
130
|
+
import { RealtimeController, RealtimeChannel } from "@nodefony/realtime";
|
|
131
|
+
import type { RealtimePublish } from "@nodefony/realtime";
|
|
132
|
+
import type { ContextType } from "@nodefony/http";
|
|
133
|
+
|
|
134
|
+
/** Les constantes vitales de la boutique : des primitives, rien d'autre. */
|
|
135
|
+
export interface ShopHealth {
|
|
136
|
+
ts: number;
|
|
137
|
+
ordersTotal: number;
|
|
138
|
+
failuresTotal: number;
|
|
139
|
+
pendingNow: number;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Compteurs tenus par le MÉTIER (incrémentés là où les commandes se traitent).
|
|
144
|
+
* La sonde les lit ; elle ne les produit pas, et surtout elle ne les mute pas.
|
|
145
|
+
*/
|
|
146
|
+
export const shopCounters = {
|
|
147
|
+
ordersTotal: 0,
|
|
148
|
+
failuresTotal: 0,
|
|
149
|
+
pendingNow: 0,
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
/** Le nom du canal : une constante, citée côté serveur ET côté client. */
|
|
153
|
+
export const SHOP_HEALTH = "shop:health";
|
|
154
|
+
|
|
155
|
+
@controller("/shop")
|
|
156
|
+
class ShopProbeController extends RealtimeController {
|
|
157
|
+
constructor(context: ContextType) {
|
|
158
|
+
super("shop", context);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** La porte WebSocket. Tout le protocole est porté par la classe de base. */
|
|
162
|
+
@route("shop-realtime", {
|
|
163
|
+
path: "/realtime",
|
|
164
|
+
requirements: { methods: ["WEBSOCKET"] },
|
|
165
|
+
})
|
|
166
|
+
async realtime(message: string | Buffer | null): Promise<void> {
|
|
167
|
+
this.handleRealtime(message);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* LE CANAL DE SANTÉ. Le hub appelle cette méthode au PREMIER abonné et attend
|
|
172
|
+
* en retour la fonction de nettoyage, qu'il appellera au DERNIER désabonné.
|
|
173
|
+
*
|
|
174
|
+
* Trois règles tiennent dans ces dix lignes :
|
|
175
|
+
* - `unref()` : le minuteur ne retient pas l'arrêt du processus ;
|
|
176
|
+
* - le nettoyage annule TOUT ce que le provider a démarré ;
|
|
177
|
+
* - le tick ne fait que LIRE des primitives — aucune collecte, aucun parcours.
|
|
178
|
+
*/
|
|
179
|
+
@RealtimeChannel(SHOP_HEALTH)
|
|
180
|
+
shopHealth(channel: string, publish: RealtimePublish): () => void {
|
|
181
|
+
const timer = setInterval(() => {
|
|
182
|
+
const payload: ShopHealth = {
|
|
183
|
+
ts: Date.now(),
|
|
184
|
+
ordersTotal: shopCounters.ordersTotal,
|
|
185
|
+
failuresTotal: shopCounters.failuresTotal,
|
|
186
|
+
pendingNow: shopCounters.pendingNow,
|
|
187
|
+
};
|
|
188
|
+
publish(channel, payload);
|
|
189
|
+
}, 2000);
|
|
190
|
+
timer.unref();
|
|
191
|
+
return () => clearInterval(timer);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export default ShopProbeController;
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### La configuration — fichier modifié
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
// nodefony.config.ts — le manifeste des modules (extrait)
|
|
202
|
+
import { defineConfig, use } from "nodefony";
|
|
203
|
+
|
|
204
|
+
export default defineConfig(() => ({
|
|
205
|
+
modules: [
|
|
206
|
+
use("@nodefony/http", {}),
|
|
207
|
+
"@nodefony/framework",
|
|
208
|
+
// La socket. `loopback` = un seul processus, aucune infrastructure externe.
|
|
209
|
+
use("@nodefony/realtime", { backplane: { driver: "loopback" } }),
|
|
210
|
+
"@mon-shop/shop",
|
|
211
|
+
],
|
|
212
|
+
}));
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Ce qu'on observe
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
# La santé de la socket elle-même — un instantané, sans WebSocket.
|
|
219
|
+
curl -k https://127.0.0.1:5152/nodefony/realtime/api/health
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Tant que personne n'est abonné à `shop:health`, il **n'apparaît pas** dans `channels[]` et **aucun
|
|
223
|
+
minuteur ne tourne** : un canal sans abonné n'existe pas. Dès qu'un client s'abonne, une ligne
|
|
224
|
+
apparaît, `subscribers` passe à 1, `messages` grimpe de 1 toutes les deux secondes, et
|
|
225
|
+
`fanoutTotal` suit le nombre de livraisons réelles.
|
|
226
|
+
|
|
227
|
+
Côté navigateur, deux lignes suffisent à consommer le canal :
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
// frontend/src/shopHealth.ts
|
|
231
|
+
import { RealtimeClient } from "nodefony/client";
|
|
232
|
+
|
|
233
|
+
const scheme = window.location.protocol === "https:" ? "wss" : "ws";
|
|
234
|
+
const socket = new RealtimeClient({
|
|
235
|
+
url: `${scheme}://${window.location.host}/shop/realtime`,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
socket.on("shop:health", (payload: unknown) => {
|
|
239
|
+
console.log("santé boutique", payload);
|
|
240
|
+
});
|
|
241
|
+
socket.subscribe("shop:health");
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
> [!TIP]
|
|
245
|
+
> Ferme l'onglet, et le minuteur s'arrête côté serveur. C'est la propriété la plus utile du patron :
|
|
246
|
+
> **on paie ce qu'on regarde**. Un tableau de bord fermé ne coûte rien.
|
|
247
|
+
|
|
248
|
+
## 🧰 Ce que la sonde expose, champ par champ
|
|
249
|
+
|
|
250
|
+
Le contrat est `IRealtimeProbe` (`IRealtimeProbe.ts:61`). Quatre familles, un discriminant.
|
|
251
|
+
|
|
252
|
+
### Canaux — qui vit, qui écoute
|
|
253
|
+
|
|
254
|
+
| Champ | Ce qu'il dit |
|
|
255
|
+
| ------------------------ | -------------------------------------------------------------------------------------------------------- |
|
|
256
|
+
| `channels[]` | Une entrée par canal **actif** (au moins un abonné), cf `IRealtimeChannelStat` (`IRealtimeProbe.ts:47`). |
|
|
257
|
+
| `channels[].channel` | Le nom exact souscrit, **suffixe de cadence compris** (`nodefony:orm:health:2000`). |
|
|
258
|
+
| `channels[].subscribers` | Abonnés locaux vivants. Instantané, pas un cumul. |
|
|
259
|
+
| `channels[].messages` | Publications cumulées sur ce canal. |
|
|
260
|
+
| `channelCount` | Nombre de canaux actifs sur ce processus. |
|
|
261
|
+
|
|
262
|
+
### Fan-out — l'amplification réelle du broker
|
|
263
|
+
|
|
264
|
+
| Champ | Ce qu'il dit |
|
|
265
|
+
| ---------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
266
|
+
| `publishTotal` | Appels à `publish`. **Ce que le serveur a voulu envoyer.** |
|
|
267
|
+
| `fanoutTotal` | Livraisons effectives (`publish` × abonnés). **Ce que ça a réellement coûté.** |
|
|
268
|
+
| `inboundTotal` | Frames poussées par les clients sur les canaux entrants, via `RealtimeHub.recordInbound()` (`RealtimeHub.ts:764`). |
|
|
269
|
+
| `ingressRejectedTotal` | Messages venus du **backplane** et jetés parce que leur canal n'est pas déclaré diffusable. |
|
|
270
|
+
|
|
271
|
+
Le rapport `fanoutTotal / publishTotal` est la mesure la plus parlante de la page : c'est la **taille
|
|
272
|
+
moyenne d'un salon**. S'il vaut 1, chaque publication ne touche qu'une personne — un canal par client,
|
|
273
|
+
autrement dit le fan-out ne sert à rien. S'il vaut 400, une publication coûte 400 sérialisations.
|
|
274
|
+
|
|
275
|
+
> [!IMPORTANT]
|
|
276
|
+
> **Un processus qui ne fait que publier paraît inactif.** `publishTotal` et `fanoutTotal` ne
|
|
277
|
+
> s'incrémentent que si le canal a un abonné **local** : un pod qui publie vers ses pairs, sans
|
|
278
|
+
> personne d'abonné chez lui — un travail planifié, un webhook entrant — affiche des compteurs à zéro
|
|
279
|
+
> alors qu'il pousse en continu. Pour juger de son activité, lire l'autre bout de la chaîne : les
|
|
280
|
+
> compteurs du pod qui **reçoit**.
|
|
281
|
+
|
|
282
|
+
### Messages du bus refusés — le signal qui ne devrait jamais bouger
|
|
283
|
+
|
|
284
|
+
`ingressRejectedTotal` compte les messages arrivés **par le bus** et refusés à l'entrée : leur canal
|
|
285
|
+
n'est pas déclaré diffusable, donc il n'a rien à faire en circulation entre processus. Les canaux
|
|
286
|
+
internes — journaux, audit, santé — sont dans ce cas par construction.
|
|
287
|
+
|
|
288
|
+
En fonctionnement normal il vaut **zéro** : un pair légitime n'émet que des canaux diffusables. Deux
|
|
289
|
+
lectures possibles quand il grimpe :
|
|
290
|
+
|
|
291
|
+
- **bénigne** — une autre application partage la même cloison de transport, et ses canaux sont
|
|
292
|
+
refusés ; chacun chez soi, mais le namespace mérite d'être séparé ;
|
|
293
|
+
- **sérieuse** — quelqu'un écrit sur votre bus. Une infrastructure de diffusion n'authentifie pas
|
|
294
|
+
l'émetteur : c'est le rôle du sceau (cf [Sécurité](./securite.md)).
|
|
295
|
+
|
|
296
|
+
Le canal visé n'est délibérément pas exposé : un compteur d'observabilité ne doit pas devenir un
|
|
297
|
+
moyen de sonder ce que le système accepte.
|
|
298
|
+
|
|
299
|
+
⚠️ Ce compteur ne voit pas tout : un message **mal scellé** est écarté par le transport, avant
|
|
300
|
+
d'atteindre le hub, donc sans être compté ici. Un bus scellé peut ainsi rester à zéro alors qu'il est
|
|
301
|
+
attaqué — le signe est alors l'absence de trafic légitime, pas ce compteur.
|
|
302
|
+
|
|
303
|
+
### Connexions et volume
|
|
304
|
+
|
|
305
|
+
`connectionCount`, `bytesSentTotal`, `messagesSentTotal` — vivants pour la première, cumulés pour les
|
|
306
|
+
deux autres. Le débit en octets par seconde se dérive de deux photos.
|
|
307
|
+
|
|
308
|
+
### Back-pressure — le risque numéro un
|
|
309
|
+
|
|
310
|
+
| Champ | Ce qu'il dit |
|
|
311
|
+
| ---------------------------------- | ----------------------------------------------------------------------------------- |
|
|
312
|
+
| `backpressure.maxBufferedAmount` | La **pire** file d'envoi de l'instant. C'est l'indicateur d'alerte, pas la moyenne. |
|
|
313
|
+
| `backpressure.totalBufferedAmount` | Somme des octets en attente sur tout le processus. |
|
|
314
|
+
| `backpressure.slowConsumers` | Connexions au-dessus du seuil de comptage. |
|
|
315
|
+
| `backpressure.drops` | Cumul des frames **jetées** pour protéger la mémoire. |
|
|
316
|
+
|
|
317
|
+
La source par connexion est `IRealtimeConnProbe` (`IRealtimeProbe.ts:25`), implémentée par le
|
|
318
|
+
transport. Les seuils d'ACTION sont distincts du seuil de comptage, et ils sont
|
|
319
|
+
**configurables** (`@nodefony/http`, `http/nodefony/config/config.ts:625`) : au-delà de
|
|
320
|
+
`websocket.maxBackpressure` (4 MiB par défaut) la politique `websocket.backpressurePolicy`
|
|
321
|
+
s'applique — `drop` par défaut, la frame est **jetée** puisque les canaux d'état sont « le
|
|
322
|
+
dernier gagne » et que le prochain instantané la remplacera. La connexion est **fermée** en
|
|
323
|
+
`1013` (« réessaie plus tard ») après `websocket.backpressureCloseAfterDrops` frames jetées
|
|
324
|
+
CONSÉCUTIVES (1000 par défaut) — une seule frame qui repart remet le compteur à zéro — ou
|
|
325
|
+
dès le premier dépassement si la politique est `close`.
|
|
326
|
+
|
|
327
|
+
> [!IMPORTANT]
|
|
328
|
+
> `drops` qui croît n'est **pas** une panne : c'est la protection qui fonctionne. Ce qui serait une
|
|
329
|
+
> panne, c'est une file d'envoi non bornée multipliée par le nombre de clients.
|
|
330
|
+
|
|
331
|
+
### Fond de panier
|
|
332
|
+
|
|
333
|
+
`backplane` porte la carte d'identité du relais réellement branché — driver, nature, origine,
|
|
334
|
+
franchissement de machine. Jamais `undefined` en pratique : sans backplane, la sonde rend un
|
|
335
|
+
descripteur `local`. C'est le champ à lire quand le temps réel ne traverse pas les répliques.
|
|
336
|
+
|
|
337
|
+
Trois champs n'apparaissent que lorsqu'ils ont un sens :
|
|
338
|
+
|
|
339
|
+
- `channel` — le canal de transport **effectif**. Il répond à la seule question qu'on ne peut pas
|
|
340
|
+
poser autrement : _suis-je branché sur le bus que je crois ?_ Le nom affiché est celui utilisé,
|
|
341
|
+
pas celui écrit dans la configuration (la cloison peut venir de l'environnement).
|
|
342
|
+
- `sealed` — les messages sont-ils signés ? Renseigné par les transports **partagés**, où un tiers
|
|
343
|
+
peut écrire. `false` y annonce un bus ouvert. Absent quand la question ne se pose pas : en
|
|
344
|
+
mono-processus, ou entre un maître et ses propres workers, personne d'autre ne peut publier.
|
|
345
|
+
- `queue` — la **file d'envoi** vers le bus, présente sur les transports réseau (leurs publications
|
|
346
|
+
sont acquittées plus tard). Publier n'attend pas : le message est confié au client réseau et le
|
|
347
|
+
code continue. Si le bus ralentit, ces messages s'accumulent — sur une file **interne au client**,
|
|
348
|
+
donc invisible. Quatre compteurs la rendent lisible : `bytes` (en attente d'accusé de réception),
|
|
349
|
+
`maxBytes` (le plafond, `0` = aucun), `droppedTotal` (publications abandonnées faute de place) et
|
|
350
|
+
`failedTotal` (publications refusées par le bus). `droppedTotal` est le seul de la sonde qui
|
|
351
|
+
signale une perte **volontaire** : la mémoire du processus a été préservée au prix de messages non
|
|
352
|
+
relayés aux autres répliques. Il doit rester à zéro ; s'il bouge, regarder l'état du bus avant de
|
|
353
|
+
relever le plafond (`backplane.maxQueueBytes`) — un plafond plus haut ne rattrape pas un bus en
|
|
354
|
+
retard, il repousse l'échéance.
|
|
355
|
+
|
|
356
|
+
### La couche d'identité, au-dessus
|
|
357
|
+
|
|
358
|
+
`buildOwnHealth()` (`RealtimeAdminApi.ts:52`) enrichit ce snapshot pur pour produire
|
|
359
|
+
`IRealtimeHealth` (`IRealtimeProbe.ts:106`) : `instanceId`, la santé **process** du worker
|
|
360
|
+
(CPU, mémoire, boucle d'événements), et — si elles ont été branchées — la santé ORM et les compteurs
|
|
361
|
+
d'erreurs du journal. Ces trois derniers champs sont **additifs et optionnels** : un consommateur qui
|
|
362
|
+
ne les connaît pas les ignore.
|
|
363
|
+
|
|
364
|
+
## 🔌 Deux chemins de lecture — l'instantané et le flux
|
|
365
|
+
|
|
366
|
+
Un seul producteur, deux portes. C'est volontaire : un tableau de bord qui n'aurait que le flux
|
|
367
|
+
resterait vide jusqu'au premier tick.
|
|
368
|
+
|
|
369
|
+
### L'instantané HTTP
|
|
370
|
+
|
|
371
|
+
`GET /nodefony/realtime/api/health`, monté par `createRealtimeAdminApi()` (`RealtimeAdminApi.ts:91`)
|
|
372
|
+
sous le namespace `realtime` du data plane admin. Sert le premier affichage, les sondes de liveness,
|
|
373
|
+
et tout script qui ne veut pas ouvrir une socket.
|
|
374
|
+
|
|
375
|
+
### Le canal temps réel
|
|
376
|
+
|
|
377
|
+
`nodefony:socket`. Le producteur est un minuteur qui rappelle **le même endpoint** à travers le
|
|
378
|
+
courtier admin (`StudioRealtimeController.createRealtimeChannel()`, `StudioRealtimeController.ts:182`,
|
|
379
|
+
via `createBrokerTicker()` (`providers.ts:465`)). Rien n'est dupliqué : le canal est un endpoint
|
|
380
|
+
rejoué.
|
|
381
|
+
|
|
382
|
+
### La cadence vit dans le nom du canal
|
|
383
|
+
|
|
384
|
+
Convention isomorphe, partagée par les deux bords : `base` nu = cadence par défaut du serveur ;
|
|
385
|
+
`base:<ms>` = cadence explicite. Le client fabrique le nom avec `rateChannel()` (`channelRate.ts:44`),
|
|
386
|
+
le serveur le résout et le **borne** avec `parseRate()` (`channelRate.ts:63`).
|
|
387
|
+
|
|
388
|
+
Conséquence voulue : **un canal = une cadence = un compteur de références**. `nodefony:socket:500` et
|
|
389
|
+
`nodefony:socket` sont deux minuteurs distincts, jamais réconciliés — et deux consommateurs qui
|
|
390
|
+
veulent tous deux le défaut partagent un seul minuteur, parce que la cadence par défaut ne produit
|
|
391
|
+
**aucun** suffixe.
|
|
392
|
+
|
|
393
|
+
### Les canaux de santé qui circulent sur la socket
|
|
394
|
+
|
|
395
|
+
Les bornes sont déclarées dans `RATE_BOUNDS` (`StudioRealtimeController.ts:59`), les noms dans
|
|
396
|
+
`CHANNELS` (`providers.ts:98`). Une cadence hors bornes est **ramenée dans l'intervalle**, jamais
|
|
397
|
+
refusée.
|
|
398
|
+
|
|
399
|
+
| Canal | Qui le produit | Cadence par défaut | Bornes |
|
|
400
|
+
| ---------------------- | ------------------------------------------------------------------ | ------------------ | ------------- |
|
|
401
|
+
| `nodefony:socket` | La sonde du hub, rejouée depuis l'endpoint admin `realtime` | 2 s | 500 ms – 60 s |
|
|
402
|
+
| `nodefony:supervision` | `createStatsTicker()` (`providers.ts:222`) — CPU, mémoire, GC, ELU | 1 s | 250 ms – 60 s |
|
|
403
|
+
| `nodefony:debugbar` | Le même producteur, canal séparé pour la barre de débogage | 1 s | 250 ms – 60 s |
|
|
404
|
+
| `nodefony:orm:health` | Endpoint admin `orm/connection/health`, rejoué | 5 s | 1 s – 60 s |
|
|
405
|
+
| `nodefony:orm:flow` | Endpoint admin `orm/flow` (débit de requêtes) | 2 s | 500 ms – 60 s |
|
|
406
|
+
| `nodefony:syslog` | `createSyslogBridge()` (`providers.ts:145`) | **événementiel** | — (coalescé) |
|
|
407
|
+
|
|
408
|
+
> [!WARNING]
|
|
409
|
+
> **`nodefony:dashboard` n'existe pas.** Le canal de supervision s'appelle `nodefony:supervision` ; ce
|
|
410
|
+
> nom n'apparaît plus que comme donnée de test. S'abonner à un canal inconnu ne lève **aucune
|
|
411
|
+
> erreur** — le provider rend `null` et rien n'arrive jamais. C'est le piège numéro un de la page.
|
|
412
|
+
|
|
413
|
+
`nodefony:syslog` est le seul de la liste à ne pas être cadencé : il **relaie** au lieu de sonder. Comme
|
|
414
|
+
un flot de journaux peut noyer un frontend, le pont accumule dans un tampon circulaire borné et n'émet
|
|
415
|
+
qu'une frame agrégée toutes les 200 ms, en comptant les entrées omises — le débit de la source est
|
|
416
|
+
découplé de celui de l'interface.
|
|
417
|
+
|
|
418
|
+
Deux autres formes existent, réservées au forage : `nodefony:supervision@<pid>` et `nodefony:orm:rich@<pid>`
|
|
419
|
+
ciblent **un worker précis**, et non le premier venu.
|
|
420
|
+
|
|
421
|
+
## 📡 Observabilité — Studio
|
|
422
|
+
|
|
423
|
+
Trois écrans consomment `nodefony:socket`, chacun avec une question différente.
|
|
424
|
+
|
|
425
|
+
- **Temps réel** (`/nodefony/hub`) — la console de la socket : canaux vivants et leurs abonnés,
|
|
426
|
+
volume diffusé, connexions en retard. C'est l'écran du développeur qui se demande « mon canal
|
|
427
|
+
est-il monté ? ».
|
|
428
|
+
- **Cluster** (`/nodefony/cluster`) — la vue pod : tous les workers, avec le détail par worker et le
|
|
429
|
+
forage vers un `pid`. L'écran de l'exploitation.
|
|
430
|
+
- **Carte du serveur** (`/nodefony/twin`) — le jumeau vivant : la même sonde projetée sur le graphe
|
|
431
|
+
d'architecture, chaque nœud coloré par son état.
|
|
432
|
+
|
|
433
|
+
Les trois sont des **abonnements comptés par référence** : monter la page active le minuteur côté
|
|
434
|
+
serveur, la démonter le coupe. Aucun d'eux ne tourne en tâche de fond.
|
|
435
|
+
|
|
436
|
+
La page module `/nodefony/modules/realtime` complète le tableau avec la **configuration effective**
|
|
437
|
+
après fusion et validation — la seule vue qui dit ce qui s'applique, plutôt que ce que tu as écrit.
|
|
438
|
+
|
|
439
|
+
## ⚙️ Configuration
|
|
440
|
+
|
|
441
|
+
| Clé | Défaut | Effet |
|
|
442
|
+
| ------------------------ | ------ | ------------------------------------------------------------------------- |
|
|
443
|
+
| `slowConsumer.bytes` | 1 Mio | Seuil de **comptage** de `slowConsumers` dans la sonde (`config.ts:121`). |
|
|
444
|
+
| `cluster.probe.enabled` | `true` | Branche la sonde agrégée pod en worker de cluster (`config.ts:30`). |
|
|
445
|
+
| `NF_CLUSTER_PROBE` (env) | — | Mis à `0`, coupe la sonde pod quelle que soit la configuration. |
|
|
446
|
+
|
|
447
|
+
> [!CAUTION]
|
|
448
|
+
> `slowConsumer.bytes` ne pilote **que la métrique**. Les seuils qui jettent une frame ou ferment une
|
|
449
|
+
> connexion sont des constantes du transport. Baisser cette clé rend le signalement plus précoce ; ça
|
|
450
|
+
> ne change rien au comportement de la socket.
|
|
451
|
+
|
|
452
|
+
Couper la sonde cluster est un **contournement total**, pas une mise en sourdine : sans elle, il n'y a
|
|
453
|
+
ni client, ni minuteur, ni écouteur, ni message IPC (`Realtime.wireClusterProbe()`, `index.ts:351`), et
|
|
454
|
+
l'endpoint retombe sur la vue per-instance. Aucun coût « au cas où ».
|
|
455
|
+
|
|
456
|
+
## 🏗️ En cluster — par pod ou par worker ?
|
|
457
|
+
|
|
458
|
+
**Le problème**, d'abord. En cluster, une requête de santé atterrit sur **un** worker au hasard. Son
|
|
459
|
+
instantané est exact — et ne dit rien des trois autres. Pire : deux appels consécutifs peuvent tomber
|
|
460
|
+
sur deux workers et sembler se contredire.
|
|
461
|
+
|
|
462
|
+
**La réponse de Nodefony est un modèle _push_.** Chaque worker remonte périodiquement sa santé au
|
|
463
|
+
maître via `ClusterProbeClient.start()` (`ClusterProbeClient.ts:170`) ; le maître fusionne et
|
|
464
|
+
rediffuse ; chaque worker met le résultat en cache. **N'importe lequel** sert alors la vue pod en temps
|
|
465
|
+
constant, sans latence de requête (`ClusterProbeClient.getClusterHealth()`, `ClusterProbeClient.ts:285`).
|
|
466
|
+
|
|
467
|
+
La fusion, `mergeClusterHealth()` (`ClusterProbeClient.ts:46`), est une fonction pure — et sa règle
|
|
468
|
+
mérite d'être connue :
|
|
469
|
+
|
|
470
|
+
| Grandeur | Agrégation | Pourquoi |
|
|
471
|
+
| ---------------------------------------------------------------- | ----------- | -------------------------------------------------------------------------- |
|
|
472
|
+
| `publishTotal`, `fanoutTotal`, `connectionCount`, octets, frames | **somme** | Le pod fait la somme de ce que font ses workers. |
|
|
473
|
+
| `ingressRejectedTotal` | somme | Un refus reste un refus, quel que soit le worker qui l'a opposé. |
|
|
474
|
+
| `backpressure.maxBufferedAmount` | **maximum** | La santé d'une flotte se juge sur son **pire** membre, pas sur sa moyenne. |
|
|
475
|
+
| `slowConsumers`, `drops` | somme | Ce sont des dénombrements. |
|
|
476
|
+
|
|
477
|
+
Le discriminant est le champ `cluster: true` de `IRealtimeClusterHealth` (`IRealtimeProbe.ts:155`) :
|
|
478
|
+
un consommateur sait immédiatement s'il lit une vue pod ou une vue per-instance, et `instances[]`
|
|
479
|
+
garde le détail par worker pour le forage.
|
|
480
|
+
|
|
481
|
+
### On paie ce qu'on regarde
|
|
482
|
+
|
|
483
|
+
Les sondes coûteuses ne tournent pas en permanence. Le maître peut demander à un worker précis
|
|
484
|
+
d'**enrichir** sa remontée — `ClusterProbeClient.requestEnrich()` (`ClusterProbeClient.ts:239`) — quand
|
|
485
|
+
un humain ouvre le forage de ce `pid`. La sonde riche (espaces mémoire, cycles de ramasse-miettes,
|
|
486
|
+
descripteurs actifs) est alors allouée ; à la fermeture de l'écran, elle est libérée et son observateur
|
|
487
|
+
détaché.
|
|
488
|
+
|
|
489
|
+
Ce qui est **par pod** : la vue agrégée, `instances[]`, les totaux. Ce qui reste **par worker** :
|
|
490
|
+
absolument tout le reste — un `channelCount` per-instance ne dit rien du pod, et une connexion
|
|
491
|
+
appartient à un seul processus.
|
|
492
|
+
|
|
493
|
+
## ⚡ Performance & mémoire — écrire une sonde qui ne coûte rien
|
|
494
|
+
|
|
495
|
+
Deux règles. Les deux sont des blocages, pas des conseils.
|
|
496
|
+
|
|
497
|
+
### Règle 1 — `unref()` sur tout minuteur, sans exception
|
|
498
|
+
|
|
499
|
+
Un minuteur ordinaire **retient la boucle d'événements**. Un processus qui en garde un actif ne sort
|
|
500
|
+
jamais tout seul : les tests d'intégration restent suspendus, un conteneur ignore `SIGTERM`, et
|
|
501
|
+
l'orchestrateur finit par le tuer de force à l'expiration du délai de grâce.
|
|
502
|
+
|
|
503
|
+
La règle est tenue partout dans le code, y compris là où on ne l'attendrait pas — le tick de
|
|
504
|
+
revalidation d'identité du hub, qui ne démarre qu'au premier inscrit, est lui aussi détaché
|
|
505
|
+
(`RealtimeHub.ts:437`), tout comme le report de la sonde cluster (`ClusterProbeClient.ts:170`) et le
|
|
506
|
+
minuteur générique des canaux (`providers.ts:465`).
|
|
507
|
+
|
|
508
|
+
```ts ignore
|
|
509
|
+
const timer = setInterval(tick, 2000);
|
|
510
|
+
timer.unref(); // ⬅ non négociable : le processus doit pouvoir sortir
|
|
511
|
+
return () => clearInterval(timer); // ⬅ et le nettoyage doit annuler ce qu'on a créé
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
Le corollaire vaut autant : **un provider rend toujours une fonction de nettoyage qui annule tout ce
|
|
515
|
+
qu'il a démarré**. Minuteur, écouteur, observateur. Le hub l'appelle au dernier désabonné et à la
|
|
516
|
+
fermeture de la connexion ; s'il ne trouve rien à appeler, le minuteur survit à l'écran qui l'avait
|
|
517
|
+
demandé.
|
|
518
|
+
|
|
519
|
+
### Règle 2 — une sonde LIT, elle ne collecte pas
|
|
520
|
+
|
|
521
|
+
> [!CAUTION]
|
|
522
|
+
> **L'anti-patron : la sonde qui alloue à chaque tick.** Un instantané d'environ 1 Ko, publié à 1 Hz
|
|
523
|
+
> pendant 24 h, ce sont **~84 Mo alloués pour rien** en une journée — et autant de pression sur le
|
|
524
|
+
> ramasse-miettes, donc de la latence en fin de distribution. Multiplie par le nombre de canaux
|
|
525
|
+
> observés.
|
|
526
|
+
|
|
527
|
+
Concrètement, dans le corps d'un tick :
|
|
528
|
+
|
|
529
|
+
- **À faire** : lire des primitives déjà tenues à jour ailleurs, et composer un objet plat.
|
|
530
|
+
- **À éviter** : `.map()`, `.filter()`, `.push()` sur des tableaux vivants ; sérialiser ; interroger
|
|
531
|
+
une base ; parcourir une structure pour produire la mesure.
|
|
532
|
+
- **Jamais** : muter l'état observé. Une sonde qui incrémente ce qu'elle mesure ne mesure plus rien.
|
|
533
|
+
|
|
534
|
+
C'est exactement ce que fait `RealtimeHub.probe()` : une boucle sur les canaux actifs, une boucle sur
|
|
535
|
+
les connexions, des additions d'entiers. Le seul objet alloué est le résultat lui-même, et il ne l'est
|
|
536
|
+
qu'à la demande.
|
|
537
|
+
|
|
538
|
+
**Coût toujours actif de l'observabilité de la socket** : deux incréments d'entiers par frame
|
|
539
|
+
envoyée dans le transport (octets et frames), plus la lecture de `bufferedAmount` déjà nécessaire au
|
|
540
|
+
seuil de contre-pression. Pas de chronométrage, pas de sérialisation supplémentaire. La
|
|
541
|
+
back-pressure étant le risque principal, elle est visible **sans drapeau à activer** — contrairement
|
|
542
|
+
aux sondes qui instrumentent chaque opération et se paient donc à l'usage.
|
|
543
|
+
|
|
544
|
+
## ⚠️ Pièges
|
|
545
|
+
|
|
546
|
+
| Symptôme | Cause | Correction |
|
|
547
|
+
| ---------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
|
|
548
|
+
| Le processus ne s'arrête plus, `Ctrl+C` reste sans effet | Un minuteur de sonde sans `unref()` retient la boucle d'événements | `timer.unref()` sur **tout** minuteur de provider, et une fonction de nettoyage qui l'annule |
|
|
549
|
+
| Le minuteur tourne encore après le départ du dernier abonné | Le provider ne rend pas de fonction de nettoyage, ou elle n'annule pas ce qu'il a démarré | Toujours rendre une fonction qui annule minuteurs **et** écouteurs |
|
|
550
|
+
| Abonnement à un canal de santé, **rien n'arrive**, aucune erreur | Canal inconnu du serveur : le provider rend `null`, l'abonnement est refusé en silence (`nodefony:dashboard` typique) | Vérifier le nom exact ; partager une constante entre les deux bords |
|
|
551
|
+
| `publishTotal` grimpe, `fanoutTotal` stagne | Tu publies dans le vide : personne n'est abonné au canal visé | Comparer le nom publié et le nom souscrit — presque toujours une faute de frappe |
|
|
552
|
+
| Deux appels consécutifs à `/api/health` donnent des chiffres contradictoires | En cluster, la requête tombe sur un worker au hasard et rend une vue per-instance | Lire le champ `cluster` ; si absent, la sonde pod est coupée ou en démarrage à froid |
|
|
553
|
+
| La mémoire du processus grimpe sans fuite apparente | Une sonde alloue à chaque tick, ou un client lent fait grossir la file d'envoi | Lire `backpressure.totalBufferedAmount` ; simplifier le corps du tick |
|
|
554
|
+
| Baisser `slowConsumer.bytes` ne ferme pas les clients lents | Cette clé pilote le **comptage** de la sonde ; les seuils d'action sont des constantes du transport | Rien à régler : ce comportement n'est pas configurable |
|
|
555
|
+
| La cadence demandée n'est pas respectée | Elle a été **ramenée dans les bornes** du canal, silencieusement | Lire les bornes du canal ; vérifier le nom réellement souscrit dans `channels[]` |
|
|
556
|
+
| Deux abonnés au même canal, deux minuteurs | Ils ont demandé des cadences différentes — un canal cadencé est distinct par cadence | Laisser la cadence par défaut (aucun suffixe) quand elle convient : les abonnés partagent un minuteur |
|
|
557
|
+
| Rien n'est visible alors que le trafic est réel | La sonde n'observe **jamais** ce à quoi personne n'est abonné : un canal sans abonné n'existe pas | Ouvrir l'écran ou s'abonner ; c'est le comportement voulu, pas une panne |
|
|
558
|
+
|
|
559
|
+
## 🧪 Tests
|
|
560
|
+
|
|
561
|
+
L'auto-observabilité est couverte par des tests **unitaires déterministes** — elle n'a besoin d'aucune
|
|
562
|
+
infrastructure, puisque la sonde est une lecture pure.
|
|
563
|
+
|
|
564
|
+
<!-- prettier-ignore -->
|
|
565
|
+
| Suite | Ce qui est prouvé |
|
|
566
|
+
| --- | --- |
|
|
567
|
+
| `RealtimeHub.test.ts` | Hub vide → instantané à zéro ; abonnés et publications par canal ; `publishTotal` vs `fanoutTotal` ; comptage des consommateurs lents et `maxBufferedAmount` ; sortie du registre à la déconnexion ; frames entrantes ; descripteur `local` du backplane |
|
|
568
|
+
| `WsConnectionTransport.test.ts` | Les deux seuils de contre-pression : abandon « le dernier gagne », puis fermeture `1013` |
|
|
569
|
+
| `ClusterProbeClient.test.ts` | Fusion pod (sommes vs maximum), report périodique et cache du snapshot, forage `enrich`/`rich`, repli per-instance quand la sonde est absente |
|
|
570
|
+
|
|
571
|
+
**Ce qui manque, et qu'il faut savoir** : il n'existe pas de banc de charge dédié à la sonde
|
|
572
|
+
elle-même. Le coût du chemin chaud (les incréments du transport) est couvert indirectement par les
|
|
573
|
+
suites de charge du module, jamais isolé. Pour mesurer l'effet d'une sonde métier sur ta propre
|
|
574
|
+
application, passe par les outils dédiés — skill `nodefony-load-test` pour la charge et le
|
|
575
|
+
dimensionnement, `nodefony-check-memory-health` pour la mémoire.
|
|
576
|
+
|
|
577
|
+
```bash
|
|
578
|
+
cd src/packages/@nodefony/realtime && npm test
|
|
579
|
+
npm run coverage # rapport vitest — jamais de pourcentage figé dans cette page
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
## 🔗 Pour aller plus loin
|
|
583
|
+
|
|
584
|
+
- ⬆️ **Retour au hub** : [Hub](index.md) · [Toute la documentation](../../../../../docs/index.md)
|
|
585
|
+
- [Architecture](architecture.md) — d'où viennent les compteurs : hub, transport, peer, backplane.
|
|
586
|
+
- [Configuration](configuration.md) — le schéma complet, les surcharges d'environnement, la vue de la configuration effective.
|
|
587
|
+
- [Sécurité](securite.md) — qui a le droit de s'abonner à un canal de santé, et pourquoi ça compte.
|
|
588
|
+
- [Cookbook — un chat](cookbook-chat.md) — le patron appliqué de bout en bout sur une vraie fonctionnalité.
|
|
589
|
+
- [Vocabulaire](vocabulaire.md) — les douze mots de la socket.
|
|
590
|
+
</content>
|
|
591
|
+
|
|
592
|
+
</invoke>
|