@lensmcp/cluster 1.25.2 → 1.25.5
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/lifecycle.d.ts +12 -0
- package/executors/gateway/runtime/lifecycle.js +3 -3
- package/executors/gateway/runtime/own-log.d.ts +41 -0
- package/executors/gateway/runtime/own-log.js +3 -0
- package/executors/gateway/runtime/proxy.d.ts +1 -0
- package/executors/gateway/runtime/proxy.js +1 -1
- package/executors/gateway/runtime/server.js +2 -2
- package/executors/gateway/runtime/types.d.ts +4 -0
- package/executors/serve/schema.d.ts +4 -0
- package/executors/serve/schema.json +14 -1
- package/executors/serve/serve.impl.js +1 -1
- package/main.devserver.js +12 -11
- package/package.json +5 -4
- package/swap-mode.d.ts +8 -0
- package/swap-mode.js +1 -1
- package/worker-guard.d.ts +1 -0
- package/worker-guard.js +1 -0
- package/worker-supervisor.d.ts +142 -0
- package/worker-supervisor.js +2 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/** One worker, as the devserver runs it. */
|
|
2
|
+
export interface WorkerSpec {
|
|
3
|
+
/** The bundle's entry name — `<bundle dir>/<name>.js`. */
|
|
4
|
+
name: string;
|
|
5
|
+
/** Merged over the devserver's environment. */
|
|
6
|
+
env: Record<string, string>;
|
|
7
|
+
/** The `PORT` the worker gets: 0 — the kernel picks a free one — unless one is declared. */
|
|
8
|
+
port: number;
|
|
9
|
+
}
|
|
10
|
+
/** A `workers` entry of the serve executor, as project.json declares it. */
|
|
11
|
+
export interface WorkerEntryOptions {
|
|
12
|
+
name: string;
|
|
13
|
+
env?: Record<string, unknown>;
|
|
14
|
+
port?: number;
|
|
15
|
+
}
|
|
16
|
+
/** What the serve executor hands the devserver in `WORKERS`: each worker's name, env and port. */
|
|
17
|
+
export declare function serializeWorkerSpecs(workers: readonly WorkerEntryOptions[]): string;
|
|
18
|
+
/**
|
|
19
|
+
* `WORKERS` read back: a list of names (what a devserver was handed before 1.25.3) or of `{ name, env?, port? }`. What
|
|
20
|
+
* cannot be used is left out and said in `problems` — a bad entry never stops the others.
|
|
21
|
+
*/
|
|
22
|
+
export declare function parseWorkerSpecs(raw: string | undefined): {
|
|
23
|
+
specs: WorkerSpec[];
|
|
24
|
+
problems: string[];
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* The worker's environment: the devserver's, then its `PORT` — never the devserver's own, which the devserver is
|
|
28
|
+
* listening on — then what it declares. `LENSMCP_WORKER` names it, for a bundle shared by the pods and a worker.
|
|
29
|
+
*/
|
|
30
|
+
export declare function workerEnv(base: NodeJS.ProcessEnv, spec: WorkerSpec): NodeJS.ProcessEnv;
|
|
31
|
+
/** How a worker that ended is started again. */
|
|
32
|
+
export interface WorkerBackoff {
|
|
33
|
+
/** The first wait; it doubles with each crash in a row. */
|
|
34
|
+
baseMs: number;
|
|
35
|
+
maxMs: number;
|
|
36
|
+
/** Up this long, a worker's crashes in a row start again from none. */
|
|
37
|
+
stableMs: number;
|
|
38
|
+
}
|
|
39
|
+
/** Crashes in a row from which a worker is called crash-looping. */
|
|
40
|
+
export declare const CRASH_LOOP_AFTER = 3;
|
|
41
|
+
/** The shortest wait before a crashed worker starts again, whatever is configured: a worker that dies at boot must not spin. */
|
|
42
|
+
export declare const MIN_RESPAWN_MS = 100;
|
|
43
|
+
/** `LENSMCP_WORKER_BACKOFF_MS` (1 s, at least {@link MIN_RESPAWN_MS}), `LENSMCP_WORKER_BACKOFF_MAX_MS` (60 s), `LENSMCP_WORKER_STABLE_MS` (30 s). */
|
|
44
|
+
export declare function workerBackoff(env?: NodeJS.ProcessEnv): WorkerBackoff;
|
|
45
|
+
/** The wait before the next start after `crashes` crashes in a row (1 → `baseMs`): doubling, up to `maxMs`. */
|
|
46
|
+
export declare function respawnDelay(crashes: number, b: WorkerBackoff): number;
|
|
47
|
+
/**
|
|
48
|
+
* Why a worker ended:
|
|
49
|
+
* - `stopped` — it was asked to: replaced by a new build's, or its devserver is stopping;
|
|
50
|
+
* - `exited` — it ended on its own with code 0: taken to have finished its work, and not started again until the
|
|
51
|
+
* next build (a one-shot worker must not be run in a loop);
|
|
52
|
+
* - `crashed` — anything else: an exit code, a signal — `external` when it is a stop signal nobody here sent
|
|
53
|
+
* (`kill <pid>`): started again either way, as a supervisor does.
|
|
54
|
+
*/
|
|
55
|
+
export type WorkerExitVerdict = {
|
|
56
|
+
kind: 'stopped';
|
|
57
|
+
} | {
|
|
58
|
+
kind: 'exited';
|
|
59
|
+
} | {
|
|
60
|
+
kind: 'crashed';
|
|
61
|
+
external: boolean;
|
|
62
|
+
};
|
|
63
|
+
export declare function workerExitVerdict(code: number | null, signal: NodeJS.Signals | null, asked: boolean): WorkerExitVerdict;
|
|
64
|
+
/** `1.5 s`, `42 s`, `3 min`. */
|
|
65
|
+
export declare function spanOf(ms: number): string;
|
|
66
|
+
/**
|
|
67
|
+
* What the lens hears of a worker — `source: 'gateway'`, `category: 'cluster'`, the cluster's voice that the agent's
|
|
68
|
+
* cluster health and the dashboard's Cluster page fold:
|
|
69
|
+
* - `worker-started` — a worker process began: at boot, after a build (`reload`), or after it ended (`respawn`);
|
|
70
|
+
* - `worker-up` — one that had crashed has now stayed up `stableMs`: its crashes in a row are over;
|
|
71
|
+
* - `worker-crashed` — it ended unasked; when it starts again, and whether it is crash-looping;
|
|
72
|
+
* - `worker-exited` — it ended on its own with code 0, or never started (its bundle is missing): not started again
|
|
73
|
+
* until the next build;
|
|
74
|
+
* - `worker-stopped` — its devserver stopped.
|
|
75
|
+
* Every event names the worker's pid: a reader applies an end to the process it names, never to its successor.
|
|
76
|
+
*/
|
|
77
|
+
export type WorkerEventKind = 'worker-started' | 'worker-up' | 'worker-crashed' | 'worker-exited' | 'worker-stopped';
|
|
78
|
+
/** Who a worker is — stamped into every event's `raw`. */
|
|
79
|
+
export interface WorkerIdentity {
|
|
80
|
+
project: string;
|
|
81
|
+
service: string;
|
|
82
|
+
worker: string;
|
|
83
|
+
pid: number;
|
|
84
|
+
/** The devserver tree's generation (`LENSMCP_POD_GENERATION`). */
|
|
85
|
+
podGeneration: number;
|
|
86
|
+
}
|
|
87
|
+
export interface WorkerBusEvent {
|
|
88
|
+
id: string;
|
|
89
|
+
sessionId: string;
|
|
90
|
+
timestamp: number;
|
|
91
|
+
source: 'gateway';
|
|
92
|
+
category: 'cluster';
|
|
93
|
+
severity: 'info' | 'warning' | 'error';
|
|
94
|
+
context: {
|
|
95
|
+
sessionId: string;
|
|
96
|
+
projectName: string;
|
|
97
|
+
};
|
|
98
|
+
fingerprint: string;
|
|
99
|
+
title: string;
|
|
100
|
+
message?: string;
|
|
101
|
+
raw: Record<string, unknown>;
|
|
102
|
+
}
|
|
103
|
+
export interface WorkerStarted {
|
|
104
|
+
why: 'boot' | 'reload' | 'respawn';
|
|
105
|
+
port: number;
|
|
106
|
+
restarts: number;
|
|
107
|
+
}
|
|
108
|
+
export interface WorkerCrashed {
|
|
109
|
+
code: number | null;
|
|
110
|
+
signal: NodeJS.Signals | null;
|
|
111
|
+
ranMs: number;
|
|
112
|
+
crashes: number;
|
|
113
|
+
restarts: number;
|
|
114
|
+
nextInMs: number;
|
|
115
|
+
external: boolean;
|
|
116
|
+
/** The worker's last lines of output, as it printed them ({@link crashLines} folds them). */
|
|
117
|
+
lines: string[];
|
|
118
|
+
}
|
|
119
|
+
export interface WorkerUp {
|
|
120
|
+
ranMs: number;
|
|
121
|
+
afterCrashes: number;
|
|
122
|
+
}
|
|
123
|
+
export interface WorkerExited {
|
|
124
|
+
ranMs: number;
|
|
125
|
+
reason?: string;
|
|
126
|
+
}
|
|
127
|
+
export interface WorkerStopped {
|
|
128
|
+
reason: string;
|
|
129
|
+
}
|
|
130
|
+
/** Lines a crash event carries — the end of what the worker said, its stack folded ({@link crashLines}). */
|
|
131
|
+
export declare const CRASH_LINES = 12;
|
|
132
|
+
/**
|
|
133
|
+
* The end of what a worker said before it died, for a reader: a stack's frames past the first few folded into one line
|
|
134
|
+
* — an uncaught error's message and what the app logged before it are what say why; twenty frames of module loader do
|
|
135
|
+
* not, and kept whole they pushed both out of the window.
|
|
136
|
+
*/
|
|
137
|
+
export declare function crashLines(tail: readonly string[], max?: number): string[];
|
|
138
|
+
export declare function buildWorkerStartedEvent(who: WorkerIdentity, s: WorkerStarted, at?: number, id?: () => string): WorkerBusEvent;
|
|
139
|
+
export declare function buildWorkerUpEvent(who: WorkerIdentity, u: WorkerUp, at?: number, id?: () => string): WorkerBusEvent;
|
|
140
|
+
export declare function buildWorkerCrashedEvent(who: WorkerIdentity, c: WorkerCrashed, at?: number, id?: () => string): WorkerBusEvent;
|
|
141
|
+
export declare function buildWorkerExitedEvent(who: WorkerIdentity, e: WorkerExited, at?: number, id?: () => string): WorkerBusEvent;
|
|
142
|
+
export declare function buildWorkerStoppedEvent(who: WorkerIdentity, s: WorkerStopped, at?: number, id?: () => string): WorkerBusEvent;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";var k=Object.defineProperty;var a=(r,e)=>k(r,"name",{value:e,configurable:!0});var f=Object.defineProperty,i=a((r,e)=>f(r,"name",{value:e,configurable:!0}),"i");Object.defineProperty(exports,"__esModule",{value:!0}),exports.CRASH_LINES=exports.MIN_RESPAWN_MS=exports.CRASH_LOOP_AFTER=void 0,exports.serializeWorkerSpecs=serializeWorkerSpecs,exports.parseWorkerSpecs=parseWorkerSpecs,exports.workerEnv=workerEnv,exports.workerBackoff=workerBackoff,exports.respawnDelay=respawnDelay,exports.workerExitVerdict=workerExitVerdict,exports.spanOf=spanOf,exports.crashLines=crashLines,exports.buildWorkerStartedEvent=buildWorkerStartedEvent,exports.buildWorkerUpEvent=buildWorkerUpEvent,exports.buildWorkerCrashedEvent=buildWorkerCrashedEvent,exports.buildWorkerExitedEvent=buildWorkerExitedEvent,exports.buildWorkerStoppedEvent=buildWorkerStoppedEvent;const exit_reason_1=require("./exit-reason"),NAME_RE=/^[\w.-]+$/,ENV_KEY_RE=/^[A-Za-z_][A-Za-z0-9_]*$/;function serializeWorkerSpecs(r){return JSON.stringify(r.map(e=>({name:e.name,...e.env&&typeof e.env=="object"?{env:e.env}:{},...e.port!==void 0?{port:e.port}:{}})))}a(serializeWorkerSpecs,"serializeWorkerSpecs"),i(serializeWorkerSpecs,"serializeWorkerSpecs");function parseWorkerSpecs(r){const e=[];let t;try{t=JSON.parse(r||"[]")}catch{return{specs:[],problems:[`WORKERS is not JSON: ${String(r).slice(0,80)}`]}}if(!Array.isArray(t))return{specs:[],problems:["WORKERS is not a list"]};const n=[];for(const s of t){const o=typeof s=="string"?{name:s}:s,p=typeof o?.name=="string"?o.name:"";if(!NAME_RE.test(p)){e.push(`worker ${JSON.stringify(o?.name??s)} is not a plain file name \u2014 not started`);continue}if(n.some(d=>d.name===p)){e.push(`worker "${p}" is declared twice \u2014 started once`);continue}const u={};for(const[d,l]of Object.entries(o?.env&&typeof o.env=="object"?o.env:{})){if(!ENV_KEY_RE.test(d)){e.push(`worker "${p}": env ${JSON.stringify(d)} is not a variable name \u2014 left out`);continue}typeof l=="string"||typeof l=="number"||typeof l=="boolean"?u[d]=String(l):e.push(`worker "${p}": env ${d} is not a string, number or boolean \u2014 left out`)}let c=0;o?.port!==void 0&&(Number.isInteger(o.port)&&o.port>=0&&o.port<=65535?c=o.port:e.push(`worker "${p}": port ${JSON.stringify(o.port)} is not a port \u2014 it gets 0 (a free one)`)),n.push({name:p,env:u,port:c})}return{specs:n,problems:e}}a(parseWorkerSpecs,"parseWorkerSpecs"),i(parseWorkerSpecs,"parseWorkerSpecs");function workerEnv(r,e){const t={...r,PORT:String(e.port),...e.env,LENSMCP_WORKER:e.name};return delete t.WORKERS,t}a(workerEnv,"workerEnv"),i(workerEnv,"workerEnv"),exports.CRASH_LOOP_AFTER=3;function envMs(r,e,t){const n=Number(r[e]);return r[e]!==void 0&&r[e]!==""&&Number.isFinite(n)&&n>=0?Math.floor(n):t}a(envMs,"envMs"),i(envMs,"envMs"),exports.MIN_RESPAWN_MS=100;function workerBackoff(r=process.env){const e=Math.max(exports.MIN_RESPAWN_MS,envMs(r,"LENSMCP_WORKER_BACKOFF_MS",1e3));return{baseMs:e,maxMs:Math.max(e,envMs(r,"LENSMCP_WORKER_BACKOFF_MAX_MS",6e4)),stableMs:envMs(r,"LENSMCP_WORKER_STABLE_MS",3e4)}}a(workerBackoff,"workerBackoff"),i(workerBackoff,"workerBackoff");function respawnDelay(r,e){const t=Math.max(1,Math.floor(r));return Math.min(e.maxMs,e.baseMs*2**Math.min(t-1,30))}a(respawnDelay,"respawnDelay"),i(respawnDelay,"respawnDelay");function workerExitVerdict(r,e,t){return t?{kind:"stopped"}:r===0&&!e?{kind:"exited"}:{kind:"crashed",external:e==="SIGTERM"||e==="SIGINT"||e==="SIGHUP"}}a(workerExitVerdict,"workerExitVerdict"),i(workerExitVerdict,"workerExitVerdict");function spanOf(r){return r<1e4?`${Math.round(r/100)/10} s`:r<12e4?`${Math.round(r/1e3)} s`:`${Math.round(r/6e4)} min`}a(spanOf,"spanOf"),i(spanOf,"spanOf"),exports.CRASH_LINES=12;const STACK_FRAMES=3;function crashLines(r,e=exports.CRASH_LINES){const t=[];let n=0,s=0;const o=i(()=>{s&&t.push(` \u2026 ${s} more frame${s===1?"":"s"}`),s=0},"fold");for(const p of r){if(/^\s+at\s/.test(p)){if(n+=1,n>STACK_FRAMES){s+=1;continue}}else o(),n=0;t.push(p)}return o(),t.slice(-e)}a(crashLines,"crashLines"),i(crashLines,"crashLines");let idCounter=0;function makeId(){return idCounter=(idCounter+1)%65535,`${Date.now().toString(36)}${idCounter.toString(36)}${Math.random().toString(36).slice(2,8)}`}a(makeId,"makeId"),i(makeId,"makeId");function envelope(r,e,t,n,s,o,p,u){const c="pending";return{id:p(),sessionId:c,timestamp:o,source:"gateway",category:"cluster",severity:t,context:{sessionId:c,projectName:r.project},fingerprint:`cluster-${e}:${r.service}`,title:n,...u?{message:u}:{},raw:{kind:e,project:r.project,service:r.service,worker:r.worker,pid:r.pid,podGeneration:r.podGeneration,...s}}}a(envelope,"envelope"),i(envelope,"envelope");function buildWorkerStartedEvent(r,e,t=Date.now(),n=makeId){const s=e.why==="respawn"?`started again (restart ${e.restarts})`:e.why==="reload"?"started on the new build":"started";return envelope(r,"worker-started","info",`${r.service} worker ${r.worker} ${s} \u2014 pid ${r.pid}`,{why:e.why,port:e.port,restarts:e.restarts},t,n)}a(buildWorkerStartedEvent,"buildWorkerStartedEvent"),i(buildWorkerStartedEvent,"buildWorkerStartedEvent");function buildWorkerUpEvent(r,e,t=Date.now(),n=makeId){return envelope(r,"worker-up","info",`${r.service} worker ${r.worker} is up again \u2014 ${spanOf(e.ranMs)} without a crash, after ${e.afterCrashes} in a row`,{ranMs:e.ranMs,afterCrashes:e.afterCrashes},t,n)}a(buildWorkerUpEvent,"buildWorkerUpEvent"),i(buildWorkerUpEvent,"buildWorkerUpEvent");function buildWorkerCrashedEvent(r,e,t=Date.now(),n=makeId){const s=e.crashes>=exports.CRASH_LOOP_AFTER,o=e.signal?e.signal:`exit ${e.code}`,p=s?`${r.service} worker ${r.worker} is crash-looping \u2014 ${e.crashes} crashes in a row (${o}); next start in ${spanOf(e.nextInMs)}`:`${r.service} worker ${r.worker} crashed (${o}${e.external?", a signal from outside its devserver":""}) after ${spanOf(e.ranMs)} \u2014 starting it again in ${spanOf(e.nextInMs)}`,u=e.external?`stopped by ${e.signal} from outside its devserver (\`kill\`, Activity Monitor)`:(0,exit_reason_1.describeExit)(e.code,e.signal),c=crashLines(e.lines);return envelope(r,"worker-crashed","error",p,{exitCode:e.code,signal:e.signal,ranMs:e.ranMs,crashes:e.crashes,restarts:e.restarts,nextInMs:e.nextInMs,crashLooping:s,external:e.external,...u?{reading:u}:{},lines:c},t,n,c.length?c.join(`
|
|
2
|
+
`):void 0)}a(buildWorkerCrashedEvent,"buildWorkerCrashedEvent"),i(buildWorkerCrashedEvent,"buildWorkerCrashedEvent");function buildWorkerExitedEvent(r,e,t=Date.now(),n=makeId){const s=e.reason??`it ended on its own (code 0) after ${spanOf(e.ranMs)}`;return envelope(r,"worker-exited","warning",`${r.service} worker ${r.worker} is not running \u2014 ${s}; not started again until the next build`,{ranMs:e.ranMs,...e.reason?{reason:e.reason}:{}},t,n)}a(buildWorkerExitedEvent,"buildWorkerExitedEvent"),i(buildWorkerExitedEvent,"buildWorkerExitedEvent");function buildWorkerStoppedEvent(r,e,t=Date.now(),n=makeId){return envelope(r,"worker-stopped","info",`${r.service} worker ${r.worker} stopped \u2014 ${e.reason}`,{reason:e.reason},t,n)}a(buildWorkerStoppedEvent,"buildWorkerStoppedEvent"),i(buildWorkerStoppedEvent,"buildWorkerStoppedEvent");
|