@juspay/neurolink 12.12.9 → 12.12.10
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 +394 -395
- package/dist/cli/commands/proxy.d.ts +4 -0
- package/dist/cli/commands/proxy.js +60 -18
- package/dist/cli/commands/proxyAnalyze.js +8 -1
- package/dist/proxy/bodyCaptureProcessing.d.ts +22 -0
- package/dist/proxy/bodyCaptureProcessing.js +219 -0
- package/dist/proxy/bodyCaptureWorker.d.ts +16 -0
- package/dist/proxy/bodyCaptureWorker.js +232 -0
- package/dist/proxy/bodyCaptureWorkerEntry.d.ts +1 -0
- package/dist/proxy/bodyCaptureWorkerEntry.js +34 -0
- package/dist/proxy/proxyAnalysis.js +159 -17
- package/dist/proxy/proxyLifecycle.d.ts +25 -0
- package/dist/proxy/proxyLifecycle.js +111 -5
- package/dist/proxy/proxyRequestKind.d.ts +2 -0
- package/dist/proxy/proxyRequestKind.js +6 -0
- package/dist/proxy/proxyRuntimeMetrics.d.ts +3 -0
- package/dist/proxy/proxyRuntimeMetrics.js +34 -0
- package/dist/proxy/requestLogger.d.ts +10 -6
- package/dist/proxy/requestLogger.js +99 -231
- package/dist/proxy/rollingProxyServer.js +15 -4
- package/dist/proxy/rollingWorkerProcess.d.ts +4 -0
- package/dist/proxy/rollingWorkerProcess.js +25 -8
- package/dist/proxy/rollingWorkerProtocol.d.ts +6 -0
- package/dist/proxy/rollingWorkerProtocol.js +12 -1
- package/dist/proxy/rollingWorkerSupervisor.d.ts +28 -0
- package/dist/proxy/rollingWorkerSupervisor.js +73 -25
- package/dist/proxy/socketWorkerRuntime.d.ts +5 -0
- package/dist/proxy/socketWorkerRuntime.js +19 -2
- package/dist/server/routes/codexProxyRoutes.js +39 -3
- package/dist/services/server/ai/observability/instrumentation.js +7 -1
- package/dist/types/cli.d.ts +1 -1
- package/dist/types/proxy.d.ts +69 -4
- package/package.json +1 -1
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { Worker } from "node:worker_threads";
|
|
2
|
+
const MAX_PENDING = 16;
|
|
3
|
+
const MAX_PENDING_BYTES = 32 * 1024 * 1024;
|
|
4
|
+
const MAX_ENTRY_BYTES = 8 * 1024 * 1024;
|
|
5
|
+
export const PROXY_BODY_CAPTURE_DEADLINE_MS = 20_000;
|
|
6
|
+
let worker;
|
|
7
|
+
let workerUrl;
|
|
8
|
+
let retryAfter = 0;
|
|
9
|
+
let nextId = 0;
|
|
10
|
+
const snapshot = {
|
|
11
|
+
attempted: 0,
|
|
12
|
+
completed: 0,
|
|
13
|
+
rejected: 0,
|
|
14
|
+
failed: 0,
|
|
15
|
+
pending: 0,
|
|
16
|
+
pendingBytes: 0,
|
|
17
|
+
maxPending: MAX_PENDING,
|
|
18
|
+
maxPendingBytes: MAX_PENDING_BYTES,
|
|
19
|
+
};
|
|
20
|
+
const pending = new Map();
|
|
21
|
+
// Bound traversal as well as the structured clone sent to the worker. Never
|
|
22
|
+
// invoke getters/toJSON or stringify a large body on the serving event loop.
|
|
23
|
+
/**
|
|
24
|
+
* Conservatively bound clone size and traversal work without invoking
|
|
25
|
+
* getters or serializers.
|
|
26
|
+
*/
|
|
27
|
+
function estimateCloneBytes(value) {
|
|
28
|
+
const stack = [value];
|
|
29
|
+
const seen = new Set();
|
|
30
|
+
let bytes = 0, nodes = 0;
|
|
31
|
+
while (stack.length) {
|
|
32
|
+
if (++nodes > 100_000 || bytes > MAX_ENTRY_BYTES) {
|
|
33
|
+
return Infinity;
|
|
34
|
+
}
|
|
35
|
+
const item = stack.pop();
|
|
36
|
+
if (typeof item === "string") {
|
|
37
|
+
bytes += item.length * 3;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
bytes += 16;
|
|
41
|
+
if (!item || typeof item !== "object") {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (seen.has(item)) {
|
|
45
|
+
return Infinity;
|
|
46
|
+
}
|
|
47
|
+
seen.add(item);
|
|
48
|
+
if (!Array.isArray(item) &&
|
|
49
|
+
Object.getPrototypeOf(item) !== Object.prototype &&
|
|
50
|
+
Object.getPrototypeOf(item) !== null) {
|
|
51
|
+
return Infinity;
|
|
52
|
+
}
|
|
53
|
+
for (const key of Object.keys(item)) {
|
|
54
|
+
bytes += key.length * 3;
|
|
55
|
+
const descriptor = Object.getOwnPropertyDescriptor(item, key);
|
|
56
|
+
if (!descriptor || descriptor.get || descriptor.set) {
|
|
57
|
+
return Infinity;
|
|
58
|
+
}
|
|
59
|
+
stack.push(descriptor.value);
|
|
60
|
+
if (stack.length > 100_000 || bytes > MAX_ENTRY_BYTES) {
|
|
61
|
+
return Infinity;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return bytes;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Settle IPC ownership once while publication retains the capture memory
|
|
69
|
+
* lease.
|
|
70
|
+
*/
|
|
71
|
+
function settle(id, result) {
|
|
72
|
+
const task = pending.get(id);
|
|
73
|
+
if (!task) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
pending.delete(id);
|
|
77
|
+
clearTimeout(task.timer);
|
|
78
|
+
task.resolve(result);
|
|
79
|
+
if (!pending.size) {
|
|
80
|
+
worker?.unref();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Fail every pending capture explicitly and back off without moving bulk
|
|
85
|
+
* work to the caller.
|
|
86
|
+
*/
|
|
87
|
+
function failWorker(current, reason) {
|
|
88
|
+
if (worker !== current) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
worker = undefined;
|
|
92
|
+
retryAfter = Date.now() + 5_000;
|
|
93
|
+
for (const id of pending.keys()) {
|
|
94
|
+
settle(id, {
|
|
95
|
+
error: reason,
|
|
96
|
+
stored: { bodyWriteFailed: true },
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
void current.terminate().catch(() => undefined);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Lazily create the bounded worker; only admitted processing keeps it
|
|
103
|
+
* referenced.
|
|
104
|
+
*/
|
|
105
|
+
function getWorker() {
|
|
106
|
+
if (worker) {
|
|
107
|
+
return worker;
|
|
108
|
+
}
|
|
109
|
+
const current = new Worker(workerUrl ?? new URL("./bodyCaptureWorkerEntry.js", import.meta.url), {
|
|
110
|
+
execArgv: process.execArgv.filter((arg) => !arg.startsWith("--input-type")),
|
|
111
|
+
resourceLimits: { maxOldGenerationSizeMb: 128 },
|
|
112
|
+
});
|
|
113
|
+
worker = current;
|
|
114
|
+
current.on("message", (message) => {
|
|
115
|
+
if (worker === current) {
|
|
116
|
+
settle(message.id, message.result);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
current.on("error", () => failWorker(current, "body_worker_error"));
|
|
120
|
+
current.on("exit", () => failWorker(current, "body_worker_exit"));
|
|
121
|
+
current.unref();
|
|
122
|
+
return current;
|
|
123
|
+
}
|
|
124
|
+
/** Bounded bulk capture. Failures are indexed; never fall back to blocking work. */
|
|
125
|
+
export async function captureProxyBody(entry, logDir, consume) {
|
|
126
|
+
snapshot.attempted += 1;
|
|
127
|
+
let bytes;
|
|
128
|
+
try {
|
|
129
|
+
bytes = estimateCloneBytes(entry);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
bytes = Infinity;
|
|
133
|
+
}
|
|
134
|
+
if (bytes > MAX_ENTRY_BYTES ||
|
|
135
|
+
snapshot.pending >= MAX_PENDING ||
|
|
136
|
+
snapshot.pendingBytes + bytes > MAX_PENDING_BYTES ||
|
|
137
|
+
Date.now() < retryAfter) {
|
|
138
|
+
snapshot.rejected += 1;
|
|
139
|
+
const error = bytes > MAX_ENTRY_BYTES
|
|
140
|
+
? "body_capture_too_large_or_non_json"
|
|
141
|
+
: Date.now() < retryAfter
|
|
142
|
+
? "body_worker_backoff"
|
|
143
|
+
: "body_capture_queue_full";
|
|
144
|
+
snapshot.lastError = error;
|
|
145
|
+
return consume({ error, stored: { bodyWriteFailed: true } });
|
|
146
|
+
}
|
|
147
|
+
let current;
|
|
148
|
+
try {
|
|
149
|
+
current = getWorker();
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
snapshot.failed += 1;
|
|
153
|
+
retryAfter = Date.now() + 5_000;
|
|
154
|
+
return consume({
|
|
155
|
+
error: "body_worker_start_failed",
|
|
156
|
+
stored: { bodyWriteFailed: true },
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
const id = ++nextId;
|
|
160
|
+
return new Promise((resolve) => {
|
|
161
|
+
const timer = setTimeout(() => failWorker(current, "body_worker_timeout"), PROXY_BODY_CAPTURE_DEADLINE_MS);
|
|
162
|
+
timer.unref();
|
|
163
|
+
pending.set(id, {
|
|
164
|
+
timer,
|
|
165
|
+
resolve: (result) => {
|
|
166
|
+
// Keep the byte/count lease through index writes and OTLP publication,
|
|
167
|
+
// so completed worker results cannot form an unbounded parent backlog.
|
|
168
|
+
void consume(result)
|
|
169
|
+
.catch(() => {
|
|
170
|
+
result.error ??= "body_capture_publication_failed";
|
|
171
|
+
})
|
|
172
|
+
.finally(() => {
|
|
173
|
+
snapshot.pending -= 1;
|
|
174
|
+
snapshot.pendingBytes -= bytes;
|
|
175
|
+
if (result.error || result.stored.bodyWriteFailed) {
|
|
176
|
+
snapshot.failed += 1;
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
snapshot.completed += 1;
|
|
180
|
+
}
|
|
181
|
+
if (result.error) {
|
|
182
|
+
snapshot.lastError = result.error;
|
|
183
|
+
}
|
|
184
|
+
resolve();
|
|
185
|
+
});
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
snapshot.pending += 1;
|
|
189
|
+
snapshot.pendingBytes += bytes;
|
|
190
|
+
current.ref();
|
|
191
|
+
try {
|
|
192
|
+
current.postMessage({ id, entry, logDir, queuedAt: Date.now() });
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
settle(id, {
|
|
196
|
+
error: "body_capture_clone_failed",
|
|
197
|
+
stored: { bodyWriteFailed: true },
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Return independent counters for processing, rejection, failure, and
|
|
204
|
+
* retained publication work.
|
|
205
|
+
*/
|
|
206
|
+
export function getBodyCaptureWorkerSnapshot() {
|
|
207
|
+
return { ...snapshot };
|
|
208
|
+
}
|
|
209
|
+
/** Isolated tests point at a separately executed built worker. */
|
|
210
|
+
export const __bodyCaptureWorkerTestHooks = {
|
|
211
|
+
/**
|
|
212
|
+
* Reset an isolated worker after capture publications drain, optionally selecting a fixture entry.
|
|
213
|
+
*/
|
|
214
|
+
async reset(url) {
|
|
215
|
+
if (worker) {
|
|
216
|
+
const current = worker;
|
|
217
|
+
failWorker(current, "body_worker_test_reset");
|
|
218
|
+
await current.terminate();
|
|
219
|
+
}
|
|
220
|
+
workerUrl = url;
|
|
221
|
+
retryAfter = 0;
|
|
222
|
+
Object.assign(snapshot, {
|
|
223
|
+
attempted: 0,
|
|
224
|
+
completed: 0,
|
|
225
|
+
rejected: 0,
|
|
226
|
+
failed: 0,
|
|
227
|
+
pending: 0,
|
|
228
|
+
pendingBytes: 0,
|
|
229
|
+
lastError: undefined,
|
|
230
|
+
});
|
|
231
|
+
},
|
|
232
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { parentPort } from "node:worker_threads";
|
|
2
|
+
import { performance } from "node:perf_hooks";
|
|
3
|
+
import { processProxyBodyCapture } from "./bodyCaptureProcessing.js";
|
|
4
|
+
// Sequential processing bounds serialization/compression memory. The parent
|
|
5
|
+
// bounds both the queued record count and clone bytes, including in-flight work.
|
|
6
|
+
let tail = Promise.resolve();
|
|
7
|
+
parentPort?.on("message", (message) => {
|
|
8
|
+
tail = tail.then(async () => {
|
|
9
|
+
const started = performance.now();
|
|
10
|
+
const queueWaitMs = Math.max(0, Date.now() - message.queuedAt);
|
|
11
|
+
try {
|
|
12
|
+
const result = await processProxyBodyCapture(message.entry, message.logDir);
|
|
13
|
+
parentPort?.postMessage({
|
|
14
|
+
id: message.id,
|
|
15
|
+
result: {
|
|
16
|
+
...result,
|
|
17
|
+
queueWaitMs,
|
|
18
|
+
processingMs: performance.now() - started,
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
parentPort?.postMessage({
|
|
24
|
+
id: message.id,
|
|
25
|
+
result: {
|
|
26
|
+
error: "body_capture_processing_failed",
|
|
27
|
+
stored: { bodyWriteFailed: true },
|
|
28
|
+
queueWaitMs,
|
|
29
|
+
processingMs: performance.now() - started,
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import { createReadStream } from "node:fs";
|
|
2
2
|
import { isDeepStrictEqual } from "node:util";
|
|
3
|
+
import { isProxyAuxiliaryRequest } from "./proxyRequestKind.js";
|
|
3
4
|
import { lstat, readdir, realpath, stat } from "node:fs/promises";
|
|
4
5
|
import { homedir } from "node:os";
|
|
5
6
|
import { createInterface } from "node:readline";
|
|
6
7
|
import { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
7
8
|
import { ACCOUNT_COOLING_REASONS, PROXY_ACCOUNT_TYPES, PROXY_ACCOUNT_ROUTING_MODES, PROXY_ACCOUNT_ROUTING_REASONS, PROXY_ACCOUNT_ROUTING_STRATEGIES, } from "./routingEvidence.js";
|
|
8
9
|
import { calculateCost, hasPricing, isExactPricingMatch, } from "../utils/pricing.js";
|
|
9
|
-
const LIFECYCLE_FILE_PATTERN = /^proxy-lifecycle-\d{4}-\d{2}-\d{2}\.jsonl$/;
|
|
10
|
+
const LIFECYCLE_FILE_PATTERN = /^proxy-(?:lifecycle|supervisor)-\d{4}-\d{2}-\d{2}\.jsonl$/;
|
|
10
11
|
const REQUEST_FILE_PATTERN = /^proxy-\d{4}-\d{2}-\d{2}\.jsonl$/;
|
|
11
12
|
const ATTEMPT_FILE_PATTERN = /^proxy-attempts-\d{4}-\d{2}-\d{2}\.jsonl$/;
|
|
12
13
|
const DEBUG_FILE_PATTERN = /^proxy-debug-\d{4}-\d{2}-\d{2}\.jsonl$/;
|
|
13
14
|
const ARTIFACT_STAT_CONCURRENCY = 64;
|
|
14
15
|
const LIFECYCLE_EVENTS = new Set([
|
|
16
|
+
"runtime_sample",
|
|
17
|
+
"supervisor_event",
|
|
15
18
|
"request_accepted",
|
|
16
19
|
"response_headers",
|
|
17
20
|
"response_first_chunk",
|
|
@@ -585,6 +588,7 @@ export async function analyzeProxyLogs(options) {
|
|
|
585
588
|
let malformedLines = 0;
|
|
586
589
|
let unsupportedLifecycleLines = 0;
|
|
587
590
|
const accepted = new Set();
|
|
591
|
+
const auxiliaryRequests = new Set();
|
|
588
592
|
const headers = new Set();
|
|
589
593
|
const firstChunks = new Set();
|
|
590
594
|
const terminal = new Set();
|
|
@@ -595,18 +599,36 @@ export async function analyzeProxyLogs(options) {
|
|
|
595
599
|
const firstChunkLatencyByRequest = new Map();
|
|
596
600
|
const terminalLatencyByRequest = new Map();
|
|
597
601
|
const sequences = new Map();
|
|
602
|
+
const selectedSequenceRanges = new Map();
|
|
598
603
|
const seenLifecycleEvents = new Map();
|
|
599
604
|
let conflictingLifecycleDuplicates = 0;
|
|
600
605
|
const conflictedRequests = new Set();
|
|
601
606
|
const terminalRecords = new Map();
|
|
607
|
+
const acceptedWorkers = new Map();
|
|
608
|
+
const admittedWorkerIds = new Set();
|
|
609
|
+
const conflictedWorkerExits = new Set();
|
|
610
|
+
const conflictingIdentities = new Set();
|
|
611
|
+
const runtimeRecords = new Map();
|
|
612
|
+
const workerExits = new Map();
|
|
613
|
+
const runtime = {
|
|
614
|
+
samples: 0,
|
|
615
|
+
maxEventLoopDelayMs: null,
|
|
616
|
+
maxRssBytes: null,
|
|
617
|
+
maxCpuPercentOneCore: null,
|
|
618
|
+
maxHostLoad1m: null,
|
|
619
|
+
};
|
|
602
620
|
for (const filePath of lifecycleFiles) {
|
|
603
621
|
linesRead += await readJsonLines(filePath, (record) => {
|
|
604
|
-
const
|
|
622
|
+
const operational = record.event === "runtime_sample" ||
|
|
623
|
+
record.event === "supervisor_event";
|
|
624
|
+
const operationalTimestamp = Date.parse(String(record.timestamp));
|
|
625
|
+
const timestamp = operational
|
|
626
|
+
? Number.isFinite(operationalTimestamp)
|
|
627
|
+
? operationalTimestamp
|
|
628
|
+
: null
|
|
629
|
+
: observeTimestamp("lifecycle", record);
|
|
605
630
|
const requestId = stringValue(record.requestId);
|
|
606
|
-
if (timestamp === null ||
|
|
607
|
-
!requestId ||
|
|
608
|
-
(!accepted.has(requestId) &&
|
|
609
|
-
(timestamp < sinceMs || timestamp > untilMs))) {
|
|
631
|
+
if (timestamp === null || !requestId) {
|
|
610
632
|
return;
|
|
611
633
|
}
|
|
612
634
|
const event = stringValue(record.event);
|
|
@@ -614,7 +636,10 @@ export async function analyzeProxyLogs(options) {
|
|
|
614
636
|
!event ||
|
|
615
637
|
!LIFECYCLE_EVENTS.has(event) ||
|
|
616
638
|
!requestId) {
|
|
617
|
-
|
|
639
|
+
if (accepted.has(requestId) ||
|
|
640
|
+
(timestamp >= sinceMs && timestamp <= untilMs)) {
|
|
641
|
+
unsupportedLifecycleLines += 1;
|
|
642
|
+
}
|
|
618
643
|
return;
|
|
619
644
|
}
|
|
620
645
|
const processId = stringValue(record.processInstanceId);
|
|
@@ -623,11 +648,40 @@ export async function analyzeProxyLogs(options) {
|
|
|
623
648
|
const values = sequences.get(processId) ?? [];
|
|
624
649
|
values.push(sequence);
|
|
625
650
|
sequences.set(processId, values);
|
|
651
|
+
}
|
|
652
|
+
const details = record.supervisorEvent && typeof record.supervisorEvent === "object"
|
|
653
|
+
? record.supervisorEvent
|
|
654
|
+
: undefined;
|
|
655
|
+
const exitedWorkerId = stringValue(details?.workerProcessInstanceId);
|
|
656
|
+
const relatedExit = event === "supervisor_event" &&
|
|
657
|
+
details?.type === "worker_exit" &&
|
|
658
|
+
exitedWorkerId &&
|
|
659
|
+
admittedWorkerIds.has(exitedWorkerId);
|
|
660
|
+
// Audit intervening sequences before selecting the request cohort.
|
|
661
|
+
if (!accepted.has(requestId) &&
|
|
662
|
+
!relatedExit &&
|
|
663
|
+
(timestamp < sinceMs || timestamp > untilMs)) {
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
if (processId && sequence !== null && Number.isInteger(sequence)) {
|
|
667
|
+
const range = selectedSequenceRanges.get(processId);
|
|
668
|
+
selectedSequenceRanges.set(processId, {
|
|
669
|
+
min: Math.min(range?.min ?? sequence, sequence),
|
|
670
|
+
max: Math.max(range?.max ?? sequence, sequence),
|
|
671
|
+
});
|
|
626
672
|
const identity = `${processId}:${sequence}`;
|
|
627
673
|
const previous = seenLifecycleEvents.get(identity);
|
|
628
674
|
if (previous) {
|
|
629
675
|
if (!isDeepStrictEqual(previous, record)) {
|
|
630
676
|
conflictingLifecycleDuplicates += 1;
|
|
677
|
+
conflictingIdentities.add(identity);
|
|
678
|
+
for (const copy of [previous, record]) {
|
|
679
|
+
const detail = copy.supervisorEvent;
|
|
680
|
+
const workerId = stringValue(detail?.workerProcessInstanceId);
|
|
681
|
+
if (workerId) {
|
|
682
|
+
conflictedWorkerExits.add(workerId);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
631
685
|
conflictedRequests.add(requestId);
|
|
632
686
|
const previousRequestId = stringValue(previous.requestId);
|
|
633
687
|
if (previousRequestId) {
|
|
@@ -638,9 +692,36 @@ export async function analyzeProxyLogs(options) {
|
|
|
638
692
|
}
|
|
639
693
|
seenLifecycleEvents.set(identity, record);
|
|
640
694
|
}
|
|
695
|
+
if (event === "runtime_sample") {
|
|
696
|
+
if (record.runtimeSample &&
|
|
697
|
+
typeof record.runtimeSample === "object" &&
|
|
698
|
+
processId &&
|
|
699
|
+
sequence !== null) {
|
|
700
|
+
runtimeRecords.set(`${processId}:${sequence}`, record.runtimeSample);
|
|
701
|
+
}
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
if (event === "supervisor_event") {
|
|
705
|
+
if (details?.type === "worker_exit" && exitedWorkerId) {
|
|
706
|
+
const exit = { ...details, at: record.timestamp };
|
|
707
|
+
const previous = workerExits.get(exitedWorkerId);
|
|
708
|
+
if (previous && !isDeepStrictEqual(previous, exit)) {
|
|
709
|
+
conflictedWorkerExits.add(exitedWorkerId);
|
|
710
|
+
}
|
|
711
|
+
workerExits.set(exitedWorkerId, exit);
|
|
712
|
+
}
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
if (isProxyAuxiliaryRequest(stringValue(record.method) ?? "", stringValue(record.path) ?? "")) {
|
|
716
|
+
auxiliaryRequests.add(requestId);
|
|
717
|
+
}
|
|
641
718
|
const elapsed = finiteNumber(record.elapsedMs);
|
|
642
719
|
if (event === "request_accepted") {
|
|
643
720
|
accepted.add(requestId);
|
|
721
|
+
if (processId) {
|
|
722
|
+
acceptedWorkers.set(requestId, processId);
|
|
723
|
+
admittedWorkerIds.add(processId);
|
|
724
|
+
}
|
|
644
725
|
}
|
|
645
726
|
else if (event === "response_headers") {
|
|
646
727
|
if (headers.has(requestId)) {
|
|
@@ -683,6 +764,25 @@ export async function analyzeProxyLogs(options) {
|
|
|
683
764
|
malformedLines += 1;
|
|
684
765
|
});
|
|
685
766
|
}
|
|
767
|
+
for (const [identity, sample] of runtimeRecords) {
|
|
768
|
+
if (conflictingIdentities.has(identity)) {
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
771
|
+
runtime.samples += 1;
|
|
772
|
+
const mapping = {
|
|
773
|
+
maxEventLoopDelayMs: "eventLoopDelayMaxMs",
|
|
774
|
+
maxRssBytes: "rssBytes",
|
|
775
|
+
maxCpuPercentOneCore: "cpuPercentOneCore",
|
|
776
|
+
maxHostLoad1m: "hostLoad1m",
|
|
777
|
+
};
|
|
778
|
+
for (const [key, source] of Object.entries(mapping)) {
|
|
779
|
+
const value = finiteNumber(sample[source]);
|
|
780
|
+
if (value !== null && value >= 0) {
|
|
781
|
+
const target = key;
|
|
782
|
+
runtime[target] = Math.max(runtime[target] ?? value, value);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
686
786
|
// Contradictory copies are not reliable latency samples. Keep their data
|
|
687
787
|
// quality count, but do not choose one timing arbitrarily.
|
|
688
788
|
const verifiedLatencies = (values) => [...values]
|
|
@@ -693,7 +793,12 @@ export async function analyzeProxyLogs(options) {
|
|
|
693
793
|
const terminalLatency = verifiedLatencies(terminalLatencyByRequest);
|
|
694
794
|
let lifecycleSequenceGaps = 0;
|
|
695
795
|
let lifecycleSequenceDuplicates = 0;
|
|
696
|
-
for (const
|
|
796
|
+
for (const [processId, allValues] of sequences) {
|
|
797
|
+
const range = selectedSequenceRanges.get(processId);
|
|
798
|
+
if (!range) {
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
const values = allValues.filter((value) => value >= range.min && value <= range.max);
|
|
697
802
|
values.sort((a, b) => a - b);
|
|
698
803
|
for (let index = 1; index < values.length; index += 1) {
|
|
699
804
|
const difference = values[index] - values[index - 1];
|
|
@@ -918,6 +1023,9 @@ export async function analyzeProxyLogs(options) {
|
|
|
918
1023
|
for (const [requestId, record] of terminalRecords) {
|
|
919
1024
|
const final = finalRequests.get(requestId);
|
|
920
1025
|
const recorded = stringValue(record.terminalOutcome) ?? "unknown";
|
|
1026
|
+
const auxiliaryTransport = auxiliaryRequests.has(requestId)
|
|
1027
|
+
? (stringValue(record.transportOutcome) ?? recorded)
|
|
1028
|
+
: null;
|
|
921
1029
|
const resolved = final
|
|
922
1030
|
? final.status === 499 || final.errorType === "client_cancelled"
|
|
923
1031
|
? "client_cancelled"
|
|
@@ -927,11 +1035,20 @@ export async function analyzeProxyLogs(options) {
|
|
|
927
1035
|
: final.status >= 400 || final.errorType
|
|
928
1036
|
? "handler_error"
|
|
929
1037
|
: "completed"
|
|
930
|
-
: conflictedRequests.has(requestId)
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
1038
|
+
: auxiliaryTransport && !conflictedRequests.has(requestId)
|
|
1039
|
+
? auxiliaryTransport === "completed" ||
|
|
1040
|
+
auxiliaryTransport === "bodyless"
|
|
1041
|
+
? finiteNumber(record.responseStatus) === null
|
|
1042
|
+
? "unknown"
|
|
1043
|
+
: Number(record.responseStatus) >= 400
|
|
1044
|
+
? "handler_error"
|
|
1045
|
+
: auxiliaryTransport
|
|
1046
|
+
: auxiliaryTransport
|
|
1047
|
+
: conflictedRequests.has(requestId) ||
|
|
1048
|
+
recorded === "completed" ||
|
|
1049
|
+
recorded === "bodyless"
|
|
1050
|
+
? "unknown"
|
|
1051
|
+
: recorded;
|
|
935
1052
|
if (final &&
|
|
936
1053
|
recorded !== resolved &&
|
|
937
1054
|
!(recorded === "bodyless" && resolved === "completed")) {
|
|
@@ -1038,10 +1155,8 @@ export async function analyzeProxyLogs(options) {
|
|
|
1038
1155
|
conflictingLifecycleDuplicates,
|
|
1039
1156
|
duplicateAttempts,
|
|
1040
1157
|
finalOutcomeConflicts,
|
|
1041
|
-
acceptedWithoutFinal: [...accepted].filter((id) => !finalRequests.has(id))
|
|
1042
|
-
|
|
1043
|
-
terminalWithoutFinal: [...terminal].filter((id) => !finalRequests.has(id))
|
|
1044
|
-
.length,
|
|
1158
|
+
acceptedWithoutFinal: [...accepted].filter((id) => !finalRequests.has(id) && !auxiliaryRequests.has(id)).length,
|
|
1159
|
+
terminalWithoutFinal: [...terminal].filter((id) => !finalRequests.has(id) && !auxiliaryRequests.has(id)).length,
|
|
1045
1160
|
streams: Object.fromEntries(Object.entries(observedRanges).map(([stream, range]) => [
|
|
1046
1161
|
stream,
|
|
1047
1162
|
{
|
|
@@ -1066,8 +1181,35 @@ export async function analyzeProxyLogs(options) {
|
|
|
1066
1181
|
absent: absentRoutingDecisions,
|
|
1067
1182
|
},
|
|
1068
1183
|
},
|
|
1184
|
+
runtime,
|
|
1069
1185
|
lifecycle: {
|
|
1186
|
+
unconfirmedAtWorkerExit: [...accepted].flatMap((requestId) => {
|
|
1187
|
+
const workerProcessInstanceId = acceptedWorkers.get(requestId);
|
|
1188
|
+
const exit = workerProcessInstanceId
|
|
1189
|
+
? workerExits.get(workerProcessInstanceId)
|
|
1190
|
+
: undefined;
|
|
1191
|
+
if (!exit ||
|
|
1192
|
+
!workerProcessInstanceId ||
|
|
1193
|
+
conflictedWorkerExits.has(workerProcessInstanceId) ||
|
|
1194
|
+
terminal.has(requestId) ||
|
|
1195
|
+
conflictedRequests.has(requestId)) {
|
|
1196
|
+
return [];
|
|
1197
|
+
}
|
|
1198
|
+
return [
|
|
1199
|
+
{
|
|
1200
|
+
requestId,
|
|
1201
|
+
workerProcessInstanceId,
|
|
1202
|
+
at: String(exit.at),
|
|
1203
|
+
workerExitCode: finiteNumber(exit.workerExitCode),
|
|
1204
|
+
workerExitSignal: stringValue(exit.workerExitSignal),
|
|
1205
|
+
// A provider final cannot prove the client received the entire body.
|
|
1206
|
+
providerFinalRecorded: finalRequests.has(requestId),
|
|
1207
|
+
},
|
|
1208
|
+
];
|
|
1209
|
+
}),
|
|
1070
1210
|
accepted: accepted.size,
|
|
1211
|
+
auxiliaryRequests: [...accepted].filter((id) => auxiliaryRequests.has(id))
|
|
1212
|
+
.length,
|
|
1071
1213
|
headers: headers.size,
|
|
1072
1214
|
firstChunks: firstChunks.size,
|
|
1073
1215
|
terminal: terminal.size,
|
|
@@ -1,13 +1,38 @@
|
|
|
1
|
+
import { chmodSync } from "node:fs";
|
|
1
2
|
import { appendFile } from "node:fs/promises";
|
|
2
3
|
import type { ProxyLifecycleEventInput, ProxyLifecycleLoggerOptions, ProxyLifecycleLoggerSnapshot } from "../types/index.js";
|
|
3
4
|
export declare function hashProxyLifecycleSessionId(sessionId: string | undefined): string | undefined;
|
|
5
|
+
/**
|
|
6
|
+
* Configure private journal storage while retaining required admission
|
|
7
|
+
* when initialization fails.
|
|
8
|
+
*/
|
|
4
9
|
export declare function configureProxyLifecycleLogger(options: ProxyLifecycleLoggerOptions): void;
|
|
5
10
|
/** Enqueue fixed-size lifecycle metadata without awaiting filesystem work. */
|
|
6
11
|
export declare function logProxyLifecycleEvent(input: ProxyLifecycleEventInput): void;
|
|
12
|
+
/** Confirm admission before upstream dispatch, without waiting for later traffic.
|
|
13
|
+
* Disabled logging is explicit; an enabled but unhealthy sink refuses dispatch.
|
|
14
|
+
* A timeout never retries an ambiguous provider request or the pending append.
|
|
15
|
+
*/
|
|
16
|
+
export declare function persistProxyLifecycleAcceptance(input: Omit<ProxyLifecycleEventInput, "event">, timeoutMs?: number): Promise<void>;
|
|
7
17
|
export declare function flushProxyLifecycleEvents(timeoutMs?: number): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Expose journal accounting, process identity, and outstanding writes
|
|
20
|
+
* without changing them.
|
|
21
|
+
*/
|
|
8
22
|
export declare function getProxyLifecycleLoggerSnapshot(): ProxyLifecycleLoggerSnapshot;
|
|
23
|
+
/**
|
|
24
|
+
* Reset timers, accounting, and injected I/O after isolated tests have
|
|
25
|
+
* drained their work.
|
|
26
|
+
*/
|
|
9
27
|
export declare function resetProxyLifecycleLoggerForTests(): void;
|
|
10
28
|
/** Isolated failure injection for lifecycle durability tests. */
|
|
11
29
|
export declare const __proxyLifecycleTestHooks: {
|
|
30
|
+
/**
|
|
31
|
+
* Inject directory-hardening failures without altering real filesystem permissions.
|
|
32
|
+
*/
|
|
33
|
+
setChmodForTests(chmod: typeof chmodSync): void;
|
|
34
|
+
/**
|
|
35
|
+
* Inject controlled append outcomes while preserving the production admission and queue paths.
|
|
36
|
+
*/
|
|
12
37
|
setAppendFileForTests(append: typeof appendFile): void;
|
|
13
38
|
};
|