@lensmcp/cluster 1.18.0 → 1.18.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/executors/gateway/runtime/lens-children.d.ts +6 -0
- package/executors/gateway/runtime/lens-children.d.ts.map +1 -1
- package/executors/gateway/runtime/lens-children.js +50 -20
- package/executors/gateway/runtime/lifecycle.d.ts +7 -1
- package/executors/gateway/runtime/lifecycle.d.ts.map +1 -1
- package/executors/gateway/runtime/lifecycle.js +50 -11
- package/executors/gateway/runtime/proxy.d.ts.map +1 -1
- package/executors/gateway/runtime/proxy.js +9 -0
- package/package.json +3 -3
|
@@ -44,6 +44,12 @@ export interface LensChildren {
|
|
|
44
44
|
* everything"): spawn each app's vite (+ capture) in the workspace's OWN root + event bus, on a
|
|
45
45
|
* collision-safe port, and point its route target at that port. Returns a reaper for unregister. */
|
|
46
46
|
hostWorkspaceApps(fragment: WorkspaceFragment): Promise<() => void>;
|
|
47
|
+
/** ONE liveness round over every tracked lens-frontend vite: TCP-probe its pinned port, emit the
|
|
48
|
+
* RECOVERY edge (`service-up`) the first time it accepts, and — past the spawn grace and with the
|
|
49
|
+
* probe enabled — the wedge verdict + SIGKILL. The sweeper calls this on a timer; it is exposed
|
|
50
|
+
* (like `ServiceLayer.scanNow`) so the transitions are testable without a 15-second interval.
|
|
51
|
+
* Concurrency-guarded: a call while a round is in flight returns immediately. */
|
|
52
|
+
probeLensFrontends(): Promise<void>;
|
|
47
53
|
/** Reap all managed children (called from `stop()`): SIGTERM, a grace window, then SIGKILL any survivor
|
|
48
54
|
* (vite ignores SIGTERM → would orphan when the daemon exits). Async so `stop()` awaits the escalation. */
|
|
49
55
|
reapAll(): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lens-children.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/lens-children.ts"],"names":[],"mappings":"AASA,OAAO,EAIL,KAAK,UAAU,EAChB,MAAM,kCAAkC,CAAC;AAI1C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,OAAO,KAAK,EAAE,cAAc,EAAE,qBAAqB,EAAuB,MAAM,SAAS,CAAC;AAiB1F,OAAO,EAA0B,KAAK,aAAa,EAAE,MAAM,iBAAiB,CAAC;AA0B7E,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAalE;AAyBD;;;;;;mDAMmD;AACnD,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,SAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,CAG1F;AAMD;;;;;;;;;GASG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,IAAI,GAAE;IAAE,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAO,GAClF,OAAO,CAAC,OAAO,CAAC,CAUlB;AAED;;;iFAGiF;AACjF,wBAAgB,WAAW,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAQvE;AAED,MAAM,WAAW,YAAY;IAC3B,iBAAiB,EAAE,UAAU,CAAC;IAC9B;;gFAE4E;IAC5E,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvC;;6GAEyG;IACzG,sBAAsB,CAAC,QAAQ,EAAE,iBAAiB,GAAG,OAAO,CAAC,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAC,CAAC;IACvF;;yGAEqG;IACrG,iBAAiB,CAAC,QAAQ,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,IAAI,CAAC,CAAC;IACpE;gHAC4G;IAC5G,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED,wBAAgB,kBAAkB,CAChC,EAAE,EAAE,cAAc,EAClB,GAAG,EAAE,aAAa,EAClB,OAAO,EAAE,qBAAqB,EAE9B,IAAI,CAAC,EAAE;IAAE,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;CAAE,GACzD,YAAY,
|
|
1
|
+
{"version":3,"file":"lens-children.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/lens-children.ts"],"names":[],"mappings":"AASA,OAAO,EAIL,KAAK,UAAU,EAChB,MAAM,kCAAkC,CAAC;AAI1C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE1D,OAAO,KAAK,EAAE,cAAc,EAAE,qBAAqB,EAAuB,MAAM,SAAS,CAAC;AAiB1F,OAAO,EAA0B,KAAK,aAAa,EAAE,MAAM,iBAAiB,CAAC;AA0B7E,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAalE;AAyBD;;;;;;mDAMmD;AACnD,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,SAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,CAG1F;AAMD;;;;;;;;;GASG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,IAAI,GAAE;IAAE,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAO,GAClF,OAAO,CAAC,OAAO,CAAC,CAUlB;AAED;;;iFAGiF;AACjF,wBAAgB,WAAW,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAQvE;AAED,MAAM,WAAW,YAAY;IAC3B,iBAAiB,EAAE,UAAU,CAAC;IAC9B;;gFAE4E;IAC5E,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvC;;6GAEyG;IACzG,sBAAsB,CAAC,QAAQ,EAAE,iBAAiB,GAAG,OAAO,CAAC,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAC,CAAC;IACvF;;yGAEqG;IACrG,iBAAiB,CAAC,QAAQ,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,IAAI,CAAC,CAAC;IACpE;;;;sFAIkF;IAClF,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC;gHAC4G;IAC5G,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED,wBAAgB,kBAAkB,CAChC,EAAE,EAAE,cAAc,EAClB,GAAG,EAAE,aAAa,EAClB,OAAO,EAAE,qBAAqB,EAE9B,IAAI,CAAC,EAAE;IAAE,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;CAAE,GACzD,YAAY,CAwfd"}
|
|
@@ -145,11 +145,6 @@ deps) {
|
|
|
145
145
|
// `portConflicts` counts CONSECUTIVE lost bind races (EADDRINUSE) — forgiven rather than charged to
|
|
146
146
|
// `attempts`, but capped so a permanently-squatted port still falls back to backoff.
|
|
147
147
|
const lensHeal = new Map();
|
|
148
|
-
// Zombie recycle + LIVENESS probe: the live `lens:true` app VITE children (label 'vite') + when each
|
|
149
|
-
// spawned, its pinned `port`, and its run of consecutive probe failures. `port`/`probeFails` drive the
|
|
150
|
-
// wedge probe (alive but NOT listening); `spawnedAt` also drives the stale-source-set recycle. Both reset
|
|
151
|
-
// naturally on a respawn, which registers a FRESH handle. The capture sidecar + the dashboard/MCP
|
|
152
|
-
// singletons are NOT tracked (they don't serve app source).
|
|
153
148
|
const viteChildren = new Map();
|
|
154
149
|
// Per-project last-recycle time — the cooldown survives the kill→auto-heal→respawn (a new child handle).
|
|
155
150
|
const viteRecycleAt = new Map();
|
|
@@ -210,8 +205,20 @@ deps) {
|
|
|
210
205
|
// The port THIS child owns (pinned `--port <n> --strictPort`): what the liveness probe checks and what
|
|
211
206
|
// the respawn must wait to be free. `undefined` for the dashboard/MCP singletons + an unpinned vite.
|
|
212
207
|
const ownPort = isVite ? viteArgPort(args) : undefined;
|
|
213
|
-
if (isVite)
|
|
214
|
-
|
|
208
|
+
if (isVite) {
|
|
209
|
+
// `spawnLensFrontend` always stamps the OWNING workspace's bus into the child env, so this is
|
|
210
|
+
// the authoritative "whose app is this" — a registered workspace's lifecycle events go to its
|
|
211
|
+
// own `.lensmcp/events.jsonl`, exactly as the service layer's `emitSvc` routes pod events.
|
|
212
|
+
const childBus = typeof env?.['LENSMCP_EVENT_FILE'] === 'string' ? env['LENSMCP_EVENT_FILE'] : undefined;
|
|
213
|
+
viteChildren.set(child, {
|
|
214
|
+
project: env?.['LENSMCP_PROJECT'] ?? 'app',
|
|
215
|
+
spawnedAt,
|
|
216
|
+
port: ownPort,
|
|
217
|
+
probeFails: 0,
|
|
218
|
+
listening: false,
|
|
219
|
+
...(childBus && childBus !== rt.eventFile ? { bus: childBus } : {}),
|
|
220
|
+
});
|
|
221
|
+
}
|
|
215
222
|
child.on('exit', (code, sig) => {
|
|
216
223
|
const idx = lensChildren.indexOf(child);
|
|
217
224
|
if (idx >= 0)
|
|
@@ -269,7 +276,7 @@ deps) {
|
|
|
269
276
|
// 502ing forever. The kill lands on the proven exit→auto-heal path, which respawns after waiting the port
|
|
270
277
|
// out. `probeInFlight` keeps a slow round from overlapping the next tick.
|
|
271
278
|
let probeInFlight = false;
|
|
272
|
-
const probeLensChildren = async (cfg) => {
|
|
279
|
+
const probeLensChildren = async (cfg, killWedged) => {
|
|
273
280
|
if (probeInFlight)
|
|
274
281
|
return;
|
|
275
282
|
probeInFlight = true;
|
|
@@ -281,22 +288,39 @@ deps) {
|
|
|
281
288
|
continue; // unpinned port ⇒ nothing to probe
|
|
282
289
|
if (child.exitCode !== null || child.signalCode !== null)
|
|
283
290
|
continue; // already dying — the exit heal owns it
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
291
|
+
// The probe now runs DURING the spawn grace too. It costs one extra TCP connect per child
|
|
292
|
+
// for the first `graceMs`, and it buys the RECOVERY edge below at first-listen instead of
|
|
293
|
+
// 45s later — the grace exists to withhold the KILL verdict from a booting vite, not to
|
|
294
|
+
// withhold the observation that it came up.
|
|
288
295
|
if (await probePortListening(meta.port, types_1.LENS_PROBE_TIMEOUT_MS)) {
|
|
289
296
|
meta.probeFails = 0;
|
|
297
|
+
// RECOVERY EDGE. A wedged/zombie-recycled vite is SIGKILLed and auto-healed by the exit
|
|
298
|
+
// handler, which announces nothing — so `pod-recycle` (→ `wedged` in the lens fold) had no
|
|
299
|
+
// counterpart and a lens app stayed "wedged" for the rest of the session while serving
|
|
300
|
+
// every request (foodguard 2026-07-31: `wedged: ["authentication","dashboard"]`, `up: []`,
|
|
301
|
+
// zero 502s). An accepting listener is the same evidence the wedge verdict is the absence
|
|
302
|
+
// of, so it is what clears it. Once per generation: a fresh child starts `listening: false`.
|
|
303
|
+
if (!meta.listening) {
|
|
304
|
+
meta.listening = true;
|
|
305
|
+
emit('info', `lens app up: ${meta.project} (vite :${meta.port} listening)`, `cluster-lens-app:${meta.project}`, { kind: 'service-up', project: meta.project, port: meta.port }, undefined, meta.bus);
|
|
306
|
+
}
|
|
290
307
|
continue;
|
|
291
308
|
}
|
|
309
|
+
if (Date.now() - meta.spawnedAt < cfg.graceMs) {
|
|
310
|
+
meta.probeFails = 0;
|
|
311
|
+
continue;
|
|
312
|
+
} // still booting
|
|
313
|
+
if (!killWedged)
|
|
314
|
+
continue; // liveness still OBSERVED above; only the kill verdict is switched off
|
|
292
315
|
meta.probeFails += 1;
|
|
293
316
|
const reason = (0, lifecycle_1.lensChildWedged)(meta, viteRecycleAt.get(meta.project) ?? 0, Date.now(), cfg);
|
|
294
317
|
if (!reason)
|
|
295
318
|
continue; // failing, but not yet N-in-a-row / still in cooldown
|
|
296
319
|
viteRecycleAt.set(meta.project, Date.now());
|
|
297
320
|
meta.probeFails = 0;
|
|
321
|
+
meta.listening = false; // re-arm the recovery edge, in case the SIGKILL below doesn't take
|
|
298
322
|
console.error(`[gateway] wedge recycle: lens app ${meta.project} is ALIVE but NOT LISTENING on :${meta.port} after ${Math.max(2, cfg.failureThreshold)} probes — SIGKILL; a fresh vite is coming.`);
|
|
299
|
-
emit('warning', `wedge recycle: ${meta.project} (${reason})`, `cluster-lens-app:${meta.project}`, { kind: 'pod-recycle', project: meta.project, reason, port: meta.port });
|
|
323
|
+
emit('warning', `wedge recycle: ${meta.project} (${reason})`, `cluster-lens-app:${meta.project}`, { kind: 'pod-recycle', project: meta.project, reason, port: meta.port }, undefined, meta.bus);
|
|
300
324
|
try {
|
|
301
325
|
child.kill('SIGKILL');
|
|
302
326
|
}
|
|
@@ -321,14 +345,15 @@ deps) {
|
|
|
321
345
|
if (lensSweeper || (!types_1.POD_RECYCLE_ENABLED && !types_1.LENS_PROBE_ENABLED))
|
|
322
346
|
return;
|
|
323
347
|
const cfg = { graceMs: types_1.POD_STALE_GRACE_MS, settleMs: types_1.POD_RECYCLE_SETTLE_MS, cooldownMs: types_1.POD_STALE_RECYCLE_COOLDOWN_MS };
|
|
324
|
-
const probeCfg = { graceMs: types_1.LENS_PROBE_GRACE_MS, failureThreshold: types_1.LENS_PROBE_FAILS, cooldownMs: types_1.LENS_PROBE_COOLDOWN_MS };
|
|
325
348
|
lensSweeper = setInterval(() => {
|
|
326
349
|
if (rt.stopped() || viteChildren.size === 0)
|
|
327
350
|
return;
|
|
328
351
|
// The liveness probe is INDEPENDENT of the source-set clock — a wedge has nothing to do with the file
|
|
329
|
-
// set, so it must run before (and regardless of) the staleness early-return below.
|
|
330
|
-
|
|
331
|
-
|
|
352
|
+
// set, so it must run before (and regardless of) the staleness early-return below. It runs whenever
|
|
353
|
+
// the sweeper does: `LENS_PROBE_ENABLED` gates the KILL verdict, NOT the observation, because the
|
|
354
|
+
// probe is also the only RECOVERY signal a lens frontend has — with the staleness recycle on and the
|
|
355
|
+
// probe off, a zombie-recycled app would otherwise stay `wedged` in the fold forever.
|
|
356
|
+
void probeLensFrontends();
|
|
332
357
|
if (!types_1.POD_RECYCLE_ENABLED)
|
|
333
358
|
return;
|
|
334
359
|
const wideChangedAt = rt.sourceSetChangedAt ?? 0;
|
|
@@ -343,8 +368,9 @@ deps) {
|
|
|
343
368
|
if (!(0, lifecycle_1.lensFrontendStale)(meta.spawnedAt, last, changedAt, now, cfg))
|
|
344
369
|
continue;
|
|
345
370
|
viteRecycleAt.set(meta.project, now);
|
|
371
|
+
meta.listening = false; // re-arm the recovery edge — the respawned vite must announce itself
|
|
346
372
|
console.log(`[gateway] zombie recycle: lens app ${meta.project} (stale-source-set) — vite missed a file add; restarting it`);
|
|
347
|
-
emit('warning', `zombie recycle: ${meta.project} (stale-source-set)`, `cluster-lens-app:${meta.project}`, { kind: 'pod-recycle', project: meta.project, reason: 'stale-source-set' });
|
|
373
|
+
emit('warning', `zombie recycle: ${meta.project} (stale-source-set)`, `cluster-lens-app:${meta.project}`, { kind: 'pod-recycle', project: meta.project, reason: 'stale-source-set' }, undefined, meta.bus);
|
|
348
374
|
try {
|
|
349
375
|
child.kill('SIGTERM');
|
|
350
376
|
}
|
|
@@ -569,7 +595,10 @@ deps) {
|
|
|
569
595
|
labelPrefix,
|
|
570
596
|
log: (l) => console.log(l),
|
|
571
597
|
}, spawnInRoot);
|
|
572
|
-
|
|
598
|
+
// Onto the REGISTERED workspace's OWN bus (like the service layer's `emitSvc`): its cluster
|
|
599
|
+
// view must show its apps, and the daemon's must not — two workspaces can both own a
|
|
600
|
+
// project called `dashboard`, so a shared bus makes the fold ambiguous as well as wrong.
|
|
601
|
+
emit('info', `lens app started: ${fragment.wsKey}/${project} (vite :${port})`, `cluster-lens-app:${fragment.wsKey}:${project}`, { kind: 'lens-app-up', project, port }, undefined, eventFile);
|
|
573
602
|
}
|
|
574
603
|
catch (e) {
|
|
575
604
|
console.error(`[gateway] lens app ${fragment.wsKey}/${project}: ${e.message}`);
|
|
@@ -617,5 +646,6 @@ deps) {
|
|
|
617
646
|
}
|
|
618
647
|
}
|
|
619
648
|
};
|
|
620
|
-
|
|
649
|
+
const probeLensFrontends = () => probeLensChildren({ graceMs: types_1.LENS_PROBE_GRACE_MS, failureThreshold: types_1.LENS_PROBE_FAILS, cooldownMs: types_1.LENS_PROBE_COOLDOWN_MS }, types_1.LENS_PROBE_ENABLED);
|
|
650
|
+
return { spawnManagedChild, bootSingletonsAndApps, hostWorkspaceDashboard, hostWorkspaceApps, probeLensFrontends, reapAll };
|
|
621
651
|
}
|
|
@@ -116,7 +116,13 @@ export interface ServiceLayer {
|
|
|
116
116
|
spawnService(svc: ServiceCtl): void;
|
|
117
117
|
ensureUp(svc: ServiceCtl): Promise<boolean>;
|
|
118
118
|
scaleUp(svc: ServiceCtl): void;
|
|
119
|
-
killSpawned(svc: ServiceCtl, why: string
|
|
119
|
+
killSpawned(svc: ServiceCtl, why: string, opts?: {
|
|
120
|
+
announce?: boolean;
|
|
121
|
+
}): void;
|
|
122
|
+
/** The RECOVERY edge: record + announce that this service is serving again.
|
|
123
|
+
* Idempotent (a no-op once `state === 'up'`), so the hot path can call it per
|
|
124
|
+
* request. Returns true only on the transition. See the implementation. */
|
|
125
|
+
markUp(svc: ServiceCtl, how: 'adopted' | 'observed'): boolean;
|
|
120
126
|
/** Start the idle sweeper; returns the interval (already `.unref()`'d). */
|
|
121
127
|
startSweeper(): NodeJS.Timeout;
|
|
122
128
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/lifecycle.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/lifecycle.ts"],"names":[],"mappings":"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"}
|
|
@@ -452,7 +452,7 @@ function createServiceLayer(rt, obs) {
|
|
|
452
452
|
setTimeout(() => {
|
|
453
453
|
if (rt.stopped() || !svc.lockBlockedAt)
|
|
454
454
|
return;
|
|
455
|
-
killSpawned(svc, 'parked on a foreign nx task lock — recycling');
|
|
455
|
+
killSpawned(svc, 'parked on a foreign nx task lock — recycling', { announce: false }); // a recycle: `nx-lock-break` above is the report
|
|
456
456
|
reapForeignRunners(svc, target);
|
|
457
457
|
setTimeout(() => {
|
|
458
458
|
if (!rt.stopped() && !svc.proc && svc.state === 'down')
|
|
@@ -498,12 +498,34 @@ function createServiceLayer(rt, obs) {
|
|
|
498
498
|
});
|
|
499
499
|
svc.proc = proc;
|
|
500
500
|
};
|
|
501
|
+
/**
|
|
502
|
+
* The RECOVERY edge — the counterpart to `service-down`/`service-recycle`/`pod-recycle`.
|
|
503
|
+
*
|
|
504
|
+
* The gateway used to announce a service going UP from exactly two places, both inside
|
|
505
|
+
* `ensureUp`: the adopt fast-path and the cold-start wait loop. But the proxy only calls
|
|
506
|
+
* `ensureUp` when `pickSock` came back EMPTY — so a service that self-healed on its own (the
|
|
507
|
+
* exit auto-heal respawn, a recycled pod re-binding, an externally-started devserver) was never
|
|
508
|
+
* announced. Its `svc.state` stayed `'starting'` and the lens fold behind `process://cluster`
|
|
509
|
+
* latched on the last DEGRADATION forever: `agent://current-status` reported `failing` while the
|
|
510
|
+
* app served every request (foodguard 2026-07-31 — four services `starting`, `up: []`, 20×200,
|
|
511
|
+
* 0×502).
|
|
512
|
+
*
|
|
513
|
+
* So the transition lives here, callable from every place that observes a live pod, and the
|
|
514
|
+
* evidence is the SAME socket scan the wedge detector uses (`live = scanNow(pool) > 0`) — the
|
|
515
|
+
* detector that clears `wedgedSince` must also be the one that records `up`, or the two disagree.
|
|
516
|
+
* Idempotent, so the per-request call costs one property compare.
|
|
517
|
+
*/
|
|
518
|
+
const markUp = (svc, how) => {
|
|
519
|
+
if (svc.state === 'up')
|
|
520
|
+
return false;
|
|
521
|
+
svc.state = 'up';
|
|
522
|
+
const pods = svc.pool.socks.length;
|
|
523
|
+
emitSvc(svc, 'info', `service up: ${svc.project} (${pods} pods, ${how})`, `cluster-service:${svc.project}`, { kind: 'service-up', project: svc.project, pods, adopted: how === 'adopted' });
|
|
524
|
+
return true;
|
|
525
|
+
};
|
|
501
526
|
const ensureUp = async (svc) => {
|
|
502
527
|
if (scanNow(svc.pool) > 0) {
|
|
503
|
-
|
|
504
|
-
emitSvc(svc, 'info', `service up: ${svc.project} (${svc.pool.socks.length} pods, adopted)`, `cluster-service:${svc.project}`, { kind: 'service-up', project: svc.project, pods: svc.pool.socks.length, adopted: true });
|
|
505
|
-
}
|
|
506
|
-
svc.state = 'up';
|
|
528
|
+
markUp(svc, 'adopted');
|
|
507
529
|
return true;
|
|
508
530
|
}
|
|
509
531
|
if (rt.stopped())
|
|
@@ -549,11 +571,21 @@ function createServiceLayer(rt, obs) {
|
|
|
549
571
|
emitSvc(svc, 'info', `autoscale: ${svc.project} ${cur} → ${cur + 1} pods`, `cluster-service:${svc.project}`, { kind: 'autoscale', project: svc.project, from: cur, to: cur + 1, inflight: svc.inflight });
|
|
550
572
|
};
|
|
551
573
|
// Idle sweeper: kill gateway-spawned lazy services after idleKillSec.
|
|
552
|
-
|
|
574
|
+
//
|
|
575
|
+
// `announce: false` for a caller that is TEARING THE TREE DOWN AS PART OF A RECYCLE. This function
|
|
576
|
+
// is the shared teardown for two different intents — "park this service, it's idle" and "kill this
|
|
577
|
+
// service, it's broken" — and the `idle-kill` event only describes the first. A recycle caller has
|
|
578
|
+
// ALREADY announced the real reason (`service-recycle` / `pod-recycle` / `service-crash-loop`), and
|
|
579
|
+
// an `idle-kill` on top would OVERWRITE that fault in the lens fold with a benign `idle` — the
|
|
580
|
+
// crash-loop and wedge reports would vanish milliseconds after they were raised. It also read wrong
|
|
581
|
+
// in the log ("idle kill: auth (wedged: 0 live pods — recycling)").
|
|
582
|
+
const killSpawned = (svc, why, opts) => {
|
|
553
583
|
if (!svc.proc)
|
|
554
584
|
return;
|
|
555
585
|
console.log(`[gateway] ${svc.project} ${why} — scaling to zero.`);
|
|
556
|
-
|
|
586
|
+
if (opts?.announce !== false) {
|
|
587
|
+
emitSvc(svc, 'info', `idle kill: ${svc.project} (${why})`, `cluster-service:${svc.project}`, { kind: 'idle-kill', project: svc.project, why });
|
|
588
|
+
}
|
|
557
589
|
const dying = svc.proc;
|
|
558
590
|
// Detach the corpse's handlers BEFORE killing: a dying webpack/nx flushes
|
|
559
591
|
// buffered stdout/stderr as it exits, and those `sniff` callbacks would
|
|
@@ -623,13 +655,13 @@ function createServiceLayer(rt, obs) {
|
|
|
623
655
|
svc.restarts = (svc.restarts ?? []).filter((t) => now - t < types_1.SERVICE_RESTART_WINDOW_MS);
|
|
624
656
|
if (svc.restarts.length >= types_1.SERVICE_MAX_RESTARTS) {
|
|
625
657
|
emitSvc(svc, 'error', `service crash-looping: ${svc.project} — wedged ${types_1.SERVICE_MAX_RESTARTS}+×/min, not auto-recycling (the next request retries)`, `cluster-service:${svc.project}`, { kind: 'service-crash-loop', project: svc.project });
|
|
626
|
-
killSpawned(svc, 'wedged (crash-loop cap) — leaving down for the next request'); // svc.proc cleared → scale-from-zero can retry
|
|
658
|
+
killSpawned(svc, 'wedged (crash-loop cap) — leaving down for the next request', { announce: false }); // svc.proc cleared → scale-from-zero can retry
|
|
627
659
|
return;
|
|
628
660
|
}
|
|
629
661
|
svc.restarts.push(now);
|
|
630
662
|
console.log(`[gateway] self-heal: recycling WEDGED ${svc.project} (live parent, 0 live pods; ${svc.restarts.length}/${types_1.SERVICE_MAX_RESTARTS})`);
|
|
631
663
|
emitSvc(svc, 'warning', `self-heal: recycling wedged ${svc.project} (0 live pods)`, `cluster-service:${svc.project}`, { kind: 'service-recycle', project: svc.project, attempt: svc.restarts.length });
|
|
632
|
-
killSpawned(svc, 'wedged: 0 live pods — recycling'); // kills the tree + unlinks stale socks (+1200ms) + svc.proc=undefined + state='down'
|
|
664
|
+
killSpawned(svc, 'wedged: 0 live pods — recycling', { announce: false }); // kills the tree + unlinks stale socks (+1200ms) + svc.proc=undefined + state='down'
|
|
633
665
|
// A wedge is unexpected — bring it straight back (don't wait for a request). Respawn AFTER killSpawned's
|
|
634
666
|
// deferred sock cleanup (1200ms) so the fresh child binds a clean socket path.
|
|
635
667
|
setTimeout(() => {
|
|
@@ -748,7 +780,7 @@ function createServiceLayer(rt, obs) {
|
|
|
748
780
|
console.log(`[gateway] zombie recycle: ${svc.project} (${reason}) — stale fs view; fresh pod incoming`);
|
|
749
781
|
emitSvc(svc, 'warning', `zombie recycle: ${svc.project} (${reason})`, `cluster-service:${svc.project}`, { kind: 'pod-recycle', project: svc.project, reason });
|
|
750
782
|
const eager = !!svc.decl.eager;
|
|
751
|
-
killSpawned(svc, `${reason} — recycling stale pod
|
|
783
|
+
killSpawned(svc, `${reason} — recycling stale pod`, { announce: false }); // clears proc, state='down', unlinks socks (+1200ms)
|
|
752
784
|
if (eager)
|
|
753
785
|
setTimeout(() => { if (!rt.stopped() && !svc.proc && svc.state === 'down')
|
|
754
786
|
spawnService(svc); }, 1500).unref?.();
|
|
@@ -776,6 +808,13 @@ function createServiceLayer(rt, obs) {
|
|
|
776
808
|
const live = scanNow(svc.pool) > 0;
|
|
777
809
|
if (live || coldStarting) {
|
|
778
810
|
svc.wedgedSince = undefined;
|
|
811
|
+
// RECOVERY EDGE. `live` is the exact evidence that just cleared `wedgedSince`, so it must
|
|
812
|
+
// also record the service as UP — otherwise the gateway silently forgives a wedge while
|
|
813
|
+
// the lens fold stays latched on it. This is the path that heals a service which came
|
|
814
|
+
// back WITHOUT a request (the exit auto-heal respawn), since nothing else calls `ensureUp`
|
|
815
|
+
// once a pod socket exists. Idempotent: after the first tick it is a property compare.
|
|
816
|
+
if (live)
|
|
817
|
+
markUp(svc, 'observed');
|
|
779
818
|
// ZOMBIE recycle: a live pod (live parent + live socket, answers requests) whose fs view went
|
|
780
819
|
// stale — neither exit-respawn nor wedge-recycle sees it. Idle-gated in podRecycleReason so a busy
|
|
781
820
|
// pod is never interrupted.
|
|
@@ -799,5 +838,5 @@ function createServiceLayer(rt, obs) {
|
|
|
799
838
|
sweeper.unref();
|
|
800
839
|
return sweeper;
|
|
801
840
|
};
|
|
802
|
-
return { scanNow, spawnService, ensureUp, scaleUp, killSpawned, startSweeper };
|
|
841
|
+
return { scanNow, spawnService, ensureUp, scaleUp, killSpawned, markUp, startSweeper };
|
|
803
842
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/proxy.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC,OAAO,KAAK,KAAK,MAAM,MAAM,aAAa,CAAC;AAG3C,OAAO,KAAK,EAAE,cAAc,EAAW,KAAK,EAAE,MAAM,SAAS,CAAC;AAE9D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AA8BhD,MAAM,WAAW,WAAW;IAC1B,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,GAAG,IAAI,CAAC;IACxH,EAAE,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,KAAK,IAAI,GAAG,IAAI,CAAC;IACnI,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC,QAAQ,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,KAAK,IAAI,GAAG,IAAI,CAAC;IAC/H;kGAC8F;IAC9F,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC,QAAQ,EAAE,IAAI,CAAC,aAAa,EAAE,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,KAAK,IAAI,GAAG,IAAI,CAAC;IAC7H,KAAK,CAAC,IAAI,IAAI,CAAC;CAChB;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,WAAW,CAAC;IACnB,OAAO,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1F,cAAc,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC,KAAK,GAAG,OAAO,YAAY,EAAE,KAAK,CAAC;IACzE,aAAa,IAAI,IAAI,CAAC;IACtB,UAAU,IAAI,IAAI,CAAC;CACpB;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,cAAc,EAAE,GAAG,EAAE,aAAa,EAAE,QAAQ,EAAE,YAAY,GAAG,UAAU,
|
|
1
|
+
{"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/proxy.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC,OAAO,KAAK,KAAK,MAAM,MAAM,aAAa,CAAC;AAG3C,OAAO,KAAK,EAAE,cAAc,EAAW,KAAK,EAAE,MAAM,SAAS,CAAC;AAE9D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AACrD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AA8BhD,MAAM,WAAW,WAAW;IAC1B,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,GAAG,IAAI,CAAC;IACxH,EAAE,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,KAAK,IAAI,GAAG,IAAI,CAAC;IACnI,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC,QAAQ,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,KAAK,IAAI,GAAG,IAAI,CAAC;IAC/H;kGAC8F;IAC9F,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,EAAE,CAAC,QAAQ,EAAE,IAAI,CAAC,aAAa,EAAE,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,KAAK,IAAI,GAAG,IAAI,CAAC;IAC7H,KAAK,CAAC,IAAI,IAAI,CAAC;CAChB;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,WAAW,CAAC;IACnB,OAAO,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,GAAG,EAAE,IAAI,CAAC,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1F,cAAc,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC,KAAK,GAAG,OAAO,YAAY,EAAE,KAAK,CAAC;IACzE,aAAa,IAAI,IAAI,CAAC;IACtB,UAAU,IAAI,IAAI,CAAC;CACpB;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,cAAc,EAAE,GAAG,EAAE,aAAa,EAAE,QAAQ,EAAE,YAAY,GAAG,UAAU,CA8YtG"}
|
|
@@ -256,6 +256,12 @@ function createProxy(rt, obs, svcLayer) {
|
|
|
256
256
|
if (await svcLayer.ensureUp(ctl))
|
|
257
257
|
sock = (0, discovery_1.pickSock)(route.pool, rt.scanTtlMs);
|
|
258
258
|
}
|
|
259
|
+
// A ROUTABLE pod is the recovery edge (see `markUp` in lifecycle.ts): `ensureUp` — which used
|
|
260
|
+
// to be the only announcer of a service going up — is SKIPPED whenever `pickSock` already
|
|
261
|
+
// found a socket, so a service that self-healed between requests was never announced and the
|
|
262
|
+
// lens fold latched on its last degradation. Idempotent ⇒ a property compare per request.
|
|
263
|
+
if (ctl && sock)
|
|
264
|
+
svcLayer.markUp(ctl, 'observed');
|
|
259
265
|
if (ctl)
|
|
260
266
|
svcLayer.scaleUp(ctl);
|
|
261
267
|
if (!sock) {
|
|
@@ -358,6 +364,9 @@ function createProxy(rt, obs, svcLayer) {
|
|
|
358
364
|
if (await svcLayer.ensureUp(ctl))
|
|
359
365
|
sock = (0, discovery_1.pickSock)(route.pool, rt.scanTtlMs);
|
|
360
366
|
}
|
|
367
|
+
// The recovery edge — see the twin comment in `forwardNative` above.
|
|
368
|
+
if (ctl && sock)
|
|
369
|
+
svcLayer.markUp(ctl, 'observed');
|
|
361
370
|
if (ctl)
|
|
362
371
|
svcLayer.scaleUp(ctl);
|
|
363
372
|
if (!sock) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lensmcp/cluster",
|
|
3
|
-
"version": "1.18.
|
|
3
|
+
"version": "1.18.1",
|
|
4
4
|
"description": "Run your Nx workspace as a local production cluster: pods on unix sockets with true HMR, one https gateway with per-project domains, scale-from-zero, autoscale, idle-kill, local-CA TLS — observed by the LensMCP lens.",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"types": "./index.d.ts",
|
|
@@ -65,8 +65,8 @@
|
|
|
65
65
|
}
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
|
-
"@lensmcp/node-instrumentation": "1.18.
|
|
69
|
-
"@lensmcp/nx-plugin": "1.18.
|
|
68
|
+
"@lensmcp/node-instrumentation": "1.18.1",
|
|
69
|
+
"@lensmcp/nx-plugin": "1.18.1",
|
|
70
70
|
"@typescript/native-preview": "^7.0.0-dev.20260707.2",
|
|
71
71
|
"fork-ts-checker-webpack-plugin": "^9.0.0",
|
|
72
72
|
"glob": "^11.0.0",
|