@juspay/neurolink 12.14.19 → 12.14.20
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 +367 -367
- package/dist/cli/commands/proxy.js +78 -46
- package/dist/proxy/bodyCaptureWorker.js +126 -13
- package/dist/proxy/requestLogger.d.ts +4 -1
- package/dist/proxy/requestLogger.js +10 -3
- package/dist/proxy/updateCoordinator.d.ts +13 -0
- package/dist/proxy/updateCoordinator.js +13 -0
- package/dist/types/proxy.d.ts +13 -1
- package/package.json +1 -1
- package/scripts/observability/proxy-telemetry-check.mjs +306 -33
- package/scripts/observability/query-proxy-history.mjs +1 -1
|
@@ -31,7 +31,7 @@ import { isProxyAuxiliaryRequest } from "../../proxy/proxyRequestKind.js";
|
|
|
31
31
|
import { spawnProxySocketWorker } from "../../proxy/rollingWorkerProcess.js";
|
|
32
32
|
import { PROXY_ROLLING_SUPERVISOR_ENV, PROXY_SOCKET_WORKER_ENV, } from "../../proxy/rollingWorkerProtocol.js";
|
|
33
33
|
import { attachSocketWorkerProcess } from "../../proxy/socketWorkerRuntime.js";
|
|
34
|
-
import { waitForProxyUpdateWindow } from "../../proxy/updateCoordinator.js";
|
|
34
|
+
import { shouldRefreshStaleSupervisor, waitForProxyUpdateWindow, } from "../../proxy/updateCoordinator.js";
|
|
35
35
|
import { abandonPendingUpdate, clearUpdateDeferral, isVersionSuppressed, loadUpdateState, recordCheck, recordSuccessfulUpdate, recordUpdateDeferred, recordUpdateFailure, recordUpdateInstalled, suppressVersion, } from "../../proxy/updateState.js";
|
|
36
36
|
import { loadProxyEnvFile, resolveProxyEnvFile, } from "../../proxy/proxyEnv.js";
|
|
37
37
|
import { createRequire } from "node:module";
|
|
@@ -3865,14 +3865,35 @@ export const proxyGuardCommand = {
|
|
|
3865
3865
|
let result = await checkForUpdate(runningVersion);
|
|
3866
3866
|
updateVersion = result.latestVersion;
|
|
3867
3867
|
persistUpdaterState("record update check", () => recordCheck(result.latestVersion));
|
|
3868
|
+
let supervisorRefreshOnly = false;
|
|
3868
3869
|
if (!result.updateAvailable) {
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3870
|
+
const activeVersions = rollingSupervisor
|
|
3871
|
+
? await fetchProxyRuntimeVersions(host, port).catch(() => ({}))
|
|
3872
|
+
: {};
|
|
3873
|
+
if (rollingSupervisor &&
|
|
3874
|
+
(!activeVersions.workerVersion || !activeVersions.supervisorVersion)) {
|
|
3875
|
+
logger.always(`[updater] WARNING: runtime version probe incomplete (worker=${activeVersions.workerVersion ?? "unknown"}, supervisor=${activeVersions.supervisorVersion ?? "unknown"}); stale-supervisor reconciliation deferred`);
|
|
3876
|
+
scheduleUpdateRetry("runtime version probe incomplete", result.latestVersion);
|
|
3877
|
+
return;
|
|
3878
|
+
}
|
|
3879
|
+
supervisorRefreshOnly = shouldRefreshStaleSupervisor({
|
|
3880
|
+
updateAvailable: result.updateAvailable,
|
|
3881
|
+
rollingSupervisor,
|
|
3882
|
+
runningVersion,
|
|
3883
|
+
workerVersion: activeVersions.workerVersion,
|
|
3884
|
+
supervisorVersion: activeVersions.supervisorVersion,
|
|
3885
|
+
});
|
|
3886
|
+
if (!supervisorRefreshOnly) {
|
|
3887
|
+
updateRetryAttempts = 0;
|
|
3888
|
+
updateRetryVersion = null;
|
|
3889
|
+
persistUpdaterState("clear update deferral", () => clearUpdateDeferral());
|
|
3890
|
+
return;
|
|
3891
|
+
}
|
|
3892
|
+
logger.always(`[updater] worker v${runningVersion} is current; reconciling stale supervisor v${activeVersions.supervisorVersion}`);
|
|
3873
3893
|
}
|
|
3874
3894
|
const initiallyPendingRestart = loadUpdateState()?.pendingRestartVersion === result.latestVersion;
|
|
3875
|
-
if (
|
|
3895
|
+
if (!supervisorRefreshOnly &&
|
|
3896
|
+
isVersionSuppressed(result.latestVersion) &&
|
|
3876
3897
|
!initiallyPendingRestart) {
|
|
3877
3898
|
logger.debug(`[guard] version ${result.latestVersion} is suppressed, skipping`);
|
|
3878
3899
|
return;
|
|
@@ -3932,26 +3953,30 @@ export const proxyGuardCommand = {
|
|
|
3932
3953
|
}
|
|
3933
3954
|
}
|
|
3934
3955
|
}
|
|
3935
|
-
else {
|
|
3956
|
+
else if (!supervisorRefreshOnly) {
|
|
3936
3957
|
logger.always(`[updater] rolling supervisor will keep v${runningVersion} serving during installation`);
|
|
3937
3958
|
}
|
|
3938
3959
|
// Refresh after waiting so a long deferral can never install a stale
|
|
3939
3960
|
// target while a newer release is already available.
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
persistUpdaterState("
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3961
|
+
let pendingRestart = initiallyPendingRestart;
|
|
3962
|
+
if (!supervisorRefreshOnly) {
|
|
3963
|
+
const refreshedResult = await checkForUpdate(runningVersion);
|
|
3964
|
+
result = refreshedResult;
|
|
3965
|
+
updateVersion = result.latestVersion;
|
|
3966
|
+
persistUpdaterState("record refreshed update check", () => recordCheck(result.latestVersion));
|
|
3967
|
+
if (!result.updateAvailable) {
|
|
3968
|
+
persistUpdaterState("clear update deferral", () => clearUpdateDeferral());
|
|
3969
|
+
return;
|
|
3970
|
+
}
|
|
3971
|
+
pendingRestart =
|
|
3972
|
+
loadUpdateState()?.pendingRestartVersion === result.latestVersion;
|
|
3973
|
+
if (isVersionSuppressed(result.latestVersion) && !pendingRestart) {
|
|
3974
|
+
logger.debug(`[guard] refreshed version ${result.latestVersion} is suppressed, skipping`);
|
|
3975
|
+
return;
|
|
3976
|
+
}
|
|
3952
3977
|
}
|
|
3953
3978
|
persistUpdaterState("clear update deferral", () => clearUpdateDeferral());
|
|
3954
|
-
logger.always(`[updater] ${rollingSupervisor ? "rolling activation prepared" : "safe update window acquired"} for v${result.latestVersion}`);
|
|
3979
|
+
logger.always(`[updater] ${supervisorRefreshOnly ? "supervisor reconciliation prepared" : rollingSupervisor ? "rolling activation prepared" : "safe update window acquired"} for v${result.latestVersion}`);
|
|
3955
3980
|
// 3. Install update (validate version string before passing to shell)
|
|
3956
3981
|
if (!/^\d+\.\d+\.\d+$/.test(result.latestVersion)) {
|
|
3957
3982
|
const message = `invalid version format: ${result.latestVersion}`;
|
|
@@ -3960,7 +3985,7 @@ export const proxyGuardCommand = {
|
|
|
3960
3985
|
return;
|
|
3961
3986
|
}
|
|
3962
3987
|
const { execFileSync } = await import("node:child_process");
|
|
3963
|
-
if (!pendingRestart) {
|
|
3988
|
+
if (!supervisorRefreshOnly && !pendingRestart) {
|
|
3964
3989
|
const installerResolution = resolveGlobalInstaller({
|
|
3965
3990
|
entryScript: process.argv[1],
|
|
3966
3991
|
});
|
|
@@ -3994,35 +4019,37 @@ export const proxyGuardCommand = {
|
|
|
3994
4019
|
return;
|
|
3995
4020
|
}
|
|
3996
4021
|
}
|
|
3997
|
-
else {
|
|
4022
|
+
else if (!supervisorRefreshOnly) {
|
|
3998
4023
|
logger.always(`[updater] resuming pending restart for already-installed v${result.latestVersion}`);
|
|
3999
4024
|
}
|
|
4000
4025
|
// 4. Refresh and validate the stable trampoline. The plist already
|
|
4001
4026
|
// points at this path, so it must not be unloaded or rewritten here.
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4027
|
+
if (!supervisorRefreshOnly) {
|
|
4028
|
+
try {
|
|
4029
|
+
writeTrampoline();
|
|
4030
|
+
const validation = await validateInstalledVersion({
|
|
4031
|
+
binPath: TRAMPOLINE_PATH,
|
|
4032
|
+
expectedVersion: result.latestVersion,
|
|
4033
|
+
});
|
|
4034
|
+
if (validation.version !== result.latestVersion) {
|
|
4035
|
+
const message = `trampoline validation failed after ${validation.attempts} attempts: ${validation.failure ?? "unknown failure"}`;
|
|
4036
|
+
logger.always(`[updater] WARNING: ${message}; restart deferred`);
|
|
4037
|
+
persistUpdaterState("record update failure", () => recordUpdateFailure(result.latestVersion, "validation", message));
|
|
4038
|
+
persistUpdaterState("abandon invalid pending update", () => abandonPendingUpdate(result.latestVersion));
|
|
4039
|
+
return;
|
|
4040
|
+
}
|
|
4041
|
+
persistUpdaterState("record installed update", () => recordUpdateInstalled(result.latestVersion));
|
|
4042
|
+
logger.always(`[updater] trampoline validated at v${validation.version} after ${validation.attempts} attempt(s)`);
|
|
4043
|
+
}
|
|
4044
|
+
catch (trampolineError) {
|
|
4045
|
+
const message = trampolineError instanceof Error
|
|
4046
|
+
? trampolineError.message
|
|
4047
|
+
: String(trampolineError);
|
|
4048
|
+
logger.always(`[updater] WARNING: failed to refresh trampoline; refusing restart: ${message}`);
|
|
4011
4049
|
persistUpdaterState("record update failure", () => recordUpdateFailure(result.latestVersion, "validation", message));
|
|
4012
4050
|
persistUpdaterState("abandon invalid pending update", () => abandonPendingUpdate(result.latestVersion));
|
|
4013
4051
|
return;
|
|
4014
4052
|
}
|
|
4015
|
-
persistUpdaterState("record installed update", () => recordUpdateInstalled(result.latestVersion));
|
|
4016
|
-
logger.always(`[updater] trampoline validated at v${validation.version} after ${validation.attempts} attempt(s)`);
|
|
4017
|
-
}
|
|
4018
|
-
catch (trampolineError) {
|
|
4019
|
-
const message = trampolineError instanceof Error
|
|
4020
|
-
? trampolineError.message
|
|
4021
|
-
: String(trampolineError);
|
|
4022
|
-
logger.always(`[updater] WARNING: failed to refresh trampoline; refusing restart: ${message}`);
|
|
4023
|
-
persistUpdaterState("record update failure", () => recordUpdateFailure(result.latestVersion, "validation", message));
|
|
4024
|
-
persistUpdaterState("abandon invalid pending update", () => abandonPendingUpdate(result.latestVersion));
|
|
4025
|
-
return;
|
|
4026
4053
|
}
|
|
4027
4054
|
if (guardStopping || getProcessStatus(parentPid) === "not_running") {
|
|
4028
4055
|
return;
|
|
@@ -4200,11 +4227,11 @@ export const proxyGuardCommand = {
|
|
|
4200
4227
|
process.exit(0);
|
|
4201
4228
|
}
|
|
4202
4229
|
else {
|
|
4203
|
-
logger.always(`[updater] WARNING: proxy unhealthy after update to ${result.latestVersion}`);
|
|
4230
|
+
logger.always(`[updater] WARNING: proxy unhealthy after ${supervisorRefreshOnly ? "supervisor reconciliation at" : "update to"} ${result.latestVersion}`);
|
|
4204
4231
|
let failureMessage = rollingFailure
|
|
4205
4232
|
? `rolling candidate failed: ${rollingFailure}`
|
|
4206
4233
|
: `proxy did not report v${result.latestVersion} healthy within ${UPDATE_TIMEOUT_MS}ms`;
|
|
4207
|
-
if (rollingSupervisor) {
|
|
4234
|
+
if (rollingSupervisor && !supervisorRefreshOnly) {
|
|
4208
4235
|
try {
|
|
4209
4236
|
const rollbackResolution = resolveGlobalInstaller({
|
|
4210
4237
|
entryScript: process.argv[1],
|
|
@@ -4244,8 +4271,13 @@ export const proxyGuardCommand = {
|
|
|
4244
4271
|
}
|
|
4245
4272
|
}
|
|
4246
4273
|
persistUpdaterState("record update failure", () => recordUpdateFailure(result.latestVersion, "health", failureMessage));
|
|
4247
|
-
|
|
4248
|
-
|
|
4274
|
+
if (supervisorRefreshOnly) {
|
|
4275
|
+
scheduleUpdateRetry("stale supervisor reconciliation health unavailable", result.latestVersion);
|
|
4276
|
+
}
|
|
4277
|
+
else {
|
|
4278
|
+
persistUpdaterState("abandon unhealthy pending update", () => abandonPendingUpdate(result.latestVersion));
|
|
4279
|
+
persistUpdaterState("suppress unhealthy update", () => suppressVersion(result.latestVersion, "unhealthy_after_restart"));
|
|
4280
|
+
}
|
|
4249
4281
|
updateRestartInProgress = false;
|
|
4250
4282
|
}
|
|
4251
4283
|
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { Worker } from "node:worker_threads";
|
|
2
2
|
const MAX_PENDING = 64;
|
|
3
3
|
const MAX_PENDING_BYTES = 32 * 1024 * 1024;
|
|
4
|
+
// OTel publication is asynchronous. Absorb one additional bounded burst
|
|
5
|
+
// without expanding the active structured-clone pool or blocking model
|
|
6
|
+
// responses; sustained overload still expires with an explicit index failure.
|
|
7
|
+
const MAX_OTEL_WAITING = 64;
|
|
8
|
+
const MAX_OTEL_WAITING_BYTES = 32 * 1024 * 1024;
|
|
9
|
+
const OTEL_ADMISSION_WAIT_MS = 20_000;
|
|
4
10
|
// Bound retained UTF-16 strings rather than a 3x UTF-8 guess. This accommodates
|
|
5
11
|
// the observed 7.3 MB JSON requests while retaining a 32 MiB aggregate pool.
|
|
6
12
|
const MAX_ENTRY_BYTES = 16 * 1024 * 1024;
|
|
@@ -20,9 +26,99 @@ const snapshot = {
|
|
|
20
26
|
maxPendingBytes: MAX_PENDING_BYTES,
|
|
21
27
|
highWaterPending: 0,
|
|
22
28
|
highWaterBytes: 0,
|
|
29
|
+
waiting: 0,
|
|
30
|
+
waitingBytes: 0,
|
|
31
|
+
maxWaiting: MAX_OTEL_WAITING,
|
|
32
|
+
maxWaitingBytes: MAX_OTEL_WAITING_BYTES,
|
|
33
|
+
highWaterWaiting: 0,
|
|
34
|
+
highWaterWaitingBytes: 0,
|
|
35
|
+
admissionWaits: 0,
|
|
36
|
+
admissionTimeouts: 0,
|
|
23
37
|
rejectionReasons: {},
|
|
24
38
|
};
|
|
25
39
|
const pending = new Map();
|
|
40
|
+
const otelAdmissionWaiters = [];
|
|
41
|
+
let admissionRetryTimer;
|
|
42
|
+
/** Atomically reserve the active clone/publication lease. */
|
|
43
|
+
function reserveCapture(bytes) {
|
|
44
|
+
if (Date.now() < retryAfter ||
|
|
45
|
+
snapshot.pending >= MAX_PENDING ||
|
|
46
|
+
snapshot.pendingBytes + bytes > MAX_PENDING_BYTES) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
snapshot.pending += 1;
|
|
50
|
+
snapshot.pendingBytes += bytes;
|
|
51
|
+
snapshot.highWaterPending = Math.max(snapshot.highWaterPending, snapshot.pending);
|
|
52
|
+
snapshot.highWaterBytes = Math.max(snapshot.highWaterBytes, snapshot.pendingBytes);
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
/** Admit every compatible OTel waiter as active leases settle. */
|
|
56
|
+
function drainOtelAdmissionWaiters() {
|
|
57
|
+
if (!otelAdmissionWaiters.length) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (Date.now() < retryAfter) {
|
|
61
|
+
if (!admissionRetryTimer) {
|
|
62
|
+
admissionRetryTimer = setTimeout(() => {
|
|
63
|
+
admissionRetryTimer = undefined;
|
|
64
|
+
drainOtelAdmissionWaiters();
|
|
65
|
+
}, Math.max(1, retryAfter - Date.now()));
|
|
66
|
+
admissionRetryTimer.unref?.();
|
|
67
|
+
}
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
for (let index = 0; index < otelAdmissionWaiters.length;) {
|
|
71
|
+
if (snapshot.pending >= MAX_PENDING) {
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
const waiter = otelAdmissionWaiters[index];
|
|
75
|
+
if (!reserveCapture(waiter.bytes)) {
|
|
76
|
+
// Byte pressure can block a large capture while a later small capture
|
|
77
|
+
// still fits. Keep the large capture's original timeout and scan the
|
|
78
|
+
// rest of the bounded queue once.
|
|
79
|
+
index += 1;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
otelAdmissionWaiters.splice(index, 1);
|
|
83
|
+
clearTimeout(waiter.timer);
|
|
84
|
+
snapshot.waiting -= 1;
|
|
85
|
+
snapshot.waitingBytes -= waiter.bytes;
|
|
86
|
+
waiter.resolve("admitted");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** Retain bounded OTel overflow instead of dropping a transient burst. */
|
|
90
|
+
function waitForOtelAdmission(bytes) {
|
|
91
|
+
if (snapshot.waiting >= MAX_OTEL_WAITING ||
|
|
92
|
+
snapshot.waitingBytes + bytes > MAX_OTEL_WAITING_BYTES) {
|
|
93
|
+
return Promise.resolve("queue_full");
|
|
94
|
+
}
|
|
95
|
+
snapshot.admissionWaits += 1;
|
|
96
|
+
snapshot.waiting += 1;
|
|
97
|
+
snapshot.waitingBytes += bytes;
|
|
98
|
+
snapshot.highWaterWaiting = Math.max(snapshot.highWaterWaiting, snapshot.waiting);
|
|
99
|
+
snapshot.highWaterWaitingBytes = Math.max(snapshot.highWaterWaitingBytes, snapshot.waitingBytes);
|
|
100
|
+
return new Promise((resolve) => {
|
|
101
|
+
const timer = setTimeout(() => {
|
|
102
|
+
const index = otelAdmissionWaiters.findIndex((waiter) => waiter.timer === timer);
|
|
103
|
+
if (index < 0) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
otelAdmissionWaiters.splice(index, 1);
|
|
107
|
+
snapshot.waiting -= 1;
|
|
108
|
+
snapshot.waitingBytes -= bytes;
|
|
109
|
+
snapshot.admissionTimeouts += 1;
|
|
110
|
+
resolve("timeout");
|
|
111
|
+
drainOtelAdmissionWaiters();
|
|
112
|
+
}, OTEL_ADMISSION_WAIT_MS);
|
|
113
|
+
timer.unref?.();
|
|
114
|
+
otelAdmissionWaiters.push({ bytes, resolve, timer });
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
function releaseCapture(bytes) {
|
|
118
|
+
snapshot.pending -= 1;
|
|
119
|
+
snapshot.pendingBytes -= bytes;
|
|
120
|
+
drainOtelAdmissionWaiters();
|
|
121
|
+
}
|
|
26
122
|
// Bound traversal as well as the structured clone sent to the worker. Never
|
|
27
123
|
// invoke getters/toJSON or stringify a large body on the serving event loop.
|
|
28
124
|
/**
|
|
@@ -135,6 +231,7 @@ function getWorker() {
|
|
|
135
231
|
/** Bounded bulk capture. Failures are indexed; never fall back to blocking work. */
|
|
136
232
|
export async function captureProxyBody(entry, logDir, consume) {
|
|
137
233
|
snapshot.attempted += 1;
|
|
234
|
+
const queuedAt = Date.now();
|
|
138
235
|
// An OTel capture can use the existing byte pool when it is otherwise idle.
|
|
139
236
|
// Raising the slot count does not raise the aggregate retained-memory bound.
|
|
140
237
|
const maxEntryBytes = logDir === null ? MAX_PENDING_BYTES : MAX_ENTRY_BYTES;
|
|
@@ -155,16 +252,23 @@ export async function captureProxyBody(entry, logDir, consume) {
|
|
|
155
252
|
? error.message
|
|
156
253
|
: "body_capture_unsupported_value";
|
|
157
254
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
255
|
+
let admission = "queue_full";
|
|
256
|
+
if (bytes <= maxEntryBytes && Date.now() >= retryAfter) {
|
|
257
|
+
admission = reserveCapture(bytes)
|
|
258
|
+
? "admitted"
|
|
259
|
+
: logDir === null
|
|
260
|
+
? await waitForOtelAdmission(bytes)
|
|
261
|
+
: "queue_full";
|
|
262
|
+
}
|
|
263
|
+
if (admission !== "admitted") {
|
|
162
264
|
snapshot.rejected += 1;
|
|
163
265
|
const error = bytes > maxEntryBytes
|
|
164
266
|
? (admissionError ?? "body_capture_entry_too_large")
|
|
165
267
|
: Date.now() < retryAfter
|
|
166
268
|
? "body_worker_backoff"
|
|
167
|
-
: "
|
|
269
|
+
: admission === "timeout"
|
|
270
|
+
? "body_capture_admission_timeout"
|
|
271
|
+
: "body_capture_queue_full";
|
|
168
272
|
snapshot.lastError = error;
|
|
169
273
|
snapshot.lastRejectedAt = new Date().toISOString();
|
|
170
274
|
snapshot.rejectionReasons[error] =
|
|
@@ -194,8 +298,9 @@ export async function captureProxyBody(entry, logDir, consume) {
|
|
|
194
298
|
current = getWorker();
|
|
195
299
|
}
|
|
196
300
|
catch {
|
|
197
|
-
snapshot.failed += 1;
|
|
198
301
|
retryAfter = Date.now() + 5_000;
|
|
302
|
+
releaseCapture(bytes);
|
|
303
|
+
snapshot.failed += 1;
|
|
199
304
|
return consume({
|
|
200
305
|
error: "body_worker_start_failed",
|
|
201
306
|
stored: { bodyWriteFailed: true },
|
|
@@ -215,8 +320,7 @@ export async function captureProxyBody(entry, logDir, consume) {
|
|
|
215
320
|
result.error ??= "body_capture_publication_failed";
|
|
216
321
|
})
|
|
217
322
|
.finally(() => {
|
|
218
|
-
|
|
219
|
-
snapshot.pendingBytes -= bytes;
|
|
323
|
+
releaseCapture(bytes);
|
|
220
324
|
if (result.error || result.stored.bodyWriteFailed) {
|
|
221
325
|
snapshot.failed += 1;
|
|
222
326
|
}
|
|
@@ -230,13 +334,9 @@ export async function captureProxyBody(entry, logDir, consume) {
|
|
|
230
334
|
});
|
|
231
335
|
},
|
|
232
336
|
});
|
|
233
|
-
snapshot.pending += 1;
|
|
234
|
-
snapshot.highWaterPending = Math.max(snapshot.highWaterPending, snapshot.pending);
|
|
235
|
-
snapshot.pendingBytes += bytes;
|
|
236
|
-
snapshot.highWaterBytes = Math.max(snapshot.highWaterBytes, snapshot.pendingBytes);
|
|
237
337
|
current.ref();
|
|
238
338
|
try {
|
|
239
|
-
current.postMessage({ id, entry, logDir, queuedAt
|
|
339
|
+
current.postMessage({ id, entry, logDir, queuedAt });
|
|
240
340
|
}
|
|
241
341
|
catch {
|
|
242
342
|
settle(id, {
|
|
@@ -259,6 +359,9 @@ export const __bodyCaptureWorkerTestHooks = {
|
|
|
259
359
|
* Reset an isolated worker after capture publications drain, optionally selecting a fixture entry.
|
|
260
360
|
*/
|
|
261
361
|
async reset(url) {
|
|
362
|
+
if (otelAdmissionWaiters.length) {
|
|
363
|
+
throw new Error("Cannot reset body capture worker with admission waiters");
|
|
364
|
+
}
|
|
262
365
|
if (worker) {
|
|
263
366
|
const current = worker;
|
|
264
367
|
failWorker(current, "body_worker_test_reset");
|
|
@@ -266,6 +369,10 @@ export const __bodyCaptureWorkerTestHooks = {
|
|
|
266
369
|
}
|
|
267
370
|
workerUrl = url;
|
|
268
371
|
retryAfter = 0;
|
|
372
|
+
if (admissionRetryTimer) {
|
|
373
|
+
clearTimeout(admissionRetryTimer);
|
|
374
|
+
admissionRetryTimer = undefined;
|
|
375
|
+
}
|
|
269
376
|
Object.assign(snapshot, {
|
|
270
377
|
attempted: 0,
|
|
271
378
|
completed: 0,
|
|
@@ -277,6 +384,12 @@ export const __bodyCaptureWorkerTestHooks = {
|
|
|
277
384
|
lastRejectedAt: undefined,
|
|
278
385
|
highWaterPending: 0,
|
|
279
386
|
highWaterBytes: 0,
|
|
387
|
+
waiting: 0,
|
|
388
|
+
waitingBytes: 0,
|
|
389
|
+
highWaterWaiting: 0,
|
|
390
|
+
highWaterWaitingBytes: 0,
|
|
391
|
+
admissionWaits: 0,
|
|
392
|
+
admissionTimeouts: 0,
|
|
280
393
|
rejectionReasons: {},
|
|
281
394
|
});
|
|
282
395
|
},
|
|
@@ -10,7 +10,10 @@ import type { ProxyBodyCaptureEntry, RequestAttemptLogEntry, RequestLogEntry, Pr
|
|
|
10
10
|
* Expose independent metadata-sink and body-capture counters for incident reconciliation.
|
|
11
11
|
*/
|
|
12
12
|
export declare function getRequestLoggerSnapshot(): ProxyRequestLoggerSnapshot;
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* Wait through bounded OTel admission, worker processing, and publication
|
|
15
|
+
* deadlines so a clean shutdown does not abandon an accepted overflow capture.
|
|
16
|
+
*/
|
|
14
17
|
export declare function flushRequestLogs(timeoutMs?: number): Promise<void>;
|
|
15
18
|
/**
|
|
16
19
|
* Initialize private request logs and preserve required lifecycle admission on startup failures.
|
|
@@ -26,6 +26,10 @@ let logDir = null;
|
|
|
26
26
|
let logEnabled = false;
|
|
27
27
|
const pendingLogOperations = new Set();
|
|
28
28
|
const REQUEST_LOG_IO_TIMEOUT_MS = 5_000;
|
|
29
|
+
// launchd gives the service 45 seconds after SIGTERM. Leave fifteen seconds
|
|
30
|
+
// for listener drain and exporter shutdown instead of advertising a flush
|
|
31
|
+
// deadline the service manager can terminate before it reaches.
|
|
32
|
+
const REQUEST_LOG_FLUSH_TIMEOUT_MS = PROXY_BODY_CAPTURE_DEADLINE_MS + 2 * REQUEST_LOG_IO_TIMEOUT_MS;
|
|
29
33
|
const MAX_PENDING_METADATA_RECORDS = 4_096;
|
|
30
34
|
const appendChains = new Map();
|
|
31
35
|
let appendMetadataFile = writeFile;
|
|
@@ -114,9 +118,11 @@ function trackLogOperation(operation) {
|
|
|
114
118
|
void operation.then(() => pendingLogOperations.delete(operation), () => pendingLogOperations.delete(operation));
|
|
115
119
|
return operation;
|
|
116
120
|
}
|
|
117
|
-
/**
|
|
118
|
-
|
|
119
|
-
|
|
121
|
+
/**
|
|
122
|
+
* Wait through bounded OTel admission, worker processing, and publication
|
|
123
|
+
* deadlines so a clean shutdown does not abandon an accepted overflow capture.
|
|
124
|
+
*/
|
|
125
|
+
export async function flushRequestLogs(timeoutMs = REQUEST_LOG_FLUSH_TIMEOUT_MS) {
|
|
120
126
|
const deadline = Date.now() + Math.max(1, timeoutMs);
|
|
121
127
|
while (pendingLogOperations.size > 0) {
|
|
122
128
|
const admitted = [...pendingLogOperations];
|
|
@@ -130,6 +136,7 @@ export async function flushRequestLogs(timeoutMs = PROXY_BODY_CAPTURE_DEADLINE_M
|
|
|
130
136
|
}
|
|
131
137
|
/** @internal Test-only hook for exercising shutdown behavior without real I/O. */
|
|
132
138
|
export const __requestLoggerTestHooks = {
|
|
139
|
+
defaultFlushTimeoutMs: REQUEST_LOG_FLUSH_TIMEOUT_MS,
|
|
133
140
|
pendingOperationCount: () => pendingLogOperations.size,
|
|
134
141
|
trackLogOperation,
|
|
135
142
|
setAppendFileForTests: (writer) => {
|
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
import type { ProxyUpdateWindowOptions, ProxyUpdateWindowResult } from "../types/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* A replacement updater starts from the newly installed package. The registry
|
|
4
|
+
* then reports no newer version even when its launchd parent still runs the
|
|
5
|
+
* old package, so parent reconciliation must be decided from both live
|
|
6
|
+
* generations instead of `updateAvailable` alone.
|
|
7
|
+
*/
|
|
8
|
+
export declare function shouldRefreshStaleSupervisor(options: {
|
|
9
|
+
updateAvailable: boolean;
|
|
10
|
+
rollingSupervisor: boolean;
|
|
11
|
+
runningVersion: string;
|
|
12
|
+
workerVersion?: string;
|
|
13
|
+
supervisorVersion?: string;
|
|
14
|
+
}): boolean;
|
|
2
15
|
/**
|
|
3
16
|
* Prefer a naturally quiet window, then briefly drain new inference traffic.
|
|
4
17
|
* Existing requests are never interrupted; a bounded drain failure is returned
|
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A replacement updater starts from the newly installed package. The registry
|
|
3
|
+
* then reports no newer version even when its launchd parent still runs the
|
|
4
|
+
* old package, so parent reconciliation must be decided from both live
|
|
5
|
+
* generations instead of `updateAvailable` alone.
|
|
6
|
+
*/
|
|
7
|
+
export function shouldRefreshStaleSupervisor(options) {
|
|
8
|
+
return (!options.updateAvailable &&
|
|
9
|
+
options.rollingSupervisor &&
|
|
10
|
+
options.workerVersion === options.runningVersion &&
|
|
11
|
+
typeof options.supervisorVersion === "string" &&
|
|
12
|
+
options.supervisorVersion !== options.runningVersion);
|
|
13
|
+
}
|
|
1
14
|
function isQuiet(activity, quietThresholdMs, nowMs) {
|
|
2
15
|
if (activity.activeRequests > 0) {
|
|
3
16
|
return false;
|
package/dist/types/proxy.d.ts
CHANGED
|
@@ -669,7 +669,7 @@ export type ProxyBodyCaptureWorkerSnapshot = {
|
|
|
669
669
|
rejectionReasons: Record<string, number>;
|
|
670
670
|
lastError?: string;
|
|
671
671
|
lastRejectedAt?: string;
|
|
672
|
-
};
|
|
672
|
+
} & ProxyBodyCaptureWaitingSnapshot;
|
|
673
673
|
/** Collector transport evidence; acknowledgement does not prove backend storage. */
|
|
674
674
|
export type ProxyBodyDeliveryResult = {
|
|
675
675
|
status: "transport_acknowledged" | "export_unconfirmed" | "rejected" | "partial";
|
|
@@ -4000,3 +4000,15 @@ export type ProxyLogTraceContext = {
|
|
|
4000
4000
|
spanId: string;
|
|
4001
4001
|
traceFlags: number;
|
|
4002
4002
|
};
|
|
4003
|
+
/** OTel-only captures waiting for bounded worker capacity. */
|
|
4004
|
+
type ProxyBodyCaptureWaitingSnapshot = {
|
|
4005
|
+
waiting: number;
|
|
4006
|
+
waitingBytes: number;
|
|
4007
|
+
maxWaiting: number;
|
|
4008
|
+
maxWaitingBytes: number;
|
|
4009
|
+
highWaterWaiting: number;
|
|
4010
|
+
highWaterWaitingBytes: number;
|
|
4011
|
+
admissionWaits: number;
|
|
4012
|
+
admissionTimeouts: number;
|
|
4013
|
+
};
|
|
4014
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@juspay/neurolink",
|
|
3
|
-
"version": "12.14.
|
|
3
|
+
"version": "12.14.20",
|
|
4
4
|
"packageManager": "pnpm@10.15.1",
|
|
5
5
|
"description": "TypeScript AI SDK with 24+ LLM providers behind one consistent API. MCP-native (connect any MCP server), voice TTS/STT/realtime, RAG, agents, memory, context compaction. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · DeepSeek · NVIDIA NIM and more.",
|
|
6
6
|
"author": {
|