@camstack/server 1.1.24 → 1.1.26
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.
|
@@ -1,4 +1,24 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Health endpoints — fast probe surface for monitoring (k8s, uptime,
|
|
4
|
+
* external watchdogs).
|
|
5
|
+
*
|
|
6
|
+
* Routes:
|
|
7
|
+
* - GET /health → hub self-health (in-memory registry ONLY, no RPC)
|
|
8
|
+
* - GET /health/agents → online agent node IDs (in-memory registry ONLY, no RPC)
|
|
9
|
+
* - GET /health/agents/:nodeId → forward to agent's `$agent.health`
|
|
10
|
+
* - GET /health/cluster → hub + every online agent in one shot
|
|
11
|
+
*
|
|
12
|
+
* `/health` and `/health/agents` are served purely from the hub's in-memory
|
|
13
|
+
* Moleculer node registry (`listNodeLiveness`) and NEVER fan out to agent
|
|
14
|
+
* nodes — a frozen/ghost agent cannot slow them. This is what makes `/health`
|
|
15
|
+
* safe as the admin-ui's 4s hub-liveness probe (#26): the old path went
|
|
16
|
+
* through `listNodes()` which RPCs `$agent.status`/`$process.list` at 5s/node.
|
|
17
|
+
* `/health/agents/:nodeId` and `/health/cluster` are the (intentional)
|
|
18
|
+
* forwarding surfaces backed by the `$agent.health` Moleculer action, so
|
|
19
|
+
* monitors talking to the hub and monitors talking directly to an agent
|
|
20
|
+
* (`http://<agent>:4444/health`) see identical payloads.
|
|
21
|
+
*/
|
|
2
22
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
23
|
exports.buildHubHealth = buildHubHealth;
|
|
4
24
|
exports.registerHealthRoutes = registerHealthRoutes;
|
|
@@ -7,9 +27,10 @@ function nowIso() {
|
|
|
7
27
|
return new Date().toISOString();
|
|
8
28
|
}
|
|
9
29
|
async function buildHubHealth(deps, proc = process) {
|
|
10
|
-
|
|
30
|
+
// Fan-out-free: in-memory registry snapshot only, never a per-node RPC (#26).
|
|
31
|
+
const nodes = deps.agentRegistry.listNodeLiveness();
|
|
11
32
|
const remote = nodes.filter((n) => !n.isHub);
|
|
12
|
-
const offlineNodes = remote.filter((n) => n.isOnline
|
|
33
|
+
const offlineNodes = remote.filter((n) => !n.isOnline);
|
|
13
34
|
const total = remote.length;
|
|
14
35
|
const online = total - offlineNodes.length;
|
|
15
36
|
const memUsage = proc.memoryUsage();
|
|
@@ -27,7 +48,7 @@ async function buildHubHealth(deps, proc = process) {
|
|
|
27
48
|
offline: offlineNodes.length,
|
|
28
49
|
// Named so health consumers (admin-ui connection banner) can say
|
|
29
50
|
// WHICH node is degraded, not only how many.
|
|
30
|
-
offlineIds: offlineNodes.map((n) => n.
|
|
51
|
+
offlineIds: offlineNodes.map((n) => n.id),
|
|
31
52
|
},
|
|
32
53
|
cpuPercent: 0,
|
|
33
54
|
memoryPercent,
|
|
@@ -55,10 +76,8 @@ function registerHealthRoutes(fastify, deps) {
|
|
|
55
76
|
return health;
|
|
56
77
|
});
|
|
57
78
|
fastify.get('/health/agents', async () => {
|
|
58
|
-
const nodes =
|
|
59
|
-
return {
|
|
60
|
-
agents: nodes.filter((n) => !n.isHub && n.isOnline !== false).map((n) => n.info.id),
|
|
61
|
-
};
|
|
79
|
+
const nodes = deps.agentRegistry.listNodeLiveness();
|
|
80
|
+
return { agents: nodes.filter((n) => !n.isHub && n.isOnline).map((n) => n.id) };
|
|
62
81
|
});
|
|
63
82
|
fastify.get('/health/agents/:nodeId', async (req, reply) => {
|
|
64
83
|
const { nodeId } = req.params;
|
|
@@ -73,9 +92,10 @@ function registerHealthRoutes(fastify, deps) {
|
|
|
73
92
|
});
|
|
74
93
|
fastify.get('/health/cluster', async () => {
|
|
75
94
|
const hub = await buildHubHealth(deps);
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
const
|
|
95
|
+
// Enumeration only (fan-out-free); the $agent.health fan-out below is intentional.
|
|
96
|
+
const nodes = deps.agentRegistry.listNodeLiveness();
|
|
97
|
+
const remote = nodes.filter((n) => !n.isHub && n.isOnline);
|
|
98
|
+
const agents = await Promise.all(remote.map((n) => fetchAgentHealth(deps, n.id)));
|
|
79
99
|
const ok = hub.ok && agents.every((a) => a.ok);
|
|
80
100
|
return { ok, hub, agents, checkedAt: nowIso() };
|
|
81
101
|
});
|
|
@@ -2337,6 +2337,33 @@ function createCapRouter_deviceManager(getProvider, createRemoteProxy) {
|
|
|
2337
2337
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
2338
2338
|
return p.setDeviceLinks(methodInput);
|
|
2339
2339
|
}),
|
|
2340
|
+
setDisplay: trpc_middleware_js_1.adminProcedure
|
|
2341
|
+
.input(types_38.deviceManagerCapability.methods.setDisplay.input.loose())
|
|
2342
|
+
.output(types_38.deviceManagerCapability.methods.setDisplay.output)
|
|
2343
|
+
.mutation(async ({ input, ctx }) => {
|
|
2344
|
+
const { nodeId, ...methodInput } = input;
|
|
2345
|
+
const p = resolveProvider('device-manager', nodeId, () => getProvider(ctx), createRemoteProxy);
|
|
2346
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
2347
|
+
return p.setDisplay(methodInput);
|
|
2348
|
+
}),
|
|
2349
|
+
getRoleDisplayDefaults: trpc_middleware_js_1.protectedProcedure
|
|
2350
|
+
.input(types_38.deviceManagerCapability.methods.getRoleDisplayDefaults.input.loose())
|
|
2351
|
+
.output(types_38.deviceManagerCapability.methods.getRoleDisplayDefaults.output)
|
|
2352
|
+
.query(async ({ input, ctx }) => {
|
|
2353
|
+
const { nodeId, ...methodInput } = input;
|
|
2354
|
+
const p = resolveProvider('device-manager', nodeId, () => getProvider(ctx), createRemoteProxy);
|
|
2355
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
2356
|
+
return p.getRoleDisplayDefaults(methodInput);
|
|
2357
|
+
}),
|
|
2358
|
+
setRoleDisplayDefaults: trpc_middleware_js_1.adminProcedure
|
|
2359
|
+
.input(types_38.deviceManagerCapability.methods.setRoleDisplayDefaults.input.loose())
|
|
2360
|
+
.output(types_38.deviceManagerCapability.methods.setRoleDisplayDefaults.output)
|
|
2361
|
+
.mutation(async ({ input, ctx }) => {
|
|
2362
|
+
const { nodeId, ...methodInput } = input;
|
|
2363
|
+
const p = resolveProvider('device-manager', nodeId, () => getProvider(ctx), createRemoteProxy);
|
|
2364
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-argument
|
|
2365
|
+
return p.setRoleDisplayDefaults(methodInput);
|
|
2366
|
+
}),
|
|
2340
2367
|
getWireableFields: trpc_middleware_js_1.protectedProcedure
|
|
2341
2368
|
.input(types_38.deviceManagerCapability.methods.getWireableFields.input.loose())
|
|
2342
2369
|
.output(types_38.deviceManagerCapability.methods.getWireableFields.output)
|
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.AgentRegistryService = void 0;
|
|
37
|
+
exports.toNodeLiveness = toNodeLiveness;
|
|
37
38
|
const node_crypto_1 = require("node:crypto");
|
|
38
39
|
const os = __importStar(require("node:os"));
|
|
39
40
|
const system_1 = require("@camstack/system");
|
|
@@ -70,6 +71,16 @@ const AGENT_BOOTSTRAP_PACKAGES = new Set([
|
|
|
70
71
|
'@camstack/system',
|
|
71
72
|
'@camstack/addon-agent-ui',
|
|
72
73
|
]);
|
|
74
|
+
/**
|
|
75
|
+
* Map raw Moleculer registry nodes to the health-surface liveness rows.
|
|
76
|
+
* Child runner ids (`hub/foo`, `agent/bar`) are processes, not cluster
|
|
77
|
+
* nodes — excluded, same rule as `listNodes()` / `classifyNode`.
|
|
78
|
+
*/
|
|
79
|
+
function toNodeLiveness(nodes) {
|
|
80
|
+
return nodes
|
|
81
|
+
.filter((node) => !node.id.includes('/'))
|
|
82
|
+
.map((node) => ({ id: node.id, isHub: node.id === 'hub', isOnline: node.available }));
|
|
83
|
+
}
|
|
73
84
|
class AgentRegistryService {
|
|
74
85
|
eventBus;
|
|
75
86
|
moleculer;
|
|
@@ -232,6 +243,14 @@ class AgentRegistryService {
|
|
|
232
243
|
* match — a single package can ship multiple addons with distinct ids
|
|
233
244
|
* and placements.
|
|
234
245
|
*
|
|
246
|
+
* Version-skew exception: a decl-id the hub has never heard of is NOT
|
|
247
|
+
* stale when the hub already ships that addon's PACKAGE. A newer agent
|
|
248
|
+
* bundle can add a decl-id within a package the hub also ships (e.g. a
|
|
249
|
+
* new `decoder-nodeav` inside `@camstack/addon-pipeline`); undeploying
|
|
250
|
+
* it would delete the whole shared bundle dir on the agent and crash
|
|
251
|
+
* every sibling runner. Only a decl-id whose PACKAGE is absent from the
|
|
252
|
+
* hub is treated as genuinely stale.
|
|
253
|
+
*
|
|
235
254
|
* All errors are caught and logged so a single bad agent never breaks
|
|
236
255
|
* the caller (connect handler or boot pass).
|
|
237
256
|
*/
|
|
@@ -257,14 +276,20 @@ class AgentRegistryService {
|
|
|
257
276
|
if (agentAddons.length === 0)
|
|
258
277
|
return;
|
|
259
278
|
// Build the hub's placement map: decl id → placement. Absence from
|
|
260
|
-
// this map means "not installed on the hub".
|
|
279
|
+
// this map means "not installed on the hub". Also collect the set of
|
|
280
|
+
// PACKAGE names the hub ships — used by the version-skew guard below.
|
|
261
281
|
const hubPlacements = new Map();
|
|
282
|
+
const hubPackages = new Set();
|
|
262
283
|
for (const row of this.addonRegistry.listAddons()) {
|
|
263
284
|
const declId = row.manifest.id;
|
|
264
285
|
if (typeof declId !== 'string')
|
|
265
286
|
continue;
|
|
266
287
|
const decl = row.declaration ?? row.manifest;
|
|
267
288
|
hubPlacements.set(declId, (0, types_1.resolveAddonPlacement)(decl));
|
|
289
|
+
const packageName = row.manifest.packageName;
|
|
290
|
+
if (typeof packageName === 'string' && packageName.length > 0) {
|
|
291
|
+
hubPackages.add(packageName);
|
|
292
|
+
}
|
|
268
293
|
}
|
|
269
294
|
const stale = agentAddons.filter((addon) => {
|
|
270
295
|
// Agent bootstrap infrastructure (storage/settings/metrics/logging from
|
|
@@ -278,9 +303,16 @@ class AgentRegistryService {
|
|
|
278
303
|
return false;
|
|
279
304
|
}
|
|
280
305
|
const placement = hubPlacements.get(addon.id);
|
|
281
|
-
|
|
282
|
-
|
|
306
|
+
if (placement === undefined) {
|
|
307
|
+
// decl-id unknown to the hub. If the hub ships this PACKAGE, this is a
|
|
308
|
+
// version-skew sibling (a newer agent bundle added a decl-id within a
|
|
309
|
+
// package the hub already ships) — undeploying it would delete the whole
|
|
310
|
+
// shared bundle on the agent. Only undeploy when the PACKAGE itself is
|
|
311
|
+
// absent from the hub.
|
|
312
|
+
if (addon.packageName !== undefined && hubPackages.has(addon.packageName))
|
|
313
|
+
return false;
|
|
283
314
|
return true;
|
|
315
|
+
}
|
|
284
316
|
// Installed but pinned to the hub → must not run on an agent.
|
|
285
317
|
return placement === 'hub-only';
|
|
286
318
|
});
|
|
@@ -339,6 +371,19 @@ class AgentRegistryService {
|
|
|
339
371
|
updateAgentName(nodeId, name) {
|
|
340
372
|
console.log(`[agent-registry] Agent renamed: "${nodeId}" → "${name}"`);
|
|
341
373
|
}
|
|
374
|
+
/**
|
|
375
|
+
* Fan-out-free liveness snapshot for the `/health` surface. Reads ONLY
|
|
376
|
+
* the hub's in-memory Moleculer node registry — never a per-node RPC —
|
|
377
|
+
* so a frozen/ghost agent can never slow the hub-liveness probe.
|
|
378
|
+
* (`listNodes()` fans `$agent.status`/`$process.list` out at 5s/node;
|
|
379
|
+
* the admin-ui probe aborts `/health` at 4s, which turned one frozen
|
|
380
|
+
* node into a false RED "Server unreachable" banner — #26.)
|
|
381
|
+
* `onlyAvailable: false` keeps recently-offline nodes listed with
|
|
382
|
+
* `available:false`, which is what populates `agents.offlineIds`.
|
|
383
|
+
*/
|
|
384
|
+
listNodeLiveness() {
|
|
385
|
+
return toNodeLiveness(this.moleculer.broker.registry.getNodeList({ onlyAvailable: false }));
|
|
386
|
+
}
|
|
342
387
|
async listNodes() {
|
|
343
388
|
// Get child processes for hub via $process.list
|
|
344
389
|
let hubProcesses = [];
|