@juspay/neurolink 12.14.5 → 12.14.7
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/CHANGELOG.md +2 -2
- package/dist/browser/neurolink.min.js +412 -412
- package/dist/cli/commands/proxy.d.ts +1 -1
- package/dist/cli/commands/proxy.js +123 -14
- package/dist/cli/commands/proxyRestart.d.ts +3 -0
- package/dist/cli/commands/proxyRestart.js +88 -0
- package/dist/cli/parser.js +3 -1
- package/dist/proxy/bodyCaptureWorker.js +26 -1
- package/dist/proxy/codexUsage.js +83 -2
- package/dist/proxy/otelLogSink.d.ts +13 -0
- package/dist/proxy/otelLogSink.js +62 -1
- package/dist/proxy/proxyTraceContext.d.ts +21 -0
- package/dist/proxy/proxyTraceContext.js +47 -0
- package/dist/proxy/proxyTracer.d.ts +9 -5
- package/dist/proxy/proxyTracer.js +81 -2
- package/dist/proxy/requestLogger.js +24 -18
- package/dist/proxy/restartControl.d.ts +12 -0
- package/dist/proxy/restartControl.js +283 -0
- package/dist/proxy/rollingWorkerSupervisor.js +8 -0
- package/dist/server/routes/codexProxyRoutes.js +414 -332
- package/dist/types/cli.d.ts +7 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/proxy.d.ts +119 -3
- package/dist/types/proxyRestart.d.ts +49 -0
- package/dist/types/proxyRestart.js +1 -0
- package/docs-site/static/search-index.json +1 -1
- package/package.json +2 -1
- package/scripts/observability/check-proxy-telemetry.mjs +29 -241
- package/scripts/observability/proxy-telemetry-backend.mjs +202 -0
- package/scripts/observability/proxy-telemetry-check.mjs +544 -0
- package/scripts/observability/query-proxy-history.mjs +30 -19
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { createServer, request } from "node:http";
|
|
2
|
+
import { chmod, mkdir, rm } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
const statusSchema = z.object({
|
|
7
|
+
pid: z.number().int().positive(),
|
|
8
|
+
version: z.string(),
|
|
9
|
+
health: z.object({
|
|
10
|
+
ready: z.literal(true),
|
|
11
|
+
acceptingConnections: z.literal(true),
|
|
12
|
+
drainingForUpdate: z.literal(false),
|
|
13
|
+
}),
|
|
14
|
+
observability: z.object({
|
|
15
|
+
requestLogs: z.object({
|
|
16
|
+
diskEnabled: z.boolean(),
|
|
17
|
+
otel: z.object({ initialized: z.boolean() }),
|
|
18
|
+
}),
|
|
19
|
+
}),
|
|
20
|
+
});
|
|
21
|
+
const resultSchema = z.object({
|
|
22
|
+
ok: z.boolean(),
|
|
23
|
+
phase: z.enum([
|
|
24
|
+
"checked",
|
|
25
|
+
"refused",
|
|
26
|
+
"failed",
|
|
27
|
+
"activated",
|
|
28
|
+
"activated_unverified",
|
|
29
|
+
]),
|
|
30
|
+
message: z.string(),
|
|
31
|
+
supervisorPid: z.number().int().positive(),
|
|
32
|
+
previousWorkerPid: z.number().int().positive().optional(),
|
|
33
|
+
workerPid: z.number().int().positive().optional(),
|
|
34
|
+
version: z.string().optional(),
|
|
35
|
+
drainingWorkers: z.number().int().nonnegative(),
|
|
36
|
+
rejectedSocketsDelta: z.number().int().optional(),
|
|
37
|
+
failedTransfersDelta: z.number().int().optional(),
|
|
38
|
+
});
|
|
39
|
+
/**
|
|
40
|
+
* Own restart completion in the supervisor, so a disconnected CLI cannot leave
|
|
41
|
+
* admission closed. The control socket is private to the service's OS user.
|
|
42
|
+
* No update history, launcher, environment file or launchd unit is rewritten.
|
|
43
|
+
*/
|
|
44
|
+
export async function startProxyRestartControl(options) {
|
|
45
|
+
const instanceId = randomUUID();
|
|
46
|
+
const socketPath = join(options.stateDir, `restart-${process.pid}-${instanceId}.sock`);
|
|
47
|
+
await mkdir(options.stateDir, { recursive: true, mode: 0o700 });
|
|
48
|
+
// Never unlink an existing path: an unexpected owner must make setup fail.
|
|
49
|
+
let busy = false;
|
|
50
|
+
let closing = false;
|
|
51
|
+
const result = (phase, message) => {
|
|
52
|
+
const snapshot = options.server.snapshot();
|
|
53
|
+
return {
|
|
54
|
+
ok: phase === "checked" || phase === "activated",
|
|
55
|
+
phase,
|
|
56
|
+
message,
|
|
57
|
+
supervisorPid: process.pid,
|
|
58
|
+
workerPid: snapshot.active?.pid,
|
|
59
|
+
version: snapshot.active?.version,
|
|
60
|
+
drainingWorkers: snapshot.draining.length,
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
const assertIdle = () => {
|
|
64
|
+
const snapshot = options.server.snapshot();
|
|
65
|
+
if (closing || !snapshot.active) {
|
|
66
|
+
throw new Error("No serving worker is available for a rolling restart.");
|
|
67
|
+
}
|
|
68
|
+
if (snapshot.candidate || options.isUpdatePending()) {
|
|
69
|
+
throw new Error("An update or worker replacement is already in progress.");
|
|
70
|
+
}
|
|
71
|
+
if (snapshot.draining.length) {
|
|
72
|
+
throw new Error("A previous worker is still finishing requests; another restart is deferred.");
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
const operate = async (restart) => {
|
|
76
|
+
if (busy || closing) {
|
|
77
|
+
return result("refused", "A restart check or activation is already in progress.");
|
|
78
|
+
}
|
|
79
|
+
busy = true;
|
|
80
|
+
let activated = false;
|
|
81
|
+
let previousWorkerPid;
|
|
82
|
+
let handoffBaseline;
|
|
83
|
+
const transferDeltas = () => {
|
|
84
|
+
if (!handoffBaseline) {
|
|
85
|
+
return {};
|
|
86
|
+
}
|
|
87
|
+
const latest = options.server.snapshot();
|
|
88
|
+
return {
|
|
89
|
+
rejectedSocketsDelta: latest.rejectedSockets - handoffBaseline.rejectedSockets,
|
|
90
|
+
failedTransfersDelta: latest.failedTransfers - handoffBaseline.failedTransfers,
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
try {
|
|
94
|
+
assertIdle();
|
|
95
|
+
const before = options.server.snapshot();
|
|
96
|
+
const active = before.active;
|
|
97
|
+
if (!active) {
|
|
98
|
+
throw new Error("The serving worker exited during preflight.");
|
|
99
|
+
}
|
|
100
|
+
previousWorkerPid = active.pid;
|
|
101
|
+
const status = statusSchema.parse(await options.getStatus());
|
|
102
|
+
if (status.pid !== previousWorkerPid ||
|
|
103
|
+
status.version !== active.version) {
|
|
104
|
+
throw new Error("Serving worker identity changed during preflight; retry the check.");
|
|
105
|
+
}
|
|
106
|
+
const version = await options.getInstalledVersion();
|
|
107
|
+
if (!version || !/^\d+\.\d+\.\d+$/.test(version)) {
|
|
108
|
+
throw new Error("The configured worker executable did not report a valid installed version.");
|
|
109
|
+
}
|
|
110
|
+
assertIdle();
|
|
111
|
+
if (options.server.snapshot().active?.pid !== previousWorkerPid) {
|
|
112
|
+
throw new Error("Serving worker changed during preflight; retry the check.");
|
|
113
|
+
}
|
|
114
|
+
if (!restart) {
|
|
115
|
+
return {
|
|
116
|
+
...result("checked", "Rolling restart is available. Service settings and existing streams will be preserved."),
|
|
117
|
+
version,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
// The existing supervisor imposes a 120-second candidate readiness
|
|
121
|
+
// deadline and retains the serving worker when startup/activation fails.
|
|
122
|
+
// It switches new sockets before draining the previous generation.
|
|
123
|
+
handoffBaseline = options.server.snapshot();
|
|
124
|
+
const after = await options.server.replace(version);
|
|
125
|
+
activated = true;
|
|
126
|
+
const fresh = statusSchema.parse(await options.getStatus());
|
|
127
|
+
if (fresh.pid !== after.active?.pid ||
|
|
128
|
+
fresh.pid === previousWorkerPid ||
|
|
129
|
+
fresh.version !== version) {
|
|
130
|
+
throw new Error("The replacement's serving identity could not be verified.");
|
|
131
|
+
}
|
|
132
|
+
const beforeLogs = status.observability.requestLogs;
|
|
133
|
+
const afterLogs = fresh.observability.requestLogs;
|
|
134
|
+
// Preserve the selected sink in both directions. Unexpected disk writes
|
|
135
|
+
// regress OTel-only deployments; disabling an existing disk sink loses
|
|
136
|
+
// requested logs. Restart must not silently make either policy change.
|
|
137
|
+
if (beforeLogs.diskEnabled !== afterLogs.diskEnabled ||
|
|
138
|
+
(beforeLogs.otel.initialized && !afterLogs.otel.initialized)) {
|
|
139
|
+
throw new Error("The replacement's logging state regressed; inspect telemetry before further changes.");
|
|
140
|
+
}
|
|
141
|
+
const latest = options.server.snapshot();
|
|
142
|
+
const deltas = transferDeltas();
|
|
143
|
+
// These are observed failures, not attribution to the restart. A ready
|
|
144
|
+
// worker can coexist with a handoff whose no-loss claim is unverified.
|
|
145
|
+
if (latest.active?.pid !== fresh.pid ||
|
|
146
|
+
deltas.rejectedSocketsDelta !== 0 ||
|
|
147
|
+
deltas.failedTransfersDelta !== 0) {
|
|
148
|
+
throw new Error("Worker identity or socket transfer counters changed during verification.");
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
...result("activated", "Replacement is serving and accepting requests. Previous streams finish on their original worker."),
|
|
152
|
+
previousWorkerPid,
|
|
153
|
+
...deltas,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
// Schema errors contain paths only; never echo the status payload or env.
|
|
158
|
+
const message = error instanceof z.ZodError
|
|
159
|
+
? "Readiness, admission or logging status could not be verified."
|
|
160
|
+
: error instanceof Error
|
|
161
|
+
? error.message
|
|
162
|
+
: "Restart verification failed.";
|
|
163
|
+
return {
|
|
164
|
+
...result(activated ? "activated_unverified" : "failed", message),
|
|
165
|
+
previousWorkerPid,
|
|
166
|
+
...transferDeltas(),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
busy = false;
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
const control = createServer((req, res) => {
|
|
174
|
+
// The instance nonce prevents a stale client from acting on a reused path.
|
|
175
|
+
if (closing || req.headers["x-neurolink-instance"] !== instanceId) {
|
|
176
|
+
res.writeHead(409).end();
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (req.headers["transfer-encoding"] ||
|
|
180
|
+
(req.headers["content-length"] && req.headers["content-length"] !== "0")) {
|
|
181
|
+
res.writeHead(400, { connection: "close" }).end();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (!((req.method === "GET" && req.url === "/check") ||
|
|
185
|
+
(req.method === "POST" && req.url === "/restart"))) {
|
|
186
|
+
res.writeHead(404).end();
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
req.resume();
|
|
190
|
+
void operate(req.method === "POST")
|
|
191
|
+
.then((outcome) => {
|
|
192
|
+
if (res.destroyed || res.writableEnded) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
res.writeHead(outcome.ok ? 200 : 409, {
|
|
196
|
+
"content-type": "application/json",
|
|
197
|
+
connection: "close",
|
|
198
|
+
});
|
|
199
|
+
res.end(JSON.stringify(outcome));
|
|
200
|
+
})
|
|
201
|
+
.catch(() => {
|
|
202
|
+
if (res.destroyed || res.writableEnded) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
res.writeHead(500).end();
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
control.headersTimeout = 5_000;
|
|
209
|
+
control.requestTimeout = 5_000;
|
|
210
|
+
control.maxHeadersCount = 10;
|
|
211
|
+
control.maxConnections = 16;
|
|
212
|
+
await new Promise((resolve, reject) => {
|
|
213
|
+
control.once("error", reject);
|
|
214
|
+
control.listen(socketPath, () => {
|
|
215
|
+
control.off("error", reject);
|
|
216
|
+
// Keep handling errors after binding: a control-plane accept failure
|
|
217
|
+
// must not become an unhandled event that stops the serving listener.
|
|
218
|
+
control.on("error", (error) => {
|
|
219
|
+
options.log?.(`[proxy-supervisor] safe restart control error: ${error.message}`);
|
|
220
|
+
});
|
|
221
|
+
resolve();
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
try {
|
|
225
|
+
await chmod(socketPath, 0o600);
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
control.close();
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
231
|
+
return {
|
|
232
|
+
identity: { protocol: 1, socketPath, instanceId },
|
|
233
|
+
close: async () => {
|
|
234
|
+
closing = true;
|
|
235
|
+
control.closeAllConnections();
|
|
236
|
+
await new Promise((resolve) => control.close(() => resolve()));
|
|
237
|
+
await rm(socketPath, { force: true });
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
/** Perform one local control operation; never fall back to killing a process. */
|
|
242
|
+
export async function requestProxyRestart(identity, check) {
|
|
243
|
+
return new Promise((resolve, reject) => {
|
|
244
|
+
const req = request({
|
|
245
|
+
socketPath: identity.socketPath,
|
|
246
|
+
path: check ? "/check" : "/restart",
|
|
247
|
+
method: check ? "GET" : "POST",
|
|
248
|
+
headers: {
|
|
249
|
+
"x-neurolink-instance": identity.instanceId,
|
|
250
|
+
connection: "close",
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
const timeout = setTimeout(() => req.destroy(new Error("Restart result timed out; outcome is unknown. Inspect proxy status before retrying.")), check ? 15_000 : 150_000);
|
|
254
|
+
req.once("error", (error) => {
|
|
255
|
+
clearTimeout(timeout);
|
|
256
|
+
reject(error);
|
|
257
|
+
});
|
|
258
|
+
req.once("response", (res) => {
|
|
259
|
+
let body = "";
|
|
260
|
+
res.setEncoding("utf8");
|
|
261
|
+
res.on("data", (chunk) => {
|
|
262
|
+
body += chunk;
|
|
263
|
+
if (body.length > 65_536) {
|
|
264
|
+
req.destroy(new Error("Invalid restart control response."));
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
res.once("error", (error) => {
|
|
268
|
+
clearTimeout(timeout);
|
|
269
|
+
reject(error);
|
|
270
|
+
});
|
|
271
|
+
res.once("end", () => {
|
|
272
|
+
clearTimeout(timeout);
|
|
273
|
+
try {
|
|
274
|
+
resolve(resultSchema.parse(JSON.parse(body)));
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
reject(new Error("Restart control did not return a verified result; no service restart fallback was attempted."));
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
req.end();
|
|
282
|
+
});
|
|
283
|
+
}
|
|
@@ -253,6 +253,14 @@ export class RollingWorkerSupervisor {
|
|
|
253
253
|
if (this.candidate?.generation === generation) {
|
|
254
254
|
this.candidate = null;
|
|
255
255
|
}
|
|
256
|
+
// A candidate that never became ready may also ignore SIGTERM.
|
|
257
|
+
// Bound its cleanup independently of the still-serving generation.
|
|
258
|
+
const killTimeout = setTimeout(() => handle.terminate("SIGKILL"), 1_000);
|
|
259
|
+
killTimeout.unref?.();
|
|
260
|
+
const offCandidateExit = handle.onExit(() => {
|
|
261
|
+
clearTimeout(killTimeout);
|
|
262
|
+
offCandidateExit();
|
|
263
|
+
});
|
|
256
264
|
handle.terminate("SIGTERM");
|
|
257
265
|
dispose();
|
|
258
266
|
this.publishState();
|