@loadstrike/loadstrike-sdk 1.0.30201 → 1.0.31001
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/README.md +28 -0
- package/dist/cjs/cluster.js +2417 -7
- package/dist/cjs/index.js +13 -2
- package/dist/cjs/iteration-observation-diagnostics.js +513 -0
- package/dist/cjs/iteration-observations.js +884 -0
- package/dist/cjs/load-engine-v2.js +966 -0
- package/dist/cjs/local.js +128 -30
- package/dist/cjs/reporting.js +148 -19
- package/dist/cjs/runtime.js +2514 -196
- package/dist/cjs/sink-retry-policy.js +52 -0
- package/dist/cjs/sinks.js +580 -23
- package/dist/cjs/transports.js +154 -163
- package/dist/esm/cluster.js +2386 -7
- package/dist/esm/index.js +1 -0
- package/dist/esm/iteration-observation-diagnostics.js +508 -0
- package/dist/esm/iteration-observations.js +871 -0
- package/dist/esm/load-engine-v2.js +942 -0
- package/dist/esm/local.js +128 -30
- package/dist/esm/reporting.js +148 -19
- package/dist/esm/runtime.js +2515 -197
- package/dist/esm/sink-retry-policy.js +44 -0
- package/dist/esm/sinks.js +580 -23
- package/dist/esm/transports.js +154 -163
- package/dist/types/cluster.d.ts +379 -1
- package/dist/types/index.d.ts +3 -1
- package/dist/types/iteration-observation-diagnostics.d.ts +21 -0
- package/dist/types/iteration-observations.d.ts +230 -0
- package/dist/types/load-engine-v2.d.ts +147 -0
- package/dist/types/runtime.d.ts +216 -8
- package/dist/types/sink-retry-policy.d.ts +9 -0
- package/dist/types/sinks.d.ts +73 -0
- package/dist/types/transports.d.ts +6 -8
- package/package.json +3 -4
|
@@ -0,0 +1,884 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.IterationObservationReporter = exports.ProcessIterationObservationBuffer = exports.DEFAULT_ITERATION_OBSERVATION_SETTINGS = exports.ITERATION_OBSERVATION_STREAM_COMPLETION_SCHEMA_VERSION = exports.ITERATION_OBSERVATION_BATCH_SCHEMA_VERSION = exports.ITERATION_OBSERVATION_SCHEMA_VERSION = void 0;
|
|
4
|
+
exports.utcNowNs = utcNowNs;
|
|
5
|
+
exports.deriveIterationId = deriveIterationId;
|
|
6
|
+
exports.deriveObservationId = deriveObservationId;
|
|
7
|
+
exports.deriveIterationBatchId = deriveIterationBatchId;
|
|
8
|
+
exports.createIterationStepObservation = createIterationStepObservation;
|
|
9
|
+
exports.createIterationObservation = createIterationObservation;
|
|
10
|
+
exports.serializeIterationObservationBatchGzipJson = serializeIterationObservationBatchGzipJson;
|
|
11
|
+
exports.validateIterationObservationSettings = validateIterationObservationSettings;
|
|
12
|
+
const node_crypto_1 = require("node:crypto");
|
|
13
|
+
const node_perf_hooks_1 = require("node:perf_hooks");
|
|
14
|
+
const node_zlib_1 = require("node:zlib");
|
|
15
|
+
const iteration_observation_diagnostics_js_1 = require("./iteration-observation-diagnostics.js");
|
|
16
|
+
const sink_retry_policy_js_1 = require("./sink-retry-policy.js");
|
|
17
|
+
exports.ITERATION_OBSERVATION_SCHEMA_VERSION = "loadstrike.iteration-observation/1";
|
|
18
|
+
exports.ITERATION_OBSERVATION_BATCH_SCHEMA_VERSION = "loadstrike.iteration-batch/1";
|
|
19
|
+
exports.ITERATION_OBSERVATION_STREAM_COMPLETION_SCHEMA_VERSION = "loadstrike.iteration-stream-completion/1";
|
|
20
|
+
exports.DEFAULT_ITERATION_OBSERVATION_SETTINGS = Object.freeze({
|
|
21
|
+
flushIntervalMs: 5000,
|
|
22
|
+
maxBufferBytes: 256 * 1024 * 1024,
|
|
23
|
+
maxObservationsPerBatch: 50000,
|
|
24
|
+
maxBatchBytes: 8 * 1024 * 1024,
|
|
25
|
+
sinkQueueDepth: 4,
|
|
26
|
+
sinkParallelism: 2,
|
|
27
|
+
drainTimeoutMs: 30000
|
|
28
|
+
});
|
|
29
|
+
const BATCH_ENVELOPE_RESERVED_BYTES = 2048;
|
|
30
|
+
const defaultClock = {
|
|
31
|
+
nowUtcNs: utcNowNs,
|
|
32
|
+
setInterval(callback, intervalMs) {
|
|
33
|
+
const timer = setInterval(callback, intervalMs);
|
|
34
|
+
timer.unref?.();
|
|
35
|
+
return timer;
|
|
36
|
+
},
|
|
37
|
+
clearInterval(handle) {
|
|
38
|
+
clearInterval(handle);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
function utcNowNs() {
|
|
42
|
+
return BigInt(Date.now()) * 1000000n;
|
|
43
|
+
}
|
|
44
|
+
function deriveIterationId(runId, resultOwnerId, scenarioName, simulationIndex, globalOrdinal64) {
|
|
45
|
+
return derivePrefixedSha256("lsi_", `${runId}\n${resultOwnerId}\n${scenarioName}\n${Math.trunc(simulationIndex)}\n${decimalString(globalOrdinal64)}`);
|
|
46
|
+
}
|
|
47
|
+
function deriveObservationId(iterationId, attemptIndex) {
|
|
48
|
+
return derivePrefixedSha256("lso_", `${iterationId}\n${Math.max(Math.trunc(attemptIndex), 0)}`);
|
|
49
|
+
}
|
|
50
|
+
function deriveIterationBatchId(runId, resultOwnerId, batchSequence64) {
|
|
51
|
+
return derivePrefixedSha256("lsb_", `${runId}\n${resultOwnerId}\n${decimalString(batchSequence64)}`);
|
|
52
|
+
}
|
|
53
|
+
function createIterationStepObservation(input) {
|
|
54
|
+
return {
|
|
55
|
+
stepName: String(input.stepName),
|
|
56
|
+
sortIndex: Math.max(Math.trunc(input.sortIndex), 0),
|
|
57
|
+
startedUtcNs: nonNegativeDecimalString(input.startedUtcNs),
|
|
58
|
+
completedUtcNs: nonNegativeDecimalString(input.completedUtcNs),
|
|
59
|
+
observedLatencyUs64: nonNegativeDecimalString(input.observedLatencyUs64),
|
|
60
|
+
reportedLatencyUs64: nonNegativeDecimalString(input.reportedLatencyUs64),
|
|
61
|
+
isSuccess: Boolean(input.isSuccess),
|
|
62
|
+
statusCode: truncateUtf8(String(input.statusCode ?? ""), 64),
|
|
63
|
+
sizeBytes64: nonNegativeDecimalString(input.sizeBytes64)
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function createIterationObservation(input) {
|
|
67
|
+
const globalOrdinal64 = nonNegativeDecimalString(input.globalOrdinal64);
|
|
68
|
+
const globalSecondaryOrdinal64 = nonNegativeDecimalString(input.globalSecondaryOrdinal64 ?? 0);
|
|
69
|
+
const simulationIndex = Math.trunc(input.simulationIndex);
|
|
70
|
+
const attemptIndex = Math.max(Math.trunc(input.attemptIndex), 0);
|
|
71
|
+
const iterationId = input.iterationId ?? deriveIterationId(String(input.runId), String(input.resultOwnerId), String(input.scenarioName), simulationIndex, globalOrdinal64);
|
|
72
|
+
if (!iterationId) {
|
|
73
|
+
throw new Error("Iteration identity must not be empty.");
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
schemaVersion: exports.ITERATION_OBSERVATION_SCHEMA_VERSION,
|
|
77
|
+
observationId: deriveObservationId(iterationId, attemptIndex),
|
|
78
|
+
runId: String(input.runId),
|
|
79
|
+
sessionId: String(input.sessionId),
|
|
80
|
+
resultOwnerId: String(input.resultOwnerId),
|
|
81
|
+
processGroup: Math.max(Math.trunc(input.processGroup), 0),
|
|
82
|
+
scenarioName: String(input.scenarioName),
|
|
83
|
+
scenarioIndex: Math.max(Math.trunc(input.scenarioIndex), 0),
|
|
84
|
+
simulationIndex,
|
|
85
|
+
simulationKind: String(input.simulationKind || "SingleInvocation"),
|
|
86
|
+
phase: input.phase === "warmup" ? "warmup" : "bombing",
|
|
87
|
+
globalOrdinal64,
|
|
88
|
+
globalSecondaryOrdinal64,
|
|
89
|
+
shardIndex: Math.max(Math.trunc(input.shardIndex), 0),
|
|
90
|
+
shardCount: Math.max(Math.trunc(input.shardCount), 1),
|
|
91
|
+
iterationId,
|
|
92
|
+
attemptIndex,
|
|
93
|
+
isFinalAttempt: Boolean(input.isFinalAttempt),
|
|
94
|
+
startedUtcNs: nonNegativeDecimalString(input.startedUtcNs),
|
|
95
|
+
completedUtcNs: nonNegativeDecimalString(input.completedUtcNs),
|
|
96
|
+
observedLatencyUs64: nonNegativeDecimalString(input.observedLatencyUs64),
|
|
97
|
+
reportedLatencyUs64: nonNegativeDecimalString(input.reportedLatencyUs64),
|
|
98
|
+
isSuccess: Boolean(input.isSuccess),
|
|
99
|
+
statusCode: truncateUtf8(String(input.statusCode ?? ""), 64),
|
|
100
|
+
sizeBytes64: nonNegativeDecimalString(input.sizeBytes64),
|
|
101
|
+
steps: input.steps.map((step) => createIterationStepObservation(step))
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function serializeIterationObservationBatchGzipJson(value) {
|
|
105
|
+
return (0, node_zlib_1.gzipSync)(Buffer.from(JSON.stringify(value), "utf8"));
|
|
106
|
+
}
|
|
107
|
+
class ProcessIterationObservationBuffer {
|
|
108
|
+
constructor() {
|
|
109
|
+
this.streams = new Map();
|
|
110
|
+
this.streamLimits = new Map();
|
|
111
|
+
this.totalBytes = 0;
|
|
112
|
+
}
|
|
113
|
+
get byteLength() {
|
|
114
|
+
return this.totalBytes;
|
|
115
|
+
}
|
|
116
|
+
register(streamKey, maxBufferBytes) {
|
|
117
|
+
this.streamLimits.set(streamKey, maxBufferBytes);
|
|
118
|
+
}
|
|
119
|
+
offer(streamKey, observation, maxBufferBytes, encodedBytes) {
|
|
120
|
+
this.streamLimits.set(streamKey, maxBufferBytes);
|
|
121
|
+
const processLimit = this.effectiveLimit(maxBufferBytes);
|
|
122
|
+
const jsonBytes = encodedBytes ?? Buffer.byteLength(JSON.stringify(observation), "utf8");
|
|
123
|
+
if (jsonBytes > processLimit || this.totalBytes + jsonBytes > processLimit) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
const stream = this.streams.get(streamKey) ?? [];
|
|
127
|
+
stream.push({ observation, jsonBytes });
|
|
128
|
+
this.streams.set(streamKey, stream);
|
|
129
|
+
this.totalBytes += jsonBytes;
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
drain(streamKey) {
|
|
133
|
+
const stream = this.streams.get(streamKey) ?? [];
|
|
134
|
+
this.streams.delete(streamKey);
|
|
135
|
+
for (const item of stream) {
|
|
136
|
+
this.totalBytes -= item.jsonBytes;
|
|
137
|
+
}
|
|
138
|
+
this.totalBytes = Math.max(this.totalBytes, 0);
|
|
139
|
+
return stream;
|
|
140
|
+
}
|
|
141
|
+
release(streamKey) {
|
|
142
|
+
this.drain(streamKey);
|
|
143
|
+
}
|
|
144
|
+
unregister(streamKey) {
|
|
145
|
+
this.drain(streamKey);
|
|
146
|
+
this.streamLimits.delete(streamKey);
|
|
147
|
+
}
|
|
148
|
+
effectiveLimit(fallback) {
|
|
149
|
+
let limit = fallback;
|
|
150
|
+
for (const value of this.streamLimits.values()) {
|
|
151
|
+
limit = Math.min(limit, value);
|
|
152
|
+
}
|
|
153
|
+
return limit;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
exports.ProcessIterationObservationBuffer = ProcessIterationObservationBuffer;
|
|
157
|
+
const processIterationObservationBuffer = new ProcessIterationObservationBuffer();
|
|
158
|
+
class SinkDispatcher {
|
|
159
|
+
constructor(target, depth, parallelism, retryCount, retryBackoffMs, logger, runId, resultOwnerId, onDropped) {
|
|
160
|
+
this.target = target;
|
|
161
|
+
this.depth = depth;
|
|
162
|
+
this.parallelism = parallelism;
|
|
163
|
+
this.retryCount = retryCount;
|
|
164
|
+
this.retryBackoffMs = retryBackoffMs;
|
|
165
|
+
this.logger = logger;
|
|
166
|
+
this.runId = runId;
|
|
167
|
+
this.resultOwnerId = resultOwnerId;
|
|
168
|
+
this.onDropped = onDropped;
|
|
169
|
+
this.queue = [];
|
|
170
|
+
this.active = new Set();
|
|
171
|
+
this.idleWaiters = new Set();
|
|
172
|
+
this.timedOut = false;
|
|
173
|
+
this.delivered = 0n;
|
|
174
|
+
this.dropped = 0n;
|
|
175
|
+
this.drainDeadlineMs = null;
|
|
176
|
+
this.pendingRetryDelays = new Set();
|
|
177
|
+
}
|
|
178
|
+
get deliveredCount() {
|
|
179
|
+
return this.delivered;
|
|
180
|
+
}
|
|
181
|
+
get droppedCount() {
|
|
182
|
+
return this.dropped;
|
|
183
|
+
}
|
|
184
|
+
get canPublishCompletion() {
|
|
185
|
+
return !this.timedOut && this.queue.length === 0 && this.active.size === 0;
|
|
186
|
+
}
|
|
187
|
+
enqueue(batch) {
|
|
188
|
+
if (this.timedOut || this.queue.length >= this.depth) {
|
|
189
|
+
this.drop("observation_sink_overflow", batch.observations);
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
this.queue.push({ batch, count: BigInt(batch.observations.length) });
|
|
193
|
+
this.pump();
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
async waitForIdle(timeoutMs) {
|
|
197
|
+
if (this.isIdle()) {
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
let timer;
|
|
201
|
+
let waiter;
|
|
202
|
+
const idle = new Promise((resolve) => {
|
|
203
|
+
waiter = () => {
|
|
204
|
+
if (timer)
|
|
205
|
+
clearTimeout(timer);
|
|
206
|
+
this.idleWaiters.delete(waiter);
|
|
207
|
+
resolve(true);
|
|
208
|
+
};
|
|
209
|
+
this.idleWaiters.add(waiter);
|
|
210
|
+
});
|
|
211
|
+
const timeout = new Promise((resolve) => {
|
|
212
|
+
timer = setTimeout(() => resolve(false), Math.max(timeoutMs, 1));
|
|
213
|
+
});
|
|
214
|
+
const completed = await Promise.race([idle, timeout]);
|
|
215
|
+
if (timer) {
|
|
216
|
+
clearTimeout(timer);
|
|
217
|
+
}
|
|
218
|
+
if (!completed) {
|
|
219
|
+
if (waiter)
|
|
220
|
+
this.idleWaiters.delete(waiter);
|
|
221
|
+
this.timeoutOutstanding();
|
|
222
|
+
}
|
|
223
|
+
return completed;
|
|
224
|
+
}
|
|
225
|
+
beginDrain(deadlineMs) {
|
|
226
|
+
this.drainDeadlineMs = deadlineMs;
|
|
227
|
+
const delaysThatCannotFit = Array.from(this.pendingRetryDelays)
|
|
228
|
+
.filter((pending) => pending.dueAtMs >= deadlineMs);
|
|
229
|
+
if (delaysThatCannotFit.length > 0) {
|
|
230
|
+
for (const pending of delaysThatCannotFit) {
|
|
231
|
+
pending.cancel();
|
|
232
|
+
}
|
|
233
|
+
this.timeoutOutstanding();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
pump() {
|
|
237
|
+
while (!this.timedOut && this.active.size < this.parallelism && this.queue.length > 0) {
|
|
238
|
+
const item = this.queue.shift();
|
|
239
|
+
this.active.add(item);
|
|
240
|
+
Promise.resolve()
|
|
241
|
+
.then(() => this.deliver(item))
|
|
242
|
+
.then((outcome) => {
|
|
243
|
+
if (outcome === "success" && !this.timedOut) {
|
|
244
|
+
this.delivered += item.count;
|
|
245
|
+
}
|
|
246
|
+
else if (outcome === "exhausted" && !this.timedOut) {
|
|
247
|
+
this.drop("observation_sink_delivery_failed", item.batch.observations);
|
|
248
|
+
}
|
|
249
|
+
})
|
|
250
|
+
.catch(() => {
|
|
251
|
+
if (!this.timedOut) {
|
|
252
|
+
this.drop("observation_sink_delivery_failed", item.batch.observations);
|
|
253
|
+
}
|
|
254
|
+
})
|
|
255
|
+
.finally(() => {
|
|
256
|
+
this.active.delete(item);
|
|
257
|
+
this.pump();
|
|
258
|
+
this.notifyIdle();
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
this.notifyIdle();
|
|
262
|
+
}
|
|
263
|
+
async deliver(item) {
|
|
264
|
+
const maximumAttempts = this.retryCount + 1;
|
|
265
|
+
for (let attempt = 1; attempt <= maximumAttempts; attempt += 1) {
|
|
266
|
+
if (this.timedOut || this.deadlineExpired()) {
|
|
267
|
+
return "deadline";
|
|
268
|
+
}
|
|
269
|
+
try {
|
|
270
|
+
await this.target.saveIterationBatch(item.batch);
|
|
271
|
+
if (attempt > 1) {
|
|
272
|
+
(0, iteration_observation_diagnostics_js_1.logIterationObservationRecovery)(this.logger, {
|
|
273
|
+
...this.diagnosticContext(item, attempt, maximumAttempts),
|
|
274
|
+
nextDelayMs: 0
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
return "success";
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
const exhausted = attempt >= maximumAttempts;
|
|
281
|
+
const nextDelayMs = exhausted
|
|
282
|
+
? 0
|
|
283
|
+
: (0, sink_retry_policy_js_1.sinkRetryDelayMs)(this.retryBackoffMs, attempt);
|
|
284
|
+
(0, iteration_observation_diagnostics_js_1.logIterationObservationFailure)(this.logger, exhausted ? "error" : "warn", {
|
|
285
|
+
...this.diagnosticContext(item, attempt, maximumAttempts),
|
|
286
|
+
nextDelayMs
|
|
287
|
+
}, error);
|
|
288
|
+
if (exhausted) {
|
|
289
|
+
return "exhausted";
|
|
290
|
+
}
|
|
291
|
+
if (!await this.waitForRetry(attempt)) {
|
|
292
|
+
this.timeoutOutstanding();
|
|
293
|
+
return "deadline";
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return "exhausted";
|
|
298
|
+
}
|
|
299
|
+
diagnosticContext(item, attempt, maximumAttempts) {
|
|
300
|
+
return {
|
|
301
|
+
sinkName: this.target.name,
|
|
302
|
+
operation: "observation-batch",
|
|
303
|
+
runId: this.runId,
|
|
304
|
+
resultOwnerId: this.resultOwnerId,
|
|
305
|
+
batchId: item.batch.batchId,
|
|
306
|
+
batchSequence64: item.batch.batchSequence64,
|
|
307
|
+
observationCount: Number(item.count),
|
|
308
|
+
attempt,
|
|
309
|
+
maximumAttempts
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
async waitForRetry(retryNumber) {
|
|
313
|
+
const delayMs = (0, sink_retry_policy_js_1.sinkRetryDelayMs)(this.retryBackoffMs, retryNumber);
|
|
314
|
+
const deadline = this.drainDeadlineMs;
|
|
315
|
+
if (deadline !== null && !delayCanFit(delayMs, deadline)) {
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
if (delayMs === 0) {
|
|
319
|
+
await (0, sink_retry_policy_js_1.cooperativeSinkRetryYield)();
|
|
320
|
+
return !this.timedOut && !this.deadlineExpired();
|
|
321
|
+
}
|
|
322
|
+
return await new Promise((resolve) => {
|
|
323
|
+
let settled = false;
|
|
324
|
+
let pending;
|
|
325
|
+
const finish = (completed) => {
|
|
326
|
+
if (settled)
|
|
327
|
+
return;
|
|
328
|
+
settled = true;
|
|
329
|
+
clearTimeout(timer);
|
|
330
|
+
this.pendingRetryDelays.delete(pending);
|
|
331
|
+
resolve(completed && !this.timedOut && !this.deadlineExpired());
|
|
332
|
+
};
|
|
333
|
+
const timer = setTimeout(() => finish(true), delayMs);
|
|
334
|
+
const cancel = () => finish(false);
|
|
335
|
+
pending = {
|
|
336
|
+
dueAtMs: node_perf_hooks_1.performance.now() + delayMs,
|
|
337
|
+
cancel
|
|
338
|
+
};
|
|
339
|
+
this.pendingRetryDelays.add(pending);
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
deadlineExpired() {
|
|
343
|
+
return this.drainDeadlineMs !== null && node_perf_hooks_1.performance.now() >= this.drainDeadlineMs;
|
|
344
|
+
}
|
|
345
|
+
timeoutOutstanding() {
|
|
346
|
+
if (this.timedOut || this.isIdle())
|
|
347
|
+
return;
|
|
348
|
+
this.timedOut = true;
|
|
349
|
+
for (const pending of Array.from(this.pendingRetryDelays)) {
|
|
350
|
+
pending.cancel();
|
|
351
|
+
}
|
|
352
|
+
const queued = [...this.queue];
|
|
353
|
+
const active = [...this.active];
|
|
354
|
+
this.queue.length = 0;
|
|
355
|
+
for (const item of queued) {
|
|
356
|
+
this.drop("observation_sink_drain_timeout", item.batch.observations);
|
|
357
|
+
}
|
|
358
|
+
for (const item of active) {
|
|
359
|
+
// Active sink I/O cannot be cancelled safely. Warn, but settle its actual
|
|
360
|
+
// outcome before the completion marker and do not claim it was dropped.
|
|
361
|
+
this.onDropped("observation_sink_drain_timeout", this.target.name, item.batch.observations);
|
|
362
|
+
}
|
|
363
|
+
this.notifyIdle();
|
|
364
|
+
}
|
|
365
|
+
drop(code, observations) {
|
|
366
|
+
this.dropped += BigInt(observations.length);
|
|
367
|
+
this.onDropped(code, this.target.name, observations);
|
|
368
|
+
}
|
|
369
|
+
isIdle() {
|
|
370
|
+
return this.queue.length === 0 && this.active.size === 0;
|
|
371
|
+
}
|
|
372
|
+
notifyIdle(force = false) {
|
|
373
|
+
if (!force && !this.isIdle())
|
|
374
|
+
return;
|
|
375
|
+
for (const waiter of this.idleWaiters) {
|
|
376
|
+
waiter();
|
|
377
|
+
}
|
|
378
|
+
this.idleWaiters.clear();
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
class IterationObservationReporter {
|
|
382
|
+
constructor(options) {
|
|
383
|
+
this.options = options;
|
|
384
|
+
this.streamKey = Symbol("loadstrike-iteration-observations");
|
|
385
|
+
this.warnings = new Map();
|
|
386
|
+
this.sequence = 0n;
|
|
387
|
+
this.captured = 0n;
|
|
388
|
+
this.droppedBuffer = 0n;
|
|
389
|
+
this.unsupportedSinkCount = 0;
|
|
390
|
+
this.sealed = false;
|
|
391
|
+
this.finalResult = null;
|
|
392
|
+
this.expectedResultOwnerCount64 = positiveCanonicalDecimalString(options.expectedResultOwnerCount64 ?? "1", "Expected result owner count");
|
|
393
|
+
const requestedSettings = {
|
|
394
|
+
...exports.DEFAULT_ITERATION_OBSERVATION_SETTINGS,
|
|
395
|
+
...(options.settings ?? {})
|
|
396
|
+
};
|
|
397
|
+
const hasPortalSink = options.sinks.some((sink) => sink.iterationObservationPortalSink === true);
|
|
398
|
+
this.settings = hasPortalSink
|
|
399
|
+
? {
|
|
400
|
+
...requestedSettings,
|
|
401
|
+
maxObservationsPerBatch: Math.min(requestedSettings.maxObservationsPerBatch, 50000),
|
|
402
|
+
maxBatchBytes: Math.min(requestedSettings.maxBatchBytes, 8 * 1024 * 1024)
|
|
403
|
+
}
|
|
404
|
+
: requestedSettings;
|
|
405
|
+
if (!options.skipValidation) {
|
|
406
|
+
validateIterationObservationSettings(this.settings);
|
|
407
|
+
}
|
|
408
|
+
this.buffer = options.buffer ?? processIterationObservationBuffer;
|
|
409
|
+
this.clock = options.clock ?? defaultClock;
|
|
410
|
+
this.batchEnvelopeBytes = maximumIterationObservationBatchEnvelopeBytes(options);
|
|
411
|
+
this.dispatchers = options.sinks
|
|
412
|
+
.filter((sink) => {
|
|
413
|
+
if (typeof sink.saveIterationBatch === "function") {
|
|
414
|
+
if (sink.iterationObservationShapeLimited === true) {
|
|
415
|
+
this.recordWarning("sink_iteration_shape_limited", sink.name, "", -1, 1n, "The reporting sink stores a metric projection rather than the complete raw iteration shape.");
|
|
416
|
+
}
|
|
417
|
+
return true;
|
|
418
|
+
}
|
|
419
|
+
this.unsupportedSinkCount += 1;
|
|
420
|
+
this.recordWarning("sink_iteration_batches_unsupported", sink.name, "", -1, 1n, "The reporting sink does not support raw iteration batches.");
|
|
421
|
+
return false;
|
|
422
|
+
})
|
|
423
|
+
.map((target) => new SinkDispatcher(target, this.settings.sinkQueueDepth, this.settings.sinkParallelism, (0, sink_retry_policy_js_1.normalizeSinkRetryCount)(options.sinkRetryCount), (0, sink_retry_policy_js_1.normalizeSinkRetryBackoffMs)(options.sinkRetryBackoffMs), options.logger, options.runId, options.resultOwnerId, (code, sinkName, observations) => this.recordSinkDrop(code, sinkName, observations)));
|
|
424
|
+
if (this.dispatchers.length > 0) {
|
|
425
|
+
this.buffer.register(this.streamKey, this.settings.maxBufferBytes);
|
|
426
|
+
}
|
|
427
|
+
this.timer = this.clock.setInterval(() => this.flushNow(), this.settings.flushIntervalMs);
|
|
428
|
+
}
|
|
429
|
+
get enabled() {
|
|
430
|
+
return this.dispatchers.length > 0 && !this.sealed;
|
|
431
|
+
}
|
|
432
|
+
capture(observation) {
|
|
433
|
+
if (!this.enabled) {
|
|
434
|
+
return false;
|
|
435
|
+
}
|
|
436
|
+
const singleObservationBudget = Math.max(this.settings.maxBatchBytes - this.batchEnvelopeBytes, 0);
|
|
437
|
+
const upperBound = jsonUtf8SizeUpperBound(observation);
|
|
438
|
+
const encodedBytes = upperBound >= Math.floor(singleObservationBudget * 0.9)
|
|
439
|
+
? Buffer.byteLength(JSON.stringify(observation), "utf8")
|
|
440
|
+
: upperBound;
|
|
441
|
+
if (encodedBytes > singleObservationBudget) {
|
|
442
|
+
this.droppedBuffer += 1n;
|
|
443
|
+
this.recordWarning("observation_batch_oversize", undefined, observation.scenarioName, observation.simulationIndex, 1n, "The canonical raw observation cannot fit within one configured batch.", observation.scenarioIndex, observation.simulationKind);
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
const immutable = deepFreezeObservation(observation);
|
|
447
|
+
if (this.buffer.offer(this.streamKey, immutable, this.settings.maxBufferBytes, encodedBytes)) {
|
|
448
|
+
this.captured += 1n;
|
|
449
|
+
return true;
|
|
450
|
+
}
|
|
451
|
+
this.droppedBuffer += 1n;
|
|
452
|
+
this.recordWarning("observation_buffer_overflow", undefined, immutable.scenarioName, immutable.simulationIndex, 1n, "The process-wide raw observation buffer was full.", immutable.scenarioIndex, immutable.simulationKind);
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
flushNow() {
|
|
456
|
+
if (this.dispatchers.length === 0) {
|
|
457
|
+
this.buffer.release(this.streamKey);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
const observations = this.buffer.drain(this.streamKey);
|
|
461
|
+
if (!observations.length) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
const chunks = splitBufferedObservations(observations, this.settings.maxObservationsPerBatch, this.settings.maxBatchBytes, Math.max(BATCH_ENVELOPE_RESERVED_BYTES, this.batchEnvelopeBytes));
|
|
465
|
+
for (const chunk of chunks) {
|
|
466
|
+
const batch = this.createBatch(chunk.map((item) => item.observation), this.droppedBuffer);
|
|
467
|
+
for (const dispatcher of this.dispatchers) {
|
|
468
|
+
dispatcher.enqueue(batch);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
async sealAndDrain() {
|
|
473
|
+
if (this.finalResult) {
|
|
474
|
+
return { ...this.finalResult };
|
|
475
|
+
}
|
|
476
|
+
this.sealed = true;
|
|
477
|
+
this.clock.clearInterval(this.timer);
|
|
478
|
+
this.flushNow();
|
|
479
|
+
const started = node_perf_hooks_1.performance.now();
|
|
480
|
+
const deadline = started + this.settings.drainTimeoutMs;
|
|
481
|
+
for (const dispatcher of this.dispatchers) {
|
|
482
|
+
dispatcher.beginDrain(deadline);
|
|
483
|
+
}
|
|
484
|
+
const drainResults = new Array(this.dispatchers.length).fill(false);
|
|
485
|
+
const completionResults = await Promise.all(this.dispatchers.map(async (dispatcher, index) => {
|
|
486
|
+
const drained = await dispatcher.waitForIdle(Math.max(deadline - node_perf_hooks_1.performance.now(), 1));
|
|
487
|
+
drainResults[index] = drained;
|
|
488
|
+
if (!dispatcher.canPublishCompletion) {
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
491
|
+
const complete = dispatcher.target.completeIterationObservationStream;
|
|
492
|
+
if (!complete) {
|
|
493
|
+
this.recordWarning("sink_iteration_completion_unsupported", dispatcher.target.name, "", -1, 1n, "The reporting sink does not support raw iteration stream completion markers.");
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
const sinkReportingComplete = drained
|
|
497
|
+
&& this.droppedBuffer === 0n
|
|
498
|
+
&& dispatcher.droppedCount === 0n
|
|
499
|
+
&& dispatcher.deliveredCount === this.captured;
|
|
500
|
+
const completion = this.createCompletion(dispatcher, sinkReportingComplete);
|
|
501
|
+
const outcome = await this.completeWithRetries(complete, completion, deadline, dispatcher.target.name);
|
|
502
|
+
if (outcome === "success") {
|
|
503
|
+
return true;
|
|
504
|
+
}
|
|
505
|
+
this.recordWarning("observation_stream_completion_failed", dispatcher.target.name, "", -1, 1n, outcome === "timeout"
|
|
506
|
+
? "The reporting sink did not complete its observation stream marker before shutdown."
|
|
507
|
+
: "The reporting sink did not accept the observation stream completion marker.");
|
|
508
|
+
return false;
|
|
509
|
+
}));
|
|
510
|
+
const reportingComplete = this.dispatchers.length === 0
|
|
511
|
+
? this.unsupportedSinkCount === 0
|
|
512
|
+
: this.unsupportedSinkCount === 0
|
|
513
|
+
&& drainResults.every(Boolean)
|
|
514
|
+
&& completionResults.every(Boolean)
|
|
515
|
+
&& this.droppedBuffer === 0n
|
|
516
|
+
&& this.dispatchers.every((dispatcher) => dispatcher.droppedCount === 0n)
|
|
517
|
+
&& this.dispatchers.every((dispatcher) => dispatcher.deliveredCount === this.captured);
|
|
518
|
+
const delivered = this.dispatchers.length
|
|
519
|
+
? this.dispatchers.reduce((minimum, dispatcher) => dispatcher.deliveredCount < minimum
|
|
520
|
+
? dispatcher.deliveredCount
|
|
521
|
+
: minimum, this.dispatchers[0].deliveredCount)
|
|
522
|
+
: 0n;
|
|
523
|
+
const droppedSink = this.dispatchers.reduce((maximum, dispatcher) => dispatcher.droppedCount > maximum
|
|
524
|
+
? dispatcher.droppedCount
|
|
525
|
+
: maximum, 0n);
|
|
526
|
+
this.buffer.unregister(this.streamKey);
|
|
527
|
+
this.finalResult = {
|
|
528
|
+
lastBatchSequence64: this.sequence === 0n ? "-1" : (this.sequence - 1n).toString(),
|
|
529
|
+
capturedCount64: this.captured.toString(),
|
|
530
|
+
deliveredCount64: delivered.toString(),
|
|
531
|
+
droppedBufferCount64: this.droppedBuffer.toString(),
|
|
532
|
+
droppedSinkCount64: droppedSink.toString(),
|
|
533
|
+
reportingComplete
|
|
534
|
+
};
|
|
535
|
+
return { ...this.finalResult };
|
|
536
|
+
}
|
|
537
|
+
async completeWithRetries(complete, completion, deadline, sinkName) {
|
|
538
|
+
const retryCount = (0, sink_retry_policy_js_1.normalizeSinkRetryCount)(this.options.sinkRetryCount);
|
|
539
|
+
const backoffMs = (0, sink_retry_policy_js_1.normalizeSinkRetryBackoffMs)(this.options.sinkRetryBackoffMs);
|
|
540
|
+
const maximumAttempts = retryCount + 1;
|
|
541
|
+
for (let attempt = 1; attempt <= maximumAttempts; attempt += 1) {
|
|
542
|
+
const remainingMs = deadline - node_perf_hooks_1.performance.now();
|
|
543
|
+
if (remainingMs <= 0) {
|
|
544
|
+
return "timeout";
|
|
545
|
+
}
|
|
546
|
+
let timer;
|
|
547
|
+
const outcome = await Promise.race([
|
|
548
|
+
Promise.resolve()
|
|
549
|
+
.then(() => complete(completion))
|
|
550
|
+
.then(() => ({ kind: "success" }), (error) => ({ kind: "failed", error })),
|
|
551
|
+
new Promise((resolve) => {
|
|
552
|
+
timer = setTimeout(() => resolve({ kind: "timeout" }), Math.max(remainingMs, 1));
|
|
553
|
+
})
|
|
554
|
+
]);
|
|
555
|
+
if (timer) {
|
|
556
|
+
clearTimeout(timer);
|
|
557
|
+
}
|
|
558
|
+
if (outcome.kind === "success") {
|
|
559
|
+
if (attempt > 1) {
|
|
560
|
+
(0, iteration_observation_diagnostics_js_1.logIterationObservationRecovery)(this.options.logger, {
|
|
561
|
+
sinkName,
|
|
562
|
+
operation: "observation-stream-completion",
|
|
563
|
+
runId: this.options.runId,
|
|
564
|
+
resultOwnerId: this.options.resultOwnerId,
|
|
565
|
+
observationCount: 0,
|
|
566
|
+
attempt,
|
|
567
|
+
maximumAttempts,
|
|
568
|
+
nextDelayMs: 0
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
return "success";
|
|
572
|
+
}
|
|
573
|
+
if (outcome.kind === "timeout") {
|
|
574
|
+
return "timeout";
|
|
575
|
+
}
|
|
576
|
+
const exhausted = attempt >= maximumAttempts;
|
|
577
|
+
const nextDelayMs = exhausted ? 0 : (0, sink_retry_policy_js_1.sinkRetryDelayMs)(backoffMs, attempt);
|
|
578
|
+
(0, iteration_observation_diagnostics_js_1.logIterationObservationFailure)(this.options.logger, exhausted ? "error" : "warn", {
|
|
579
|
+
sinkName,
|
|
580
|
+
operation: "observation-stream-completion",
|
|
581
|
+
runId: this.options.runId,
|
|
582
|
+
resultOwnerId: this.options.resultOwnerId,
|
|
583
|
+
observationCount: 0,
|
|
584
|
+
attempt,
|
|
585
|
+
maximumAttempts,
|
|
586
|
+
nextDelayMs
|
|
587
|
+
}, outcome.error);
|
|
588
|
+
if (exhausted) {
|
|
589
|
+
return "failed";
|
|
590
|
+
}
|
|
591
|
+
const delayMs = nextDelayMs;
|
|
592
|
+
if (!delayCanFit(delayMs, deadline)) {
|
|
593
|
+
return "timeout";
|
|
594
|
+
}
|
|
595
|
+
if (delayMs === 0) {
|
|
596
|
+
await (0, sink_retry_policy_js_1.cooperativeSinkRetryYield)();
|
|
597
|
+
}
|
|
598
|
+
else {
|
|
599
|
+
await (0, sink_retry_policy_js_1.waitForSinkRetryDelay)(delayMs);
|
|
600
|
+
}
|
|
601
|
+
if (node_perf_hooks_1.performance.now() >= deadline) {
|
|
602
|
+
return "timeout";
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
return "failed";
|
|
606
|
+
}
|
|
607
|
+
buildWarnings() {
|
|
608
|
+
return Array.from(this.warnings.values())
|
|
609
|
+
.sort((left, right) => left.code.localeCompare(right.code)
|
|
610
|
+
|| String(left.sinkName ?? "").localeCompare(String(right.sinkName ?? ""))
|
|
611
|
+
|| left.scenarioName.localeCompare(right.scenarioName)
|
|
612
|
+
|| left.simulationIndex - right.simulationIndex)
|
|
613
|
+
.map((warning) => ({
|
|
614
|
+
code: warning.code,
|
|
615
|
+
...(warning.sinkName ? { sinkName: warning.sinkName } : {}),
|
|
616
|
+
scenarioName: warning.scenarioName,
|
|
617
|
+
scenarioIndex: warning.scenarioIndex,
|
|
618
|
+
simulationIndex: warning.simulationIndex,
|
|
619
|
+
simulationKind: warning.simulationKind,
|
|
620
|
+
count64: warning.count.toString(),
|
|
621
|
+
message: warning.message,
|
|
622
|
+
firstObservedUtcNs: warning.firstObservedUtcNs.toString(),
|
|
623
|
+
lastObservedUtcNs: warning.lastObservedUtcNs.toString()
|
|
624
|
+
}));
|
|
625
|
+
}
|
|
626
|
+
createBatch(observations, droppedBeforeBatch) {
|
|
627
|
+
const sequence = this.sequence;
|
|
628
|
+
this.sequence += 1n;
|
|
629
|
+
const firstObservationUtcNs = observations.reduce((minimum, observation) => {
|
|
630
|
+
const value = BigInt(observation.startedUtcNs);
|
|
631
|
+
return value < minimum ? value : minimum;
|
|
632
|
+
}, BigInt(observations[0].startedUtcNs));
|
|
633
|
+
const lastObservationUtcNs = observations.reduce((maximum, observation) => {
|
|
634
|
+
const value = BigInt(observation.completedUtcNs);
|
|
635
|
+
return value > maximum ? value : maximum;
|
|
636
|
+
}, BigInt(observations[0].completedUtcNs));
|
|
637
|
+
return deepFreezeBatch({
|
|
638
|
+
schemaVersion: exports.ITERATION_OBSERVATION_BATCH_SCHEMA_VERSION,
|
|
639
|
+
batchId: deriveIterationBatchId(this.options.runId, this.options.resultOwnerId, sequence),
|
|
640
|
+
runId: this.options.runId,
|
|
641
|
+
sessionId: this.options.sessionId,
|
|
642
|
+
resultOwnerId: this.options.resultOwnerId,
|
|
643
|
+
processGroup: Math.max(Math.trunc(this.options.processGroup), 0),
|
|
644
|
+
batchSequence64: sequence.toString(),
|
|
645
|
+
createdUtcNs: this.clock.nowUtcNs().toString(),
|
|
646
|
+
firstObservationUtcNs: firstObservationUtcNs.toString(),
|
|
647
|
+
lastObservationUtcNs: lastObservationUtcNs.toString(),
|
|
648
|
+
observations,
|
|
649
|
+
capturedCount64: this.captured.toString(),
|
|
650
|
+
droppedBeforeBatch64: droppedBeforeBatch.toString(),
|
|
651
|
+
compression: "gzip-json"
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
createCompletion(dispatcher, reportingComplete) {
|
|
655
|
+
return Object.freeze({
|
|
656
|
+
schemaVersion: exports.ITERATION_OBSERVATION_STREAM_COMPLETION_SCHEMA_VERSION,
|
|
657
|
+
runId: this.options.runId,
|
|
658
|
+
sessionId: this.options.sessionId,
|
|
659
|
+
resultOwnerId: this.options.resultOwnerId,
|
|
660
|
+
expectedResultOwnerCount64: this.expectedResultOwnerCount64,
|
|
661
|
+
processGroup: Math.max(Math.trunc(this.options.processGroup), 0),
|
|
662
|
+
lastBatchSequence64: this.sequence === 0n ? "-1" : (this.sequence - 1n).toString(),
|
|
663
|
+
capturedCount64: this.captured.toString(),
|
|
664
|
+
deliveredCount64: dispatcher.deliveredCount.toString(),
|
|
665
|
+
droppedBufferCount64: this.droppedBuffer.toString(),
|
|
666
|
+
droppedSinkCount64: dispatcher.droppedCount.toString(),
|
|
667
|
+
reportingComplete,
|
|
668
|
+
completedUtcNs: this.clock.nowUtcNs().toString()
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
recordSinkDrop(code, sinkName, observations) {
|
|
672
|
+
const grouped = new Map();
|
|
673
|
+
for (const observation of observations) {
|
|
674
|
+
const key = `${observation.scenarioName}\n${observation.simulationIndex}`;
|
|
675
|
+
const group = grouped.get(key) ?? {
|
|
676
|
+
scenarioName: observation.scenarioName,
|
|
677
|
+
scenarioIndex: observation.scenarioIndex,
|
|
678
|
+
simulationIndex: observation.simulationIndex,
|
|
679
|
+
simulationKind: observation.simulationKind,
|
|
680
|
+
count: 0n
|
|
681
|
+
};
|
|
682
|
+
group.count += 1n;
|
|
683
|
+
grouped.set(key, group);
|
|
684
|
+
}
|
|
685
|
+
for (const group of grouped.values()) {
|
|
686
|
+
this.recordWarning(code, sinkName, group.scenarioName, group.simulationIndex, group.count, code === "observation_sink_overflow"
|
|
687
|
+
? "The reporting sink raw observation queue was full."
|
|
688
|
+
: code === "observation_sink_drain_timeout"
|
|
689
|
+
? "The reporting sink raw observation queue did not drain before shutdown."
|
|
690
|
+
: "The reporting sink failed to accept a raw observation batch.", group.scenarioIndex, group.simulationKind);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
recordWarning(code, sinkName, scenarioName, simulationIndex, count, message, scenarioIndex = -1, simulationKind = "") {
|
|
694
|
+
if (count <= 0n)
|
|
695
|
+
return;
|
|
696
|
+
const key = `${code}\n${sinkName ?? ""}\n${scenarioIndex}\n${simulationIndex}\n${simulationKind}`;
|
|
697
|
+
const now = this.clock?.nowUtcNs?.() ?? utcNowNs();
|
|
698
|
+
const existing = this.warnings.get(key);
|
|
699
|
+
if (existing) {
|
|
700
|
+
existing.count += count;
|
|
701
|
+
existing.lastObservedUtcNs = now;
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
this.warnings.set(key, {
|
|
705
|
+
code,
|
|
706
|
+
sinkName,
|
|
707
|
+
scenarioName,
|
|
708
|
+
scenarioIndex,
|
|
709
|
+
simulationIndex,
|
|
710
|
+
simulationKind,
|
|
711
|
+
count,
|
|
712
|
+
message,
|
|
713
|
+
firstObservedUtcNs: now,
|
|
714
|
+
lastObservedUtcNs: now
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
exports.IterationObservationReporter = IterationObservationReporter;
|
|
719
|
+
function validateIterationObservationSettings(settings) {
|
|
720
|
+
validateIntegerRange(settings.flushIntervalMs, 1000, 60000, "Iteration observation flush interval");
|
|
721
|
+
validateIntegerRange(settings.maxBufferBytes, 16 * 1024 * 1024, 4 * 1024 * 1024 * 1024, "Iteration observation buffer bytes");
|
|
722
|
+
validateIntegerRange(settings.maxObservationsPerBatch, 100, 1000000, "Iteration observations per batch");
|
|
723
|
+
validateIntegerRange(settings.maxBatchBytes, 1024 * 1024, 64 * 1024 * 1024, "Iteration observation batch bytes");
|
|
724
|
+
validateIntegerRange(settings.sinkQueueDepth, 1, 64, "Iteration observation sink queue depth");
|
|
725
|
+
validateIntegerRange(settings.sinkParallelism, 1, 16, "Iteration observation sink parallelism");
|
|
726
|
+
validateIntegerRange(settings.drainTimeoutMs, 1000, 300000, "Iteration observation drain timeout");
|
|
727
|
+
}
|
|
728
|
+
function validateIntegerRange(value, minimum, maximum, name) {
|
|
729
|
+
if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
|
|
730
|
+
throw new RangeError(`${name} must be an integer from ${minimum} through ${maximum}.`);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
function splitBufferedObservations(observations, maxCount, maxBytes, envelopeBytes) {
|
|
734
|
+
const chunks = [];
|
|
735
|
+
let current = [];
|
|
736
|
+
let currentBytes = envelopeBytes;
|
|
737
|
+
for (const observation of observations) {
|
|
738
|
+
const projectedBytes = currentBytes + observation.jsonBytes + (current.length ? 1 : 0);
|
|
739
|
+
if (current.length > 0 && (current.length >= maxCount || projectedBytes > maxBytes)) {
|
|
740
|
+
chunks.push(current);
|
|
741
|
+
current = [];
|
|
742
|
+
currentBytes = envelopeBytes;
|
|
743
|
+
}
|
|
744
|
+
current.push(observation);
|
|
745
|
+
currentBytes += observation.jsonBytes + (current.length > 1 ? 1 : 0);
|
|
746
|
+
}
|
|
747
|
+
if (current.length) {
|
|
748
|
+
chunks.push(current);
|
|
749
|
+
}
|
|
750
|
+
return chunks;
|
|
751
|
+
}
|
|
752
|
+
function maximumIterationObservationBatchEnvelopeBytes(options) {
|
|
753
|
+
const maximumDecimal = "9223372036854775807";
|
|
754
|
+
const value = {
|
|
755
|
+
schemaVersion: exports.ITERATION_OBSERVATION_BATCH_SCHEMA_VERSION,
|
|
756
|
+
batchId: deriveIterationBatchId(options.runId, options.resultOwnerId, maximumDecimal),
|
|
757
|
+
runId: options.runId,
|
|
758
|
+
sessionId: options.sessionId,
|
|
759
|
+
resultOwnerId: options.resultOwnerId,
|
|
760
|
+
processGroup: Math.max(Math.trunc(options.processGroup), 0),
|
|
761
|
+
batchSequence64: maximumDecimal,
|
|
762
|
+
createdUtcNs: maximumDecimal,
|
|
763
|
+
firstObservationUtcNs: maximumDecimal,
|
|
764
|
+
lastObservationUtcNs: maximumDecimal,
|
|
765
|
+
observations: [],
|
|
766
|
+
capturedCount64: maximumDecimal,
|
|
767
|
+
droppedBeforeBatch64: maximumDecimal,
|
|
768
|
+
compression: "gzip-json"
|
|
769
|
+
};
|
|
770
|
+
return Buffer.byteLength(JSON.stringify(value), "utf8");
|
|
771
|
+
}
|
|
772
|
+
function jsonUtf8SizeUpperBound(value) {
|
|
773
|
+
if (value === null || value === undefined)
|
|
774
|
+
return 4;
|
|
775
|
+
if (typeof value === "string")
|
|
776
|
+
return 2 + escapedJsonStringUtf8Bytes(value);
|
|
777
|
+
if (typeof value === "boolean")
|
|
778
|
+
return value ? 4 : 5;
|
|
779
|
+
if (typeof value === "number")
|
|
780
|
+
return Number.isFinite(value) ? Buffer.byteLength(String(value), "utf8") : 4;
|
|
781
|
+
if (typeof value === "bigint")
|
|
782
|
+
return Buffer.byteLength(value.toString(), "utf8");
|
|
783
|
+
if (Array.isArray(value)) {
|
|
784
|
+
return 2 + value.reduce((total, item, index) => total + (index ? 1 : 0) + jsonUtf8SizeUpperBound(item), 0);
|
|
785
|
+
}
|
|
786
|
+
if (typeof value === "object") {
|
|
787
|
+
let total = 2;
|
|
788
|
+
let count = 0;
|
|
789
|
+
for (const [key, item] of Object.entries(value)) {
|
|
790
|
+
if (item === undefined || typeof item === "function" || typeof item === "symbol")
|
|
791
|
+
continue;
|
|
792
|
+
total += (count ? 1 : 0) + 2 + escapedJsonStringUtf8Bytes(key) + 1 + jsonUtf8SizeUpperBound(item);
|
|
793
|
+
count += 1;
|
|
794
|
+
}
|
|
795
|
+
return total;
|
|
796
|
+
}
|
|
797
|
+
return 4;
|
|
798
|
+
}
|
|
799
|
+
function escapedJsonStringUtf8Bytes(value) {
|
|
800
|
+
let bytes = 0;
|
|
801
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
802
|
+
const code = value.charCodeAt(index);
|
|
803
|
+
if (code <= 0x1f)
|
|
804
|
+
bytes += 6;
|
|
805
|
+
else if (code === 0x22 || code === 0x5c)
|
|
806
|
+
bytes += 2;
|
|
807
|
+
else if (code <= 0x7f)
|
|
808
|
+
bytes += 1;
|
|
809
|
+
else if (code <= 0x7ff)
|
|
810
|
+
bytes += 2;
|
|
811
|
+
else if (code >= 0xd800 && code <= 0xdbff) {
|
|
812
|
+
const next = value.charCodeAt(index + 1);
|
|
813
|
+
if (next >= 0xdc00 && next <= 0xdfff) {
|
|
814
|
+
bytes += 4;
|
|
815
|
+
index += 1;
|
|
816
|
+
}
|
|
817
|
+
else
|
|
818
|
+
bytes += 6;
|
|
819
|
+
}
|
|
820
|
+
else if (code >= 0xdc00 && code <= 0xdfff)
|
|
821
|
+
bytes += 6;
|
|
822
|
+
else
|
|
823
|
+
bytes += 3;
|
|
824
|
+
}
|
|
825
|
+
return bytes;
|
|
826
|
+
}
|
|
827
|
+
function derivePrefixedSha256(prefix, material) {
|
|
828
|
+
return `${prefix}${(0, node_crypto_1.createHash)("sha256").update(material, "utf8").digest("hex").slice(0, 40)}`;
|
|
829
|
+
}
|
|
830
|
+
function decimalString(value) {
|
|
831
|
+
try {
|
|
832
|
+
return BigInt(String(value)).toString();
|
|
833
|
+
}
|
|
834
|
+
catch {
|
|
835
|
+
return "0";
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
function nonNegativeDecimalString(value) {
|
|
839
|
+
try {
|
|
840
|
+
const parsed = BigInt(String(value));
|
|
841
|
+
return (parsed < 0n ? 0n : parsed).toString();
|
|
842
|
+
}
|
|
843
|
+
catch {
|
|
844
|
+
return "0";
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
function positiveCanonicalDecimalString(value, label) {
|
|
848
|
+
if (typeof value === "number" && (!Number.isSafeInteger(value) || value < 1)) {
|
|
849
|
+
throw new RangeError(`${label} must be a canonical unsigned decimal greater than zero.`);
|
|
850
|
+
}
|
|
851
|
+
const text = String(value);
|
|
852
|
+
if (!/^[1-9][0-9]*$/.test(text)) {
|
|
853
|
+
throw new RangeError(`${label} must be a canonical unsigned decimal greater than zero.`);
|
|
854
|
+
}
|
|
855
|
+
return text;
|
|
856
|
+
}
|
|
857
|
+
function truncateUtf8(value, maxBytes) {
|
|
858
|
+
if (Buffer.byteLength(value, "utf8") <= maxBytes) {
|
|
859
|
+
return value;
|
|
860
|
+
}
|
|
861
|
+
let output = "";
|
|
862
|
+
for (const token of value) {
|
|
863
|
+
if (Buffer.byteLength(output + token, "utf8") > maxBytes) {
|
|
864
|
+
break;
|
|
865
|
+
}
|
|
866
|
+
output += token;
|
|
867
|
+
}
|
|
868
|
+
return output;
|
|
869
|
+
}
|
|
870
|
+
function delayCanFit(delayMs, deadlineMs) {
|
|
871
|
+
const remainingMs = deadlineMs - node_perf_hooks_1.performance.now();
|
|
872
|
+
return remainingMs > 0 && delayMs < remainingMs;
|
|
873
|
+
}
|
|
874
|
+
function deepFreezeObservation(observation) {
|
|
875
|
+
for (const step of observation.steps) {
|
|
876
|
+
Object.freeze(step);
|
|
877
|
+
}
|
|
878
|
+
Object.freeze(observation.steps);
|
|
879
|
+
return Object.freeze(observation);
|
|
880
|
+
}
|
|
881
|
+
function deepFreezeBatch(batch) {
|
|
882
|
+
Object.freeze(batch.observations);
|
|
883
|
+
return Object.freeze(batch);
|
|
884
|
+
}
|