@lensmcp/cluster 1.15.0 → 1.16.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 +1 -1
- package/executors/gateway/runtime/lens-children.d.ts.map +1 -1
- package/executors/gateway/runtime/lens-children.js +2 -1
- package/executors/gateway/runtime/lifecycle.d.ts.map +1 -1
- package/executors/gateway/runtime/lifecycle.js +34 -2
- package/executors/gateway/runtime/observability.d.ts +9 -0
- package/executors/gateway/runtime/observability.d.ts.map +1 -1
- package/executors/gateway/runtime/observability.js +18 -0
- package/executors/gateway/runtime/proxy.d.ts.map +1 -1
- package/executors/gateway/runtime/proxy.js +34 -4
- package/executors/gateway/runtime/types.d.ts +21 -0
- package/executors/gateway/runtime/types.d.ts.map +1 -1
- package/executors/gateway/runtime/types.js +13 -0
- package/executors/serve/serve.impl.d.ts.map +1 -1
- package/executors/serve/serve.impl.js +12 -1
- package/package.json +3 -3
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type SpawnChild } from '@lensmcp/nx-plugin/lens-frontend';
|
|
2
2
|
import type { GatewayRuntime, GatewayRuntimeOptions } from './types';
|
|
3
|
-
import type
|
|
3
|
+
import { type Observability } from './observability';
|
|
4
4
|
export interface LensChildren {
|
|
5
5
|
spawnManagedChild: SpawnChild;
|
|
6
6
|
/** Boot the dashboard + MCP singletons and every `lens:true` app. Pushes the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lens-children.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/lens-children.ts"],"names":[],"mappings":"AAQA,OAAO,EAIL,KAAK,UAAU,EAChB,MAAM,kCAAkC,CAAC;AAE1C,OAAO,KAAK,EAAE,cAAc,EAAE,qBAAqB,EAAgB,MAAM,SAAS,CAAC;AACnF,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"lens-children.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/lens-children.ts"],"names":[],"mappings":"AAQA,OAAO,EAIL,KAAK,UAAU,EAChB,MAAM,kCAAkC,CAAC;AAE1C,OAAO,KAAK,EAAE,cAAc,EAAE,qBAAqB,EAAgB,MAAM,SAAS,CAAC;AACnF,OAAO,EAA0B,KAAK,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAM7E,MAAM,WAAW,YAAY;IAC3B,iBAAiB,EAAE,UAAU,CAAC;IAC9B;iFAC6E;IAC7E,qBAAqB,IAAI,IAAI,CAAC;IAC9B,wDAAwD;IACxD,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,cAAc,EAAE,GAAG,EAAE,aAAa,EAAE,OAAO,EAAE,qBAAqB,GAAG,YAAY,CA0HvH"}
|
|
@@ -11,6 +11,7 @@ const node_child_process_1 = require("node:child_process");
|
|
|
11
11
|
const manifest_1 = require("../manifest");
|
|
12
12
|
const lens_frontend_1 = require("@lensmcp/nx-plugin/lens-frontend");
|
|
13
13
|
const scope_1 = require("./scope");
|
|
14
|
+
const observability_1 = require("./observability");
|
|
14
15
|
const HEAL_BASE_MS = 1_500; // first restart delay
|
|
15
16
|
const HEAL_MAX_MS = 30_000; // backoff cap — a true boot-loop settles here, never hot-loops
|
|
16
17
|
const HEAL_HEALTHY_MS = 20_000; // ran at least this long ⇒ a FRESH crash ⇒ reset the backoff budget
|
|
@@ -72,7 +73,7 @@ function createLensChildren(rt, obs, options) {
|
|
|
72
73
|
const lensBase = scope.basePath; // e.g. /tetros
|
|
73
74
|
// Pushed BEFORE the cert SANs are derived (routes.map(r=>r.host)), so lensmcp.local
|
|
74
75
|
// auto-joins the TLS SANs. Run `gateway:trust` to add it to /etc/hosts.
|
|
75
|
-
rt.routes.push({ host: lensHost, prefix: lensBase, project:
|
|
76
|
+
rt.routes.push({ host: lensHost, prefix: lensBase, project: observability_1.LENS_DASHBOARD_PROJECT, target: `http://localhost:${dashboardPort}` });
|
|
76
77
|
(0, manifest_1.sortBySpecificity)(rt.routes);
|
|
77
78
|
console.log(`[gateway] starting lens dashboard → http://localhost:${dashboardPort} (routed at https://${lensHost}${lensBase}/)`);
|
|
78
79
|
emit('info', `lens dashboard up: ${lensHost}${lensBase} → :${dashboardPort}`, 'cluster-lens-dashboard', { kind: 'lens-dashboard-up', host: lensHost, port: dashboardPort, base: lensBase });
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/lifecycle.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/lifecycle.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEpE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AA0BrD,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAC;IAChC,YAAY,CAAC,GAAG,EAAE,UAAU,GAAG,IAAI,CAAC;IACpC,QAAQ,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5C,OAAO,CAAC,GAAG,EAAE,UAAU,GAAG,IAAI,CAAC;IAC/B,WAAW,CAAC,GAAG,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAChD,2EAA2E;IAC3E,YAAY,IAAI,MAAM,CAAC,OAAO,CAAC;CAChC;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,cAAc,EAAE,GAAG,EAAE,aAAa,GAAG,YAAY,CAoKvF"}
|
|
@@ -13,6 +13,7 @@ const http = tslib_1.__importStar(require("node:http"));
|
|
|
13
13
|
const path = tslib_1.__importStar(require("node:path"));
|
|
14
14
|
const discovery_1 = require("./discovery");
|
|
15
15
|
const service_keys_1 = require("./service-keys");
|
|
16
|
+
const types_1 = require("./types");
|
|
16
17
|
/**
|
|
17
18
|
* The east-west env a pod needs to call ANOTHER service's `internal.<host>` route (memberships, settings,
|
|
18
19
|
* plan): (1) its OWN per-service key — presented as `x-api-key`; the gateway maps it back to the caller and
|
|
@@ -71,11 +72,20 @@ function createServiceLayer(rt, obs) {
|
|
|
71
72
|
for (const line of text.split('\n')) {
|
|
72
73
|
if (/ERROR in |error TS\d+|Found \d+ error|Unhandled 'error' event|EADDRINUSE/.test(line)) {
|
|
73
74
|
// eslint-disable-next-line no-control-regex -- strip ANSI color codes from child output
|
|
74
|
-
|
|
75
|
+
const clean = line.replace(/\x1b\[[0-9;]*m/g, '').trim();
|
|
76
|
+
svc.lastErrors.push(clean);
|
|
75
77
|
if (svc.lastErrors.length > 12)
|
|
76
78
|
svc.lastErrors.shift();
|
|
77
79
|
svc.buildFailed = true;
|
|
78
|
-
|
|
80
|
+
// THROTTLE: a crash-looping pod re-prints the same error line on every restart. Emit the
|
|
81
|
+
// 'service error' event only when the error line CHANGES or after the throttle window —
|
|
82
|
+
// otherwise the bus (and the lens log) floods with identical errors many times a second.
|
|
83
|
+
const now = Date.now();
|
|
84
|
+
if (clean !== svc.lastErrorEmitSig || now - (svc.lastErrorEmitAt ?? 0) >= types_1.SERVICE_ERROR_THROTTLE_MS) {
|
|
85
|
+
svc.lastErrorEmitSig = clean;
|
|
86
|
+
svc.lastErrorEmitAt = now;
|
|
87
|
+
emit('error', `service error: ${svc.project}`, `cluster-service:${svc.project}`, { kind: 'service-error', project: svc.project, line: clean });
|
|
88
|
+
}
|
|
79
89
|
}
|
|
80
90
|
if (/No typescript errors found|webpack compiled successfully|Child#\d+ ready/.test(line))
|
|
81
91
|
svc.buildFailed = false;
|
|
@@ -92,6 +102,28 @@ function createServiceLayer(rt, obs) {
|
|
|
92
102
|
return; // gateway is tearing down — its dying services' exit chatter is noise
|
|
93
103
|
console.log(`[gateway] ${target} exited (${code})`);
|
|
94
104
|
emit(code === 0 ? 'info' : 'warning', `service down: ${svc.project} (exit ${code})`, `cluster-service:${svc.project}`, { kind: 'service-down', project: svc.project, code });
|
|
105
|
+
// SELF-HEAL: respawn an UNEXPECTEDLY-crashed pod so a transient failure recovers with no manual
|
|
106
|
+
// restart. A clean exit (code 0) or a sniffed BUILD failure (a code error → a fix, not a transient
|
|
107
|
+
// crash) is left down — scale-from-zero (ensureUp) still spawns it on the next request. A crash-loop
|
|
108
|
+
// guard (≤ SERVICE_MAX_RESTARTS within the window, exponential backoff) stops a broken service
|
|
109
|
+
// hot-looping the CPU.
|
|
110
|
+
if (code === 0 || svc.buildFailed)
|
|
111
|
+
return;
|
|
112
|
+
const now = Date.now();
|
|
113
|
+
svc.restarts = (svc.restarts ?? []).filter((t) => now - t < types_1.SERVICE_RESTART_WINDOW_MS);
|
|
114
|
+
if (svc.restarts.length >= types_1.SERVICE_MAX_RESTARTS) {
|
|
115
|
+
emit('error', `service crash-looping: ${svc.project} — ${types_1.SERVICE_MAX_RESTARTS}+ restarts/min, not auto-respawning (the next request retries)`, `cluster-service:${svc.project}`, { kind: 'service-crash-loop', project: svc.project });
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
svc.restarts.push(now);
|
|
119
|
+
const backoffMs = Math.min(8000, 500 * 2 ** (svc.restarts.length - 1)); // 0.5 → 1 → 2 → 4 → 8s
|
|
120
|
+
setTimeout(() => {
|
|
121
|
+
if (rt.stopped() || svc.proc || svc.state !== 'down')
|
|
122
|
+
return; // a request already respawned it, or teardown
|
|
123
|
+
console.log(`[gateway] self-heal: respawning ${svc.project} (restart ${svc.restarts.length}/${types_1.SERVICE_MAX_RESTARTS})`);
|
|
124
|
+
emit('info', `self-heal: respawning ${svc.project}`, `cluster-service:${svc.project}`, { kind: 'service-respawn', project: svc.project, attempt: svc.restarts.length });
|
|
125
|
+
spawnService(svc);
|
|
126
|
+
}, backoffMs);
|
|
95
127
|
});
|
|
96
128
|
svc.proc = proc;
|
|
97
129
|
};
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import type * as http from 'node:http';
|
|
2
2
|
import type { GwTrace, Route } from './types';
|
|
3
3
|
export type Severity = 'info' | 'warning' | 'error';
|
|
4
|
+
/**
|
|
5
|
+
* The lens's OWN dashboard route project (the value `lens-children.ts` gives the `lensmcp.local/<key>/`
|
|
6
|
+
* route). Traffic to it is the lens UI polling its own control-plane API (`/<key>/api/cluster|logs|snapshot`)
|
|
7
|
+
* ~1×/s — pure self-observation with NO app-diagnostic value. Its SUCCESSFUL request narration + traffic
|
|
8
|
+
* edges are suppressed from the bus (the same "don't bloat the bus" rationale as header-less traffic); a 5xx
|
|
9
|
+
* still surfaces so a genuine lens-dashboard failure isn't hidden. One home for the literal (imported by
|
|
10
|
+
* `lens-children.ts` for the route + this layer for the filter, so they can't drift).
|
|
11
|
+
*/
|
|
12
|
+
export declare const LENS_DASHBOARD_PROJECT = "(lens-dashboard)";
|
|
4
13
|
export interface Observability {
|
|
5
14
|
emit(severity: Severity, title: string, fingerprint: string, raw?: Record<string, unknown>, flowCtx?: Record<string, unknown>): void;
|
|
6
15
|
eid(): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"observability.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/observability.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,KAAK,IAAI,MAAM,WAAW,CAAC;AACvC,OAAO,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAE9C,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC;AAEpD,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACrI,GAAG,IAAI,MAAM,CAAC;IACd,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,GAAG,OAAO,GAAG,SAAS,CAAC;IAC3D,SAAS,CAAC,CAAC,EAAE,OAAO,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC1F,WAAW,CAAC,CAAC,EAAE,OAAO,GAAG,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC5G,UAAU,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5E,0FAA0F;IAC1F,gBAAgB,IAAI,MAAM,CAAC,OAAO,CAAC;CACpC;AAKD,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE;IAAE,cAAc,EAAE,MAAM,CAAA;CAAE,GAAG,aAAa,
|
|
1
|
+
{"version":3,"file":"observability.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/observability.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,KAAK,IAAI,MAAM,WAAW,CAAC;AACvC,OAAO,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAE9C,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC;AAEpD;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,qBAAqB,CAAC;AAEzD,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACrI,GAAG,IAAI,MAAM,CAAC;IACd,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,GAAG,OAAO,GAAG,SAAS,CAAC;IAC3D,SAAS,CAAC,CAAC,EAAE,OAAO,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC1F,WAAW,CAAC,CAAC,EAAE,OAAO,GAAG,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC5G,UAAU,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5E,0FAA0F;IAC1F,gBAAgB,IAAI,MAAM,CAAC,OAAO,CAAC;CACpC;AAKD,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE;IAAE,cAAc,EAAE,MAAM,CAAA;CAAE,GAAG,aAAa,CAmGtG"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.LENS_DASHBOARD_PROJECT = void 0;
|
|
3
4
|
exports.createObservability = createObservability;
|
|
4
5
|
const tslib_1 = require("tslib");
|
|
5
6
|
/**
|
|
@@ -16,6 +17,15 @@ const tslib_1 = require("tslib");
|
|
|
16
17
|
*/
|
|
17
18
|
const fs = tslib_1.__importStar(require("node:fs"));
|
|
18
19
|
const path = tslib_1.__importStar(require("node:path"));
|
|
20
|
+
/**
|
|
21
|
+
* The lens's OWN dashboard route project (the value `lens-children.ts` gives the `lensmcp.local/<key>/`
|
|
22
|
+
* route). Traffic to it is the lens UI polling its own control-plane API (`/<key>/api/cluster|logs|snapshot`)
|
|
23
|
+
* ~1×/s — pure self-observation with NO app-diagnostic value. Its SUCCESSFUL request narration + traffic
|
|
24
|
+
* edges are suppressed from the bus (the same "don't bloat the bus" rationale as header-less traffic); a 5xx
|
|
25
|
+
* still surfaces so a genuine lens-dashboard failure isn't hidden. One home for the literal (imported by
|
|
26
|
+
* `lens-children.ts` for the route + this layer for the filter, so they can't drift).
|
|
27
|
+
*/
|
|
28
|
+
exports.LENS_DASHBOARD_PROJECT = '(lens-dashboard)';
|
|
19
29
|
const hdr = (v) => typeof v === 'string' && v !== '' ? v : Array.isArray(v) ? v[0] : undefined;
|
|
20
30
|
function createObservability(eventFile, opts) {
|
|
21
31
|
const eid = () => Date.now().toString(36) + Math.random().toString(36).slice(2, 12);
|
|
@@ -69,6 +79,10 @@ function createObservability(eventFile, opts) {
|
|
|
69
79
|
if (!t || t.done)
|
|
70
80
|
return;
|
|
71
81
|
t.done = true;
|
|
82
|
+
// The lens dashboard's own control-plane self-polling (1 req/s) would spam the bus with no
|
|
83
|
+
// app-diagnostic value — suppress its successful narration, but let a 5xx through (a real lens bug).
|
|
84
|
+
if (project === exports.LENS_DASHBOARD_PROJECT && status < 500)
|
|
85
|
+
return;
|
|
72
86
|
const durationMs = Date.now() - t.startedAt;
|
|
73
87
|
emit(status >= 500 ? 'error' : 'info', `gateway ${t.method} ${t.host}${t.url} → ${status} (${durationMs}ms)`, `gateway-request:${project}`, {
|
|
74
88
|
kind: 'gateway-request', project, host: t.host, method: t.method, url: t.url,
|
|
@@ -83,6 +97,10 @@ function createObservability(eventFile, opts) {
|
|
|
83
97
|
// The request-derived topology (nodes/edges appear from observed traffic).
|
|
84
98
|
const edges = new Map();
|
|
85
99
|
const recordEdge = (route, ms, status, caller) => {
|
|
100
|
+
// Don't accumulate a traffic edge for the lens dashboard's own self-polling (kills the
|
|
101
|
+
// `traffic lensmcp.local → (lens-dashboard)` spam); an error edge (5xx / connect-fail) still records.
|
|
102
|
+
if (route.project === exports.LENS_DASHBOARD_PROJECT && status < 500 && status !== 0)
|
|
103
|
+
return;
|
|
86
104
|
const host = route.host ?? '(default)';
|
|
87
105
|
const key = (caller ? caller + '⇒' : '') + host + '→' + route.project;
|
|
88
106
|
const e = edges.get(key) ?? { host, project: route.project, caller, count: 0, totalMs: 0, errors: 0 };
|
|
@@ -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;
|
|
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;AAqBhD,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,KAAK,IAAI,GAAG,IAAI,CAAC;IACrG,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,CA4GtG"}
|
|
@@ -13,6 +13,20 @@ const http = tslib_1.__importStar(require("node:http"));
|
|
|
13
13
|
const path = tslib_1.__importStar(require("node:path"));
|
|
14
14
|
const discovery_1 = require("./discovery");
|
|
15
15
|
const edge_1 = require("./edge");
|
|
16
|
+
const types_1 = require("./types");
|
|
17
|
+
/** A CONNECTION-class upstream error — the pod/dev-server is momentarily unreachable (restarting its
|
|
18
|
+
* internal server on a config change, or warming up), NOT an application response. Only these are
|
|
19
|
+
* retried by the self-heal; a real 5xx from the app has already streamed a response and never lands here. */
|
|
20
|
+
function isConnError(err) {
|
|
21
|
+
const code = err.code ?? '';
|
|
22
|
+
return (code === 'ECONNREFUSED' ||
|
|
23
|
+
code === 'ECONNRESET' ||
|
|
24
|
+
code === 'ETIMEDOUT' ||
|
|
25
|
+
code === 'EPIPE' ||
|
|
26
|
+
code === 'EHOSTUNREACH' ||
|
|
27
|
+
code === 'ENOENT' ||
|
|
28
|
+
/ECONNREFUSED|ECONNRESET|socket hang up|EPIPE/i.test(err.message));
|
|
29
|
+
}
|
|
16
30
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
17
31
|
const httpProxy = require('http-proxy');
|
|
18
32
|
function createProxy(rt, obs, svcLayer) {
|
|
@@ -99,10 +113,26 @@ function createProxy(rt, obs, svcLayer) {
|
|
|
99
113
|
}
|
|
100
114
|
// autoRewrite ONLY for a string (external URL) target — a non-string target crashes
|
|
101
115
|
// setRedirectHostRewrite's url.parse(options.target) on a 3xx (see the socketPath case above).
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
116
|
+
// SELF-HEAL (a TCP app upstream, e.g. a Vite dev server): on a config change Vite RESTARTS its
|
|
117
|
+
// internal server — the process stays alive, so the port refuses for ~1-3s. Rather than an instant
|
|
118
|
+
// 502 ("Upstream login unavailable"), retry the CONNECTION error with a short backoff until the
|
|
119
|
+
// restart/warmup finishes (the heal window), then finally 502. Retried only BEFORE any byte is
|
|
120
|
+
// written (the err cb fires pre-proxyRes) + only for a connection error (a real app 5xx streams
|
|
121
|
+
// through untouched).
|
|
122
|
+
const tcpTarget = route.target;
|
|
123
|
+
const tcpOpts = { target: tcpTarget, autoRewrite: typeof tcpTarget === 'string', changeOrigin: true, agent: agentForTarget(tcpTarget) };
|
|
124
|
+
const tcpDeadline = Date.now() + types_1.UPSTREAM_HEAL_WINDOW_MS;
|
|
125
|
+
const tryTcp = () => {
|
|
126
|
+
proxy.web(req, res, tcpOpts, (err) => {
|
|
127
|
+
if (isConnError(err) && Date.now() < tcpDeadline && !res.headersSent && !res.writableEnded) {
|
|
128
|
+
setTimeout(tryTcp, types_1.UPSTREAM_HEAL_STEP_MS);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
console.error(`[gateway] upstream ${route.project} (${String(tcpTarget)}):`, err.message);
|
|
132
|
+
(0, edge_1.sendError)(res, req, 502, 'unavailable', `Upstream ${route.project} unavailable.`);
|
|
133
|
+
});
|
|
134
|
+
};
|
|
135
|
+
tryTcp();
|
|
106
136
|
};
|
|
107
137
|
const destroyAgents = () => { httpAgent.destroy(); httpsAgent.destroy(); };
|
|
108
138
|
const closeProxy = () => { try {
|
|
@@ -117,7 +117,28 @@ export interface ServiceCtl {
|
|
|
117
117
|
spawnAt?: number;
|
|
118
118
|
lastErrors?: string[];
|
|
119
119
|
buildFailed?: boolean;
|
|
120
|
+
/** Throttle state for the 'service error' event: the last-emitted error signature + when. Stops a
|
|
121
|
+
* crash-looping pod (re-printing the same error line every restart) from flooding the event bus —
|
|
122
|
+
* we re-emit only when the error line CHANGES or after {@link SERVICE_ERROR_THROTTLE_MS}. */
|
|
123
|
+
lastErrorEmitSig?: string;
|
|
124
|
+
lastErrorEmitAt?: number;
|
|
125
|
+
/** Self-heal: timestamps (ms) of recent auto-respawns, kept for the crash-loop window. An unexpectedly
|
|
126
|
+
* exited pod is respawned so a transient crash recovers without a manual restart; too many in the
|
|
127
|
+
* window ⇒ stop hot-looping (the next request still spawns it via scale-from-zero). */
|
|
128
|
+
restarts?: number[];
|
|
120
129
|
}
|
|
130
|
+
/** Min interval (ms) between repeat 'service error' emits for the SAME service + same error line. */
|
|
131
|
+
export declare const SERVICE_ERROR_THROTTLE_MS = 5000;
|
|
132
|
+
/** Self-heal: an unexpectedly-exited pod (crash, not a clean exit or a build error) is auto-respawned
|
|
133
|
+
* after a backoff, capped at {@link SERVICE_MAX_RESTARTS} within {@link SERVICE_RESTART_WINDOW_MS} so a
|
|
134
|
+
* genuinely-broken service doesn't hot-loop the CPU. */
|
|
135
|
+
export declare const SERVICE_MAX_RESTARTS = 5;
|
|
136
|
+
export declare const SERVICE_RESTART_WINDOW_MS = 60000;
|
|
137
|
+
/** Self-heal: how long the proxy retries a CONNECTION-class upstream error (a Vite dev server restarting
|
|
138
|
+
* its internal server on a config change → the port refuses for ~1-3s; a pod warming up) before it
|
|
139
|
+
* finally 502s. Retried only BEFORE any response byte is written, and only for a connection error. */
|
|
140
|
+
export declare const UPSTREAM_HEAL_WINDOW_MS = 8000;
|
|
141
|
+
export declare const UPSTREAM_HEAL_STEP_MS = 300;
|
|
121
142
|
/** Test-tunable knobs on top of the user-facing schema. */
|
|
122
143
|
export interface GatewayRuntimeOptions extends GatewayExecutorSchema {
|
|
123
144
|
/** Traffic-edge flush period (ms). Default 5000. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/types.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../../../libs/cluster/src/executors/gateway/runtime/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,KAAK,KAAK,IAAI,MAAM,WAAW,CAAC;AACvC,OAAO,KAAK,KAAK,MAAM,MAAM,aAAa,CAAC;AAC3C,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,WAAW,CAAC;AACvD,OAAO,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAE5D,YAAY,EAAE,KAAK,EAAE,CAAC;AAEtB,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sBAAsB,CAAC,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE;YAAE,IAAI,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;KAAE,CAAC;CACzE;AAED,2FAA2F;AAC3F,MAAM,WAAW,kBAAkB;IACjC,wDAAwD;IACxD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sGAAsG;IACtG,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8BAA8B;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iFAAiF;IACjF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,oEAAoE;IACpE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,0EAA0E;IAC1E,YAAY,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IAC9B;;iGAE6F;IAC7F,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,2FAA2F;IAC3F,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,6EAA6E;IAC7E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+EAA+E;IAC/E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0DAA0D;IAC1D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4FAA4F;IAC5F,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;;;;OASG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,QAAQ;IAAG,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE;AAE1F,MAAM,WAAW,KAAK;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB,GAAG,CAAC,EAAE,UAAU,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB;gEAC4D;IAC5D,IAAI,CAAC,EAAE,YAAY,CAAC;CACrB;AAED;;;yBAGyB;AACzB,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,mDAAmD;IACnD,WAAW,EAAE,MAAM,CAAC;IACpB,kEAAkE;IAClE,IAAI,EAAE,MAAM,CAAC;IACb,oEAAoE;IACpE,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,kBAAkB,CAAC;IACzB,IAAI,EAAE,QAAQ,CAAC;IACf,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB,KAAK,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAAC;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;iGAE6F;IAC7F,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;2FAEuF;IACvF,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB;AAED,qGAAqG;AACrG,eAAO,MAAM,yBAAyB,OAAO,CAAC;AAE9C;;wDAEwD;AACxD,eAAO,MAAM,oBAAoB,IAAI,CAAC;AACtC,eAAO,MAAM,yBAAyB,QAAS,CAAC;AAEhD;;sGAEsG;AACtG,eAAO,MAAM,uBAAuB,OAAO,CAAC;AAC5C,eAAO,MAAM,qBAAqB,MAAM,CAAC;AAEzC,2DAA2D;AAC3D,MAAM,WAAW,qBAAsB,SAAQ,qBAAqB;IAClE,oDAAoD;IACpD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,+CAA+C;IAC/C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,8CAA8C;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,mDAAmD;IACnD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,iFAAiF;IACjF,KAAK,CAAC,EAAE,YAAY,CAAC;CACtB;AAED,MAAM,WAAW,aAAa;IAC5B,0EAA0E;IAC1E,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,QAAQ,EAAE,UAAU,EAAE,CAAC;IACvB,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B;AAGD,MAAM,WAAW,OAAO;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAGD;;;;;;GAMG;AACH,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,KAAK,EAAE,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,UAAU,EAAE,CAAC;IAChC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7C,QAAQ,CAAC,YAAY,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB;wFACoF;IACpF,OAAO,IAAI,OAAO,CAAC;CACpB;AAGD,2EAA2E;AAC3E,MAAM,MAAM,YAAY,GACpB,WAAW,GACX,YAAY,GACZ,WAAW,GACX,eAAe,GACf,YAAY,GACZ,SAAS,GACT,WAAW,CAAC;AAEhB,MAAM,MAAM,WAAW,GAAG,CAAC,GAAG,EAAE,WAAW,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAErE;6EAC6E;AAC7E,MAAM,WAAW,YAAY;IAC3B,SAAS,CAAC,EAAE,WAAW,EAAE,CAAC;IAC1B,UAAU,CAAC,EAAE,WAAW,EAAE,CAAC;IAC3B,SAAS,CAAC,EAAE,WAAW,EAAE,CAAC;IAC1B,aAAa,CAAC,EAAE,WAAW,EAAE,CAAC;IAC9B,UAAU,CAAC,EAAE,WAAW,EAAE,CAAC;IAC3B,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;IACxB,SAAS,CAAC,EAAE,WAAW,EAAE,CAAC;CAC3B;AAED;;;;GAIG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,CAAC;IACnC,kDAAkD;IAClD,QAAQ,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,cAAc,CAAC;IACnC,4CAA4C;IAC5C,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC;IAChC,gEAAgE;IAChE,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,wEAAwE;IACxE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,sFAAsF;IACtF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6EAA6E;IAC7E,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,wFAAwF;IACxF,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,wCAAwC;IACxC,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,qDAAqD;IACrD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uFAAuF;IACvF,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,kBAAkB,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7E,gGAAgG;IAChG,SAAS,IAAI,OAAO,CAAC;CACtB;AAED;;;0DAG0D;AAC1D,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,OAAO,CAAC;IACZ,8DAA8D;IAC9D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,mDAAmD;IACnD,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,yCAAyC;IACzC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,kBAAkB,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB"}
|
|
@@ -1,2 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.UPSTREAM_HEAL_STEP_MS = exports.UPSTREAM_HEAL_WINDOW_MS = exports.SERVICE_RESTART_WINDOW_MS = exports.SERVICE_MAX_RESTARTS = exports.SERVICE_ERROR_THROTTLE_MS = void 0;
|
|
4
|
+
/** Min interval (ms) between repeat 'service error' emits for the SAME service + same error line. */
|
|
5
|
+
exports.SERVICE_ERROR_THROTTLE_MS = 5000;
|
|
6
|
+
/** Self-heal: an unexpectedly-exited pod (crash, not a clean exit or a build error) is auto-respawned
|
|
7
|
+
* after a backoff, capped at {@link SERVICE_MAX_RESTARTS} within {@link SERVICE_RESTART_WINDOW_MS} so a
|
|
8
|
+
* genuinely-broken service doesn't hot-loop the CPU. */
|
|
9
|
+
exports.SERVICE_MAX_RESTARTS = 5;
|
|
10
|
+
exports.SERVICE_RESTART_WINDOW_MS = 60_000;
|
|
11
|
+
/** Self-heal: how long the proxy retries a CONNECTION-class upstream error (a Vite dev server restarting
|
|
12
|
+
* its internal server on a config change → the port refuses for ~1-3s; a pod warming up) before it
|
|
13
|
+
* finally 502s. Retried only BEFORE any response byte is written, and only for a connection error. */
|
|
14
|
+
exports.UPSTREAM_HEAL_WINDOW_MS = 8000;
|
|
15
|
+
exports.UPSTREAM_HEAL_STEP_MS = 300;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve.impl.d.ts","sourceRoot":"","sources":["../../../../../libs/cluster/src/executors/serve/serve.impl.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAKpD,UAAU,eAAe;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sBAAsB,CAAC,EAAE;QACvB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE;YAAE,IAAI,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;KAC5C,CAAC;CACH;AAED;;;;;;;;GAQG;AACH,iBAAgB,aAAa,CAC3B,OAAO,EAAE,mBAAmB,EAC5B,OAAO,EAAE,eAAe,GACvB,cAAc,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"serve.impl.d.ts","sourceRoot":"","sources":["../../../../../libs/cluster/src/executors/serve/serve.impl.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAKpD,UAAU,eAAe;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sBAAsB,CAAC,EAAE;QACvB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE;YAAE,IAAI,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;KAC5C,CAAC;CACH;AAED;;;;;;;;GAQG;AACH,iBAAgB,aAAa,CAC3B,OAAO,EAAE,mBAAmB,EAC5B,OAAO,EAAE,eAAe,GACvB,cAAc,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAgQxD;AAED,eAAe,aAAa,CAAC"}
|
|
@@ -28,7 +28,8 @@ async function* serveExecutor(options, context) {
|
|
|
28
28
|
// Resolve port and serviceName from config YAML
|
|
29
29
|
const configEnv = options.configEnv || 'development';
|
|
30
30
|
const configFilePath = path.join(workspaceRoot, 'config', `config.${configEnv}.yaml`);
|
|
31
|
-
|
|
31
|
+
const DEFAULT_PUBLIC_PORT = 3050;
|
|
32
|
+
let port = DEFAULT_PUBLIC_PORT;
|
|
32
33
|
let serviceName = context.projectName;
|
|
33
34
|
let yamlConfig = {};
|
|
34
35
|
if (fs.existsSync(configFilePath)) {
|
|
@@ -36,6 +37,16 @@ async function* serveExecutor(options, context) {
|
|
|
36
37
|
port = Number(yamlConfig.port) || port;
|
|
37
38
|
serviceName = yamlConfig.serviceName || serviceName;
|
|
38
39
|
}
|
|
40
|
+
else {
|
|
41
|
+
// No per-service config → the devserver's PUBLIC port falls back to the shared default. If TWO
|
|
42
|
+
// services both miss their config, both bind DEFAULT_PUBLIC_PORT and the loser crash-loops with a
|
|
43
|
+
// cryptic `EADDRINUSE :::3050` that the gateway surfaces as a "service error" — masquerading as a
|
|
44
|
+
// service bug. Warn loudly + actionably so the real cause (a missing `config/config.<env>.yaml`) is
|
|
45
|
+
// obvious. Fix: add `config/config.${configEnv}.yaml` with a distinct `port` (mirror its cluster decl).
|
|
46
|
+
console.warn(`[serve] ${context.projectName}: no config file at ${configFilePath} — public devserver port defaults to ${DEFAULT_PUBLIC_PORT}. ` +
|
|
47
|
+
`If another service also lacks its config, both bind ${DEFAULT_PUBLIC_PORT} → EADDRINUSE. ` +
|
|
48
|
+
`Add config/config.${configEnv}.yaml with a distinct \`port:\` (matching this project's cluster.port).`);
|
|
49
|
+
}
|
|
39
50
|
if (options.serviceName) {
|
|
40
51
|
serviceName = options.serviceName;
|
|
41
52
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lensmcp/cluster",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.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.
|
|
69
|
-
"@lensmcp/nx-plugin": "1.
|
|
68
|
+
"@lensmcp/node-instrumentation": "1.16.1",
|
|
69
|
+
"@lensmcp/nx-plugin": "1.16.1",
|
|
70
70
|
"fork-ts-checker-webpack-plugin": "^9.0.0",
|
|
71
71
|
"glob": "^11.0.0",
|
|
72
72
|
"http-proxy": "^1.18.0",
|