@juspay/neurolink 12.20.0 → 12.20.2
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 -6
- package/dist/browser/neurolink.min.js +400 -400
- package/dist/cli/commands/proxy.js +171 -25
- package/dist/cli/commands/telemetry.d.ts +4 -1
- package/dist/cli/commands/telemetry.js +113 -0
- package/dist/cli/utils/nativeTelemetry.d.ts +19 -0
- package/dist/cli/utils/nativeTelemetry.js +322 -0
- package/dist/proxy/bodyCapturePolicy.d.ts +6 -0
- package/dist/proxy/bodyCapturePolicy.js +132 -0
- package/dist/proxy/codexFallback.d.ts +27 -9
- package/dist/proxy/codexFallback.js +84 -13
- package/dist/proxy/openaiFormat.js +8 -1
- package/dist/proxy/otelLogSink.d.ts +11 -1
- package/dist/proxy/otelLogSink.js +174 -27
- package/dist/proxy/otlpLogTransport.d.ts +4 -2
- package/dist/proxy/otlpLogTransport.js +22 -7
- package/dist/proxy/requestLogger.js +11 -3
- package/dist/proxy/restartControl.js +18 -3
- package/dist/proxy/rollingProxyServer.js +48 -1
- package/dist/proxy/rollingWorkerProcess.js +87 -10
- package/dist/proxy/rollingWorkerProtocol.js +4 -2
- package/dist/proxy/rollingWorkerSupervisor.js +47 -10
- package/dist/proxy/socketWorkerRuntime.js +90 -4
- package/dist/proxy/updateChecker.d.ts +3 -3
- package/dist/proxy/updateChecker.js +16 -9
- package/dist/proxy/updateCoordinator.d.ts +10 -1
- package/dist/proxy/updateCoordinator.js +68 -1
- package/dist/proxy/updateState.d.ts +2 -0
- package/dist/proxy/updateState.js +15 -0
- package/dist/proxy/vertexAnthropicFallback.d.ts +17 -1
- package/dist/proxy/vertexAnthropicFallback.js +25 -1
- package/dist/server/routes/claudeProxyRoutes.d.ts +17 -0
- package/dist/server/routes/claudeProxyRoutes.js +53 -5
- package/dist/types/cli.d.ts +18 -0
- package/dist/types/providers.d.ts +8 -0
- package/dist/types/proxy.d.ts +117 -7
- package/dist/utils/anthropicCacheBreakpoints.d.ts +22 -1
- package/dist/utils/anthropicCacheBreakpoints.js +113 -0
- package/dist/utils/pricing.js +69 -2
- package/docs-site/static/search-index.json +1 -1
- package/package.json +1 -1
- package/scripts/observability/NATIVE-DURABLE.md +68 -0
- package/scripts/observability/otel-collector.proxy-native-durable.yaml +2 -2
- package/scripts/observability/proxy-telemetry-check.mjs +135 -50
|
@@ -9,6 +9,8 @@ import { isProxyWorkerStatusMessage, PROXY_SOCKET_WORKER_ENV, PROXY_SOCKET_OFFER
|
|
|
9
9
|
export function spawnProxySocketWorker(options) {
|
|
10
10
|
const socketAckTimeoutMs = Math.max(1, options.socketAckTimeoutMs ?? 30_000);
|
|
11
11
|
let nextSocketId = 0;
|
|
12
|
+
let controlBeforeHandle = false;
|
|
13
|
+
let workerCommitAcknowledgement = false;
|
|
12
14
|
const pendingSockets = new Map();
|
|
13
15
|
const statusListeners = new Set();
|
|
14
16
|
const pendingStatusMessages = [];
|
|
@@ -124,6 +126,26 @@ export function spawnProxySocketWorker(options) {
|
|
|
124
126
|
* the commit phase.
|
|
125
127
|
*/
|
|
126
128
|
const onInternalMessage = (message) => {
|
|
129
|
+
if (isProxyWorkerStatusMessage(message) &&
|
|
130
|
+
message.type === "proxy-worker:ready" &&
|
|
131
|
+
message.generation === options.generation &&
|
|
132
|
+
message.pid === childPid) {
|
|
133
|
+
controlBeforeHandle =
|
|
134
|
+
message.socketOfferProtocol === "control-before-handle";
|
|
135
|
+
workerCommitAcknowledgement =
|
|
136
|
+
message.socketCommitProtocol === "worker-ack";
|
|
137
|
+
}
|
|
138
|
+
if (isProxyWorkerStatusMessage(message) &&
|
|
139
|
+
message.type === "proxy-worker:socket-committed" &&
|
|
140
|
+
message.generation === options.generation &&
|
|
141
|
+
message.pid === childPid) {
|
|
142
|
+
const pending = pendingSockets.get(message.socketId);
|
|
143
|
+
if (!pending || !pending.accepted || !pending.descriptorSent) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
settleSocket(message.socketId);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
127
149
|
if (isProxyWorkerStatusMessage(message) &&
|
|
128
150
|
message.type === "proxy-worker:socket-accepted" &&
|
|
129
151
|
message.generation === options.generation &&
|
|
@@ -132,22 +154,56 @@ export function spawnProxySocketWorker(options) {
|
|
|
132
154
|
if (!pending || pending.accepted) {
|
|
133
155
|
return;
|
|
134
156
|
}
|
|
157
|
+
if (Date.now() >= pending.deadlineAt) {
|
|
158
|
+
const error = Object.assign(new Error("Socket admission deadline expired before commit"), {
|
|
159
|
+
code: PROXY_SOCKET_OFFER_TIMEOUT,
|
|
160
|
+
socketNeverTransferred: !pending.descriptorSent,
|
|
161
|
+
});
|
|
162
|
+
settleSocket(message.socketId, error);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const commitBudgetMs = Math.min(socketAckTimeoutMs, pending.deadlineAt - Date.now());
|
|
166
|
+
const minimumCommitWindowMs = Math.min(socketAckTimeoutMs, 1_000);
|
|
167
|
+
if (commitBudgetMs < minimumCommitWindowMs) {
|
|
168
|
+
settleSocket(message.socketId, Object.assign(new Error("Socket admission deadline too close for commit"), {
|
|
169
|
+
code: PROXY_SOCKET_OFFER_TIMEOUT,
|
|
170
|
+
socketNeverTransferred: !pending.descriptorSent,
|
|
171
|
+
}));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
135
174
|
pending.accepted = true;
|
|
136
175
|
// Acceptance and commit are distinct phases. A late acceptance must not
|
|
137
176
|
// inherit an almost-expired offer timer and kill established streams.
|
|
138
177
|
clearTimeout(pending.timeout);
|
|
139
178
|
pending.timeout = setTimeout(() => {
|
|
140
|
-
const error = new Error(`proxy worker ${childPid} socket commit remained pending for ${
|
|
179
|
+
const error = new Error(`proxy worker ${childPid} socket commit remained pending for ${commitBudgetMs}ms`);
|
|
141
180
|
error.code = PROXY_SOCKET_COMMIT_TIMEOUT;
|
|
142
181
|
settleSocket(message.socketId, error);
|
|
143
|
-
},
|
|
182
|
+
}, commitBudgetMs);
|
|
144
183
|
pending.timeout.unref?.();
|
|
145
184
|
try {
|
|
146
|
-
|
|
185
|
+
const commit = {
|
|
147
186
|
type: "proxy-worker:socket-commit",
|
|
148
187
|
generation: options.generation,
|
|
149
188
|
socketId: message.socketId,
|
|
150
|
-
}
|
|
189
|
+
};
|
|
190
|
+
if (!pending.descriptorSent) {
|
|
191
|
+
// Mark ambiguous before calling IPC: even a thrown/send callback error
|
|
192
|
+
// cannot prove the descriptor was not delivered and consumed.
|
|
193
|
+
pending.descriptorSent = true;
|
|
194
|
+
child.send(commit, pending.socket, { keepOpen: true }, (error) => {
|
|
195
|
+
if (error || !workerCommitAcknowledgement) {
|
|
196
|
+
settleSocket(message.socketId, error ?? undefined);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
child.send(commit, (error) => {
|
|
202
|
+
if (error || !workerCommitAcknowledgement) {
|
|
203
|
+
settleSocket(message.socketId, error ?? undefined);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
}
|
|
151
207
|
}
|
|
152
208
|
catch (error) {
|
|
153
209
|
settleSocket(message.socketId, error instanceof Error ? error : new Error(String(error)));
|
|
@@ -208,8 +264,17 @@ export function spawnProxySocketWorker(options) {
|
|
|
208
264
|
};
|
|
209
265
|
return {
|
|
210
266
|
pid: childPid,
|
|
267
|
+
socketTransferTimeoutMs: 2 * socketAckTimeoutMs,
|
|
211
268
|
sendControl,
|
|
212
|
-
sendSocket: (generation, socket, callback) => {
|
|
269
|
+
sendSocket: (generation, socket, callback, deadlineAt = Date.now() + 2 * socketAckTimeoutMs) => {
|
|
270
|
+
if (Date.now() >= deadlineAt) {
|
|
271
|
+
const error = Object.assign(new Error("Socket admission deadline expired before offer"), {
|
|
272
|
+
code: PROXY_SOCKET_OFFER_TIMEOUT,
|
|
273
|
+
socketNeverTransferred: true,
|
|
274
|
+
});
|
|
275
|
+
callback(error);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
213
278
|
if (budgetResponseFailure) {
|
|
214
279
|
callback(budgetResponseFailure);
|
|
215
280
|
return;
|
|
@@ -220,23 +285,35 @@ export function spawnProxySocketWorker(options) {
|
|
|
220
285
|
}
|
|
221
286
|
const socketId = `${generation}:${++nextSocketId}`;
|
|
222
287
|
const timeout = setTimeout(() => {
|
|
223
|
-
const error = new Error(`proxy worker ${childPid} did not accept socket within ${socketAckTimeoutMs}ms`)
|
|
224
|
-
|
|
288
|
+
const error = Object.assign(new Error(`proxy worker ${childPid} did not accept socket within ${socketAckTimeoutMs}ms`), {
|
|
289
|
+
code: PROXY_SOCKET_OFFER_TIMEOUT,
|
|
290
|
+
socketNeverTransferred: pendingSockets.get(socketId)?.descriptorSent === false,
|
|
291
|
+
});
|
|
225
292
|
settleSocket(socketId, error);
|
|
226
|
-
}, socketAckTimeoutMs);
|
|
293
|
+
}, Math.max(0, Math.min(socketAckTimeoutMs, deadlineAt - Date.now())));
|
|
227
294
|
timeout.unref?.();
|
|
228
295
|
pendingSockets.set(socketId, {
|
|
229
296
|
socket,
|
|
230
297
|
callback,
|
|
231
298
|
timeout,
|
|
232
299
|
accepted: false,
|
|
300
|
+
deadlineAt,
|
|
301
|
+
descriptorSent: !controlBeforeHandle,
|
|
233
302
|
});
|
|
234
303
|
try {
|
|
235
|
-
|
|
304
|
+
const onSent = (error) => {
|
|
236
305
|
if (error) {
|
|
237
306
|
settleSocket(socketId, error);
|
|
238
307
|
}
|
|
239
|
-
}
|
|
308
|
+
};
|
|
309
|
+
if (controlBeforeHandle) {
|
|
310
|
+
child.send({ type: "proxy-worker:socket-offer", generation, socketId }, onSent);
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
// Old workers require descriptor-first offers. They may buffer input
|
|
314
|
+
// before commit, so their timed-out offers are never safe to retry.
|
|
315
|
+
child.send({ type: "proxy-worker:socket", generation, socketId }, socket, { keepOpen: true }, onSent);
|
|
316
|
+
}
|
|
240
317
|
}
|
|
241
318
|
catch (error) {
|
|
242
319
|
settleSocket(socketId, error instanceof Error ? error : new Error(String(error)));
|
|
@@ -13,7 +13,8 @@ export function isProxyWorkerControlMessage(value) {
|
|
|
13
13
|
Number(message.generation) <= 0) {
|
|
14
14
|
return false;
|
|
15
15
|
}
|
|
16
|
-
if (message.type === "proxy-worker:socket-
|
|
16
|
+
if (message.type === "proxy-worker:socket-offer" ||
|
|
17
|
+
message.type === "proxy-worker:socket-commit" ||
|
|
17
18
|
message.type === "proxy-worker:socket-cancel") {
|
|
18
19
|
return typeof message.socketId === "string" && message.socketId.length > 0;
|
|
19
20
|
}
|
|
@@ -48,7 +49,8 @@ export function isProxyWorkerStatusMessage(value) {
|
|
|
48
49
|
message.type === "proxy-worker:drained") {
|
|
49
50
|
return true;
|
|
50
51
|
}
|
|
51
|
-
if (message.type === "proxy-worker:socket-accepted"
|
|
52
|
+
if (message.type === "proxy-worker:socket-accepted" ||
|
|
53
|
+
message.type === "proxy-worker:socket-committed") {
|
|
52
54
|
return typeof message.socketId === "string" && message.socketId.length > 0;
|
|
53
55
|
}
|
|
54
56
|
if (message.type === "proxy-worker:replacement-requested") {
|
|
@@ -4,6 +4,7 @@ const DEFAULT_READY_TIMEOUT_MS = 120_000;
|
|
|
4
4
|
const DEFAULT_SOCKET_QUEUE_LIMIT = 1_024;
|
|
5
5
|
const DEFAULT_SOCKET_QUEUE_TIMEOUT_MS = 30_000;
|
|
6
6
|
const DEFAULT_SHUTDOWN_TIMEOUT_MS = 30_000;
|
|
7
|
+
const DEFAULT_SOCKET_TRANSFER_TIMEOUT_MS = 60_000;
|
|
7
8
|
const MAX_RECENT_SUPERVISOR_EVENTS = 100;
|
|
8
9
|
/**
|
|
9
10
|
* Owns worker generations while the caller owns the public listening socket.
|
|
@@ -105,33 +106,44 @@ export class RollingWorkerSupervisor {
|
|
|
105
106
|
this.rejectSocket(socket, undefined, undefined, "supervisor_closed");
|
|
106
107
|
return;
|
|
107
108
|
}
|
|
109
|
+
const admission = {
|
|
110
|
+
socket,
|
|
111
|
+
// Preserve the original offer + commit allowance. Queued sockets also
|
|
112
|
+
// retain their existing queue wait, but no retry ever renews this budget.
|
|
113
|
+
deadlineAt: Date.now() +
|
|
114
|
+
(this.active?.handle.socketTransferTimeoutMs ??
|
|
115
|
+
DEFAULT_SOCKET_TRANSFER_TIMEOUT_MS),
|
|
116
|
+
retried: false,
|
|
117
|
+
};
|
|
108
118
|
if (this.active &&
|
|
109
119
|
this.queuedSockets.length === 0 &&
|
|
110
120
|
this.active.pendingTransfers < this.options.maxPendingTransfers) {
|
|
111
|
-
this.transferSocket(this.active,
|
|
121
|
+
this.transferSocket(this.active, admission);
|
|
112
122
|
return;
|
|
113
123
|
}
|
|
114
|
-
this.
|
|
124
|
+
admission.deadlineAt += this.options.socketQueueTimeoutMs;
|
|
125
|
+
this.queueSocket(admission);
|
|
115
126
|
this.flushQueuedSockets();
|
|
116
127
|
}
|
|
117
128
|
/**
|
|
118
129
|
* Retain a socket only within the queue capacity and deadline, recording
|
|
119
130
|
* classified rejection.
|
|
120
131
|
*/
|
|
121
|
-
queueSocket(
|
|
132
|
+
queueSocket(admission) {
|
|
133
|
+
const { socket } = admission;
|
|
122
134
|
if (this.queuedSockets.length >= this.options.socketQueueLimit) {
|
|
123
135
|
this.rejectSocket(socket, undefined, undefined, "queue_capacity");
|
|
124
136
|
return;
|
|
125
137
|
}
|
|
126
138
|
const queued = {
|
|
127
|
-
|
|
139
|
+
...admission,
|
|
128
140
|
timeout: setTimeout(() => {
|
|
129
141
|
const index = this.queuedSockets.indexOf(queued);
|
|
130
142
|
if (index >= 0) {
|
|
131
143
|
this.queuedSockets.splice(index, 1);
|
|
132
144
|
this.rejectSocket(socket, undefined, undefined, "queue_timeout");
|
|
133
145
|
}
|
|
134
|
-
}, this.options.socketQueueTimeoutMs),
|
|
146
|
+
}, Math.max(0, Math.min(this.options.socketQueueTimeoutMs, admission.deadlineAt - Date.now()))),
|
|
135
147
|
};
|
|
136
148
|
queued.timeout.unref?.();
|
|
137
149
|
this.queuedSockets.push(queued);
|
|
@@ -432,14 +444,19 @@ export class RollingWorkerSupervisor {
|
|
|
432
444
|
break;
|
|
433
445
|
}
|
|
434
446
|
clearTimeout(queued.timeout);
|
|
435
|
-
this.transferSocket(this.active, queued
|
|
447
|
+
this.transferSocket(this.active, queued);
|
|
436
448
|
}
|
|
437
449
|
}
|
|
438
450
|
finally {
|
|
439
451
|
this.flushingSockets = false;
|
|
440
452
|
}
|
|
441
453
|
}
|
|
442
|
-
transferSocket(worker,
|
|
454
|
+
transferSocket(worker, admission) {
|
|
455
|
+
const { socket } = admission;
|
|
456
|
+
if (Date.now() >= admission.deadlineAt) {
|
|
457
|
+
this.rejectSocket(socket, worker.generation, worker.version, "admission_timeout");
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
443
460
|
worker.pendingTransfers += 1;
|
|
444
461
|
this.scheduleTransferState();
|
|
445
462
|
let settled = false;
|
|
@@ -450,7 +467,7 @@ export class RollingWorkerSupervisor {
|
|
|
450
467
|
settled = true;
|
|
451
468
|
worker.pendingTransfers = Math.max(0, worker.pendingTransfers - 1);
|
|
452
469
|
if (error) {
|
|
453
|
-
this.handleTransferFailure(worker,
|
|
470
|
+
this.handleTransferFailure(worker, admission, error);
|
|
454
471
|
}
|
|
455
472
|
else if (this.active?.generation === worker.generation) {
|
|
456
473
|
this.consecutiveOfferTimeouts = 0;
|
|
@@ -460,7 +477,7 @@ export class RollingWorkerSupervisor {
|
|
|
460
477
|
this.scheduleTransferState();
|
|
461
478
|
};
|
|
462
479
|
try {
|
|
463
|
-
worker.handle.sendSocket(worker.generation, socket, complete);
|
|
480
|
+
worker.handle.sendSocket(worker.generation, socket, complete, admission.deadlineAt);
|
|
464
481
|
}
|
|
465
482
|
catch (error) {
|
|
466
483
|
complete(error instanceof Error ? error : new Error(String(error)));
|
|
@@ -487,7 +504,8 @@ export class RollingWorkerSupervisor {
|
|
|
487
504
|
* Cancel the affected handoff and request bounded replacement without
|
|
488
505
|
* killing unrelated streams.
|
|
489
506
|
*/
|
|
490
|
-
handleTransferFailure(worker,
|
|
507
|
+
handleTransferFailure(worker, admission, error) {
|
|
508
|
+
const { socket } = admission;
|
|
491
509
|
this.failedTransfers += 1;
|
|
492
510
|
const detail = this.describeTransferError(error);
|
|
493
511
|
this.recordEvent({
|
|
@@ -540,6 +558,25 @@ export class RollingWorkerSupervisor {
|
|
|
540
558
|
// A failed handoff is not evidence that every connection on this worker
|
|
541
559
|
// failed. Cancel only the affected socket and activate a replacement before
|
|
542
560
|
// draining existing streams. Actual exits are handled by onExit below.
|
|
561
|
+
const replacement = this.active;
|
|
562
|
+
if (cancelledOffer &&
|
|
563
|
+
error instanceof Error &&
|
|
564
|
+
"socketNeverTransferred" in error &&
|
|
565
|
+
error.socketNeverTransferred === true &&
|
|
566
|
+
!lifecycle.observedExit &&
|
|
567
|
+
!admission.retried &&
|
|
568
|
+
!this.closed &&
|
|
569
|
+
replacement &&
|
|
570
|
+
replacement.generation !== worker.generation &&
|
|
571
|
+
replacement.pendingTransfers < this.options.maxPendingTransfers &&
|
|
572
|
+
Date.now() < admission.deadlineAt) {
|
|
573
|
+
// Only negotiated control-only offers retain every byte on this parent.
|
|
574
|
+
// Legacy descriptor offers can buffer bytes before commit and never retry.
|
|
575
|
+
// The removed IPC entry also prevents a late acceptance issuing a commit.
|
|
576
|
+
// Every other failure is ambiguous and must never replay request bytes.
|
|
577
|
+
this.transferSocket(replacement, { ...admission, retried: true });
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
543
580
|
this.rejectSocket(socket, worker.generation, worker.version, "transfer_failure");
|
|
544
581
|
}
|
|
545
582
|
/**
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isProxyWorkerControlMessage } from "./rollingWorkerProtocol.js";
|
|
2
|
+
const DEFAULT_FIRST_REQUEST_GRACE_MS = 30_000;
|
|
2
3
|
function isTransferableProxySocket(handle) {
|
|
3
4
|
if (!handle || typeof handle !== "object") {
|
|
4
5
|
return false;
|
|
@@ -32,8 +33,33 @@ export function createSocketWorkerRuntime(server, options) {
|
|
|
32
33
|
const sockets = new Set();
|
|
33
34
|
const activeBySocket = new Map();
|
|
34
35
|
const activeResponses = new Set();
|
|
36
|
+
const awaitingFirstRequest = new Set();
|
|
37
|
+
const firstRequestTimers = new Map();
|
|
38
|
+
const firstRequestGraceMs = Math.max(1, options?.firstRequestGraceMs ?? DEFAULT_FIRST_REQUEST_GRACE_MS);
|
|
35
39
|
let draining = false;
|
|
36
40
|
let drained = false;
|
|
41
|
+
const clearFirstRequestWait = (socket) => {
|
|
42
|
+
awaitingFirstRequest.delete(socket);
|
|
43
|
+
const timer = firstRequestTimers.get(socket);
|
|
44
|
+
if (timer) {
|
|
45
|
+
clearTimeout(timer);
|
|
46
|
+
firstRequestTimers.delete(socket);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
const boundFirstRequestWait = (socket) => {
|
|
50
|
+
if (firstRequestTimers.has(socket)) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const timer = setTimeout(() => {
|
|
54
|
+
firstRequestTimers.delete(socket);
|
|
55
|
+
awaitingFirstRequest.delete(socket);
|
|
56
|
+
if (draining && sockets.has(socket) && !activeBySocket.has(socket)) {
|
|
57
|
+
socket.end();
|
|
58
|
+
}
|
|
59
|
+
}, firstRequestGraceMs);
|
|
60
|
+
timer.unref?.();
|
|
61
|
+
firstRequestTimers.set(socket, timer);
|
|
62
|
+
};
|
|
37
63
|
const maybeFinishDrain = () => {
|
|
38
64
|
if (draining && !drained && sockets.size === 0) {
|
|
39
65
|
drained = true;
|
|
@@ -42,6 +68,7 @@ export function createSocketWorkerRuntime(server, options) {
|
|
|
42
68
|
};
|
|
43
69
|
const requestStarted = (request, response) => {
|
|
44
70
|
const socket = request.socket;
|
|
71
|
+
clearFirstRequestWait(socket);
|
|
45
72
|
activeBySocket.set(socket, (activeBySocket.get(socket) ?? 0) + 1);
|
|
46
73
|
activeResponses.add(response);
|
|
47
74
|
if (draining) {
|
|
@@ -75,7 +102,9 @@ export function createSocketWorkerRuntime(server, options) {
|
|
|
75
102
|
return;
|
|
76
103
|
}
|
|
77
104
|
sockets.add(socket);
|
|
105
|
+
awaitingFirstRequest.add(socket);
|
|
78
106
|
socket.once("close", () => {
|
|
107
|
+
clearFirstRequestWait(socket);
|
|
79
108
|
sockets.delete(socket);
|
|
80
109
|
activeBySocket.delete(socket);
|
|
81
110
|
maybeFinishDrain();
|
|
@@ -92,7 +121,15 @@ export function createSocketWorkerRuntime(server, options) {
|
|
|
92
121
|
response.shouldKeepAlive = false;
|
|
93
122
|
}
|
|
94
123
|
for (const socket of sockets) {
|
|
95
|
-
if (
|
|
124
|
+
if (awaitingFirstRequest.has(socket)) {
|
|
125
|
+
// IPC commit confirms descriptor delivery, not HTTP parser admission.
|
|
126
|
+
// Under host pressure a drain can otherwise end the socket before its
|
|
127
|
+
// first buffered request reaches the server. Preserve it for one
|
|
128
|
+
// bounded header-start window; the worker supervisor remains the outer
|
|
129
|
+
// hard shutdown bound.
|
|
130
|
+
boundFirstRequestWait(socket);
|
|
131
|
+
}
|
|
132
|
+
else if (!activeBySocket.has(socket)) {
|
|
96
133
|
socket.end();
|
|
97
134
|
}
|
|
98
135
|
}
|
|
@@ -103,6 +140,11 @@ export function createSocketWorkerRuntime(server, options) {
|
|
|
103
140
|
drain,
|
|
104
141
|
close: () => {
|
|
105
142
|
server.off("request", requestStarted);
|
|
143
|
+
for (const timer of firstRequestTimers.values()) {
|
|
144
|
+
clearTimeout(timer);
|
|
145
|
+
}
|
|
146
|
+
firstRequestTimers.clear();
|
|
147
|
+
awaitingFirstRequest.clear();
|
|
106
148
|
for (const socket of sockets) {
|
|
107
149
|
socket.destroy();
|
|
108
150
|
}
|
|
@@ -174,6 +216,31 @@ export function attachSocketWorkerProcess(server, input) {
|
|
|
174
216
|
};
|
|
175
217
|
/** Apply supervisor messages while retaining ownership of pending and committed sockets. */
|
|
176
218
|
const onMessage = (message, handle) => {
|
|
219
|
+
if (handle !== undefined &&
|
|
220
|
+
isProxyWorkerControlMessage(message) &&
|
|
221
|
+
message.type === "proxy-worker:socket-commit" &&
|
|
222
|
+
message.generation === input.generation) {
|
|
223
|
+
// New supervisors first offer only a socket ID. The descriptor arrives
|
|
224
|
+
// here at commit, so an abandoned offer cannot consume any client bytes.
|
|
225
|
+
if (!activated ||
|
|
226
|
+
gracefulDrain ||
|
|
227
|
+
!isTransferableProxySocket(handle) ||
|
|
228
|
+
pendingSockets.has(message.socketId) ||
|
|
229
|
+
committedSockets.has(message.socketId)) {
|
|
230
|
+
destroyTransferredHandle(handle);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
committedSockets.set(message.socketId, handle);
|
|
234
|
+
handle.once("close", () => committedSockets.delete(message.socketId));
|
|
235
|
+
runtime.acceptSocket(handle);
|
|
236
|
+
send({
|
|
237
|
+
type: "proxy-worker:socket-committed",
|
|
238
|
+
generation: input.generation,
|
|
239
|
+
pid: process.pid,
|
|
240
|
+
socketId: message.socketId,
|
|
241
|
+
});
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
177
244
|
if (message &&
|
|
178
245
|
typeof message === "object" &&
|
|
179
246
|
message.type === "proxy-worker:socket") {
|
|
@@ -245,7 +312,17 @@ export function attachSocketWorkerProcess(server, input) {
|
|
|
245
312
|
}
|
|
246
313
|
if (isProxyWorkerControlMessage(message) &&
|
|
247
314
|
message.generation === input.generation) {
|
|
248
|
-
if (message.type === "proxy-worker:
|
|
315
|
+
if (message.type === "proxy-worker:socket-offer") {
|
|
316
|
+
if (activated && !gracefulDrain && process.connected) {
|
|
317
|
+
send({
|
|
318
|
+
type: "proxy-worker:socket-accepted",
|
|
319
|
+
generation: input.generation,
|
|
320
|
+
pid: process.pid,
|
|
321
|
+
socketId: message.socketId,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
else if (message.type === "proxy-worker:activate") {
|
|
249
326
|
if (!activated) {
|
|
250
327
|
try {
|
|
251
328
|
input.onActivated?.();
|
|
@@ -280,6 +357,12 @@ export function attachSocketWorkerProcess(server, input) {
|
|
|
280
357
|
committedSockets.set(message.socketId, socket);
|
|
281
358
|
socket.once("close", () => committedSockets.delete(message.socketId));
|
|
282
359
|
runtime.acceptSocket(socket);
|
|
360
|
+
send({
|
|
361
|
+
type: "proxy-worker:socket-committed",
|
|
362
|
+
generation: input.generation,
|
|
363
|
+
pid: process.pid,
|
|
364
|
+
socketId: message.socketId,
|
|
365
|
+
});
|
|
283
366
|
}
|
|
284
367
|
else {
|
|
285
368
|
committedSockets.delete(message.socketId);
|
|
@@ -300,8 +383,9 @@ export function attachSocketWorkerProcess(server, input) {
|
|
|
300
383
|
// Terminal shutdown path: the parent IPC channel is gone (disconnect) or the
|
|
301
384
|
// process is being killed (SIGTERM/SIGINT). Unlike the graceful rolling drain
|
|
302
385
|
// above, pending offers can no longer be committed, so they are destroyed and
|
|
303
|
-
// the drain
|
|
304
|
-
//
|
|
386
|
+
// the bounded runtime drain starts immediately. A committed socket may still
|
|
387
|
+
// use its first-request grace; the supervisor remains the outer hard shutdown
|
|
388
|
+
// bound. The zero-downtime guarantee applies to the rolling handoff
|
|
305
389
|
// (control-message) path, not to process termination.
|
|
306
390
|
const drain = () => {
|
|
307
391
|
for (const socketId of [...pendingSockets.keys()]) {
|
|
@@ -320,6 +404,8 @@ export function attachSocketWorkerProcess(server, input) {
|
|
|
320
404
|
pid: process.pid,
|
|
321
405
|
version: input.version,
|
|
322
406
|
processInstanceId: input.processInstanceId,
|
|
407
|
+
socketOfferProtocol: "control-before-handle",
|
|
408
|
+
socketCommitProtocol: "worker-ack",
|
|
323
409
|
});
|
|
324
410
|
return {
|
|
325
411
|
...runtime,
|
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
* Queries the npm registry for the latest published version of
|
|
5
5
|
* `@juspay/neurolink` and compares it against the currently running version.
|
|
6
6
|
* Designed to be non-blocking and failure-tolerant — any error (network,
|
|
7
|
-
* timeout, parse)
|
|
7
|
+
* timeout, parse) returns an explicit failed check without inventing a release.
|
|
8
8
|
*/
|
|
9
9
|
import type { UpdateCheckResult } from "../types/index.js";
|
|
10
10
|
/**
|
|
11
11
|
* Query npm for the latest version of `@juspay/neurolink` and compare it
|
|
12
12
|
* against {@link currentVersion}.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* Failure retains the legacy version fallback but marks it unverified. Callers
|
|
15
|
+
* must not persist it as a successful registry observation.
|
|
16
16
|
*/
|
|
17
17
|
export declare function checkForUpdate(currentVersion: string): Promise<UpdateCheckResult>;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Queries the npm registry for the latest published version of
|
|
5
5
|
* `@juspay/neurolink` and compares it against the currently running version.
|
|
6
6
|
* Designed to be non-blocking and failure-tolerant — any error (network,
|
|
7
|
-
* timeout, parse)
|
|
7
|
+
* timeout, parse) returns an explicit failed check without inventing a release.
|
|
8
8
|
*/
|
|
9
9
|
import { execFile as execFileCb } from "node:child_process";
|
|
10
10
|
import { promisify } from "node:util";
|
|
@@ -54,15 +54,17 @@ function isNewerVersion(current, latest) {
|
|
|
54
54
|
* Query npm for the latest version of `@juspay/neurolink` and compare it
|
|
55
55
|
* against {@link currentVersion}.
|
|
56
56
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
57
|
+
* Failure retains the legacy version fallback but marks it unverified. Callers
|
|
58
|
+
* must not persist it as a successful registry observation.
|
|
59
59
|
*/
|
|
60
60
|
export async function checkForUpdate(currentVersion) {
|
|
61
|
-
const fail = {
|
|
61
|
+
const fail = (checkError) => ({
|
|
62
62
|
currentVersion,
|
|
63
63
|
latestVersion: currentVersion,
|
|
64
64
|
updateAvailable: false,
|
|
65
|
-
|
|
65
|
+
checkSucceeded: false,
|
|
66
|
+
checkError,
|
|
67
|
+
});
|
|
66
68
|
try {
|
|
67
69
|
logger.debug("[UpdateChecker] Checking for updates", { currentVersion });
|
|
68
70
|
const { stdout } = await execFile("npm", ["view", "@juspay/neurolink", "version", "--json"], { timeout: NPM_VIEW_TIMEOUT_MS });
|
|
@@ -72,14 +74,14 @@ export async function checkForUpdate(currentVersion) {
|
|
|
72
74
|
logger.warn("[UpdateChecker] Unexpected npm output type", {
|
|
73
75
|
type: typeof parsed,
|
|
74
76
|
});
|
|
75
|
-
return fail;
|
|
77
|
+
return fail("Registry returned a non-string version");
|
|
76
78
|
}
|
|
77
79
|
const latestVersion = parsed.trim();
|
|
78
80
|
if (!parseSemVer(latestVersion)) {
|
|
79
81
|
logger.warn("[UpdateChecker] Failed to parse latest version", {
|
|
80
82
|
latestVersion,
|
|
81
83
|
});
|
|
82
|
-
return fail;
|
|
84
|
+
return fail("Registry returned an invalid version");
|
|
83
85
|
}
|
|
84
86
|
const updateAvailable = isNewerVersion(currentVersion, latestVersion);
|
|
85
87
|
logger.debug("[UpdateChecker] Version check complete", {
|
|
@@ -87,11 +89,16 @@ export async function checkForUpdate(currentVersion) {
|
|
|
87
89
|
latestVersion,
|
|
88
90
|
updateAvailable,
|
|
89
91
|
});
|
|
90
|
-
return {
|
|
92
|
+
return {
|
|
93
|
+
currentVersion,
|
|
94
|
+
latestVersion,
|
|
95
|
+
updateAvailable,
|
|
96
|
+
checkSucceeded: true,
|
|
97
|
+
};
|
|
91
98
|
}
|
|
92
99
|
catch (error) {
|
|
93
100
|
const message = error instanceof Error ? error.message : String(error);
|
|
94
101
|
logger.warn("[UpdateChecker] Update check failed", { error: message });
|
|
95
|
-
return fail;
|
|
102
|
+
return fail("Registry version check failed; see updater diagnostics");
|
|
96
103
|
}
|
|
97
104
|
}
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import type { ProxyRuntimeActivity, ProxyUpdateWindowOptions, ProxyUpdateWindowResult } from "../types/index.js";
|
|
1
|
+
import type { ProxyRuntimeActivity, ProxyServingWorkerIdentity, ProxyUpdateWindowOptions, ProxyUpdateWindowResult } from "../types/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Parse serving identity conservatively from `/status`. A rolling status is
|
|
4
|
+
* trusted only when the worker-local fields agree with the supervisor snapshot.
|
|
5
|
+
*/
|
|
6
|
+
export declare function parseProxyServingWorkerIdentity(payload: unknown, rollingSupervisor: boolean): ProxyServingWorkerIdentity | null;
|
|
7
|
+
/** A long-running update may mutate live selection only for its original worker. */
|
|
8
|
+
export declare function isSameProxyServingWorker(expected: ProxyServingWorkerIdentity, current: ProxyServingWorkerIdentity | null | undefined): boolean;
|
|
2
9
|
/**
|
|
3
10
|
* A replacement updater starts from the newly installed package. The registry
|
|
4
11
|
* then reports no newer version even when its launchd parent still runs the
|
|
@@ -18,6 +25,8 @@ export declare function isProxyRuntimeSettled(activity: ProxyRuntimeActivity): b
|
|
|
18
25
|
export declare function parseProxyRuntimeActivity(payload: unknown): ProxyRuntimeActivity | null;
|
|
19
26
|
/**
|
|
20
27
|
* Prefer a naturally quiet window, then briefly drain new inference traffic.
|
|
28
|
+
* Automatic supervisor refresh uses allowBusyDrain=false: it fences admission
|
|
29
|
+
* only after a quiet observation and never waits for busy workers behind it.
|
|
21
30
|
* Existing requests are never interrupted; a bounded drain failure is returned
|
|
22
31
|
* to the caller so admission can be reopened and retried later.
|
|
23
32
|
*/
|