@loadstrike/loadstrike-sdk 1.0.30401 → 1.0.31601

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.
@@ -10,7 +10,10 @@ exports.createIterationObservation = createIterationObservation;
10
10
  exports.serializeIterationObservationBatchGzipJson = serializeIterationObservationBatchGzipJson;
11
11
  exports.validateIterationObservationSettings = validateIterationObservationSettings;
12
12
  const node_crypto_1 = require("node:crypto");
13
+ const node_perf_hooks_1 = require("node:perf_hooks");
13
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");
14
17
  exports.ITERATION_OBSERVATION_SCHEMA_VERSION = "loadstrike.iteration-observation/1";
15
18
  exports.ITERATION_OBSERVATION_BATCH_SCHEMA_VERSION = "loadstrike.iteration-batch/1";
16
19
  exports.ITERATION_OBSERVATION_STREAM_COMPLETION_SCHEMA_VERSION = "loadstrike.iteration-stream-completion/1";
@@ -153,10 +156,15 @@ class ProcessIterationObservationBuffer {
153
156
  exports.ProcessIterationObservationBuffer = ProcessIterationObservationBuffer;
154
157
  const processIterationObservationBuffer = new ProcessIterationObservationBuffer();
155
158
  class SinkDispatcher {
156
- constructor(target, depth, parallelism, onDropped) {
159
+ constructor(target, depth, parallelism, retryCount, retryBackoffMs, logger, runId, resultOwnerId, onDropped) {
157
160
  this.target = target;
158
161
  this.depth = depth;
159
162
  this.parallelism = parallelism;
163
+ this.retryCount = retryCount;
164
+ this.retryBackoffMs = retryBackoffMs;
165
+ this.logger = logger;
166
+ this.runId = runId;
167
+ this.resultOwnerId = resultOwnerId;
160
168
  this.onDropped = onDropped;
161
169
  this.queue = [];
162
170
  this.active = new Set();
@@ -164,6 +172,8 @@ class SinkDispatcher {
164
172
  this.timedOut = false;
165
173
  this.delivered = 0n;
166
174
  this.dropped = 0n;
175
+ this.drainDeadlineMs = null;
176
+ this.pendingRetryDelays = new Set();
167
177
  }
168
178
  get deliveredCount() {
169
179
  return this.delivered;
@@ -172,7 +182,7 @@ class SinkDispatcher {
172
182
  return this.dropped;
173
183
  }
174
184
  get canPublishCompletion() {
175
- return this.queue.length === 0 && this.active.size === 0;
185
+ return !this.timedOut && this.queue.length === 0 && this.active.size === 0;
176
186
  }
177
187
  enqueue(batch) {
178
188
  if (this.timedOut || this.queue.length >= this.depth) {
@@ -200,7 +210,6 @@ class SinkDispatcher {
200
210
  });
201
211
  const timeout = new Promise((resolve) => {
202
212
  timer = setTimeout(() => resolve(false), Math.max(timeoutMs, 1));
203
- timer.unref?.();
204
213
  });
205
214
  const completed = await Promise.race([idle, timeout]);
206
215
  if (timer) {
@@ -213,16 +222,30 @@ class SinkDispatcher {
213
222
  }
214
223
  return completed;
215
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
+ }
216
236
  pump() {
217
237
  while (!this.timedOut && this.active.size < this.parallelism && this.queue.length > 0) {
218
238
  const item = this.queue.shift();
219
239
  this.active.add(item);
220
240
  Promise.resolve()
221
- .then(() => this.target.saveIterationBatch(item.batch))
222
- .then(() => {
223
- if (!this.timedOut) {
241
+ .then(() => this.deliver(item))
242
+ .then((outcome) => {
243
+ if (outcome === "success" && !this.timedOut) {
224
244
  this.delivered += item.count;
225
245
  }
246
+ else if (outcome === "exhausted" && !this.timedOut) {
247
+ this.drop("observation_sink_delivery_failed", item.batch.observations);
248
+ }
226
249
  })
227
250
  .catch(() => {
228
251
  if (!this.timedOut) {
@@ -237,10 +260,95 @@ class SinkDispatcher {
237
260
  }
238
261
  this.notifyIdle();
239
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
+ }
240
345
  timeoutOutstanding() {
241
346
  if (this.timedOut || this.isIdle())
242
347
  return;
243
348
  this.timedOut = true;
349
+ for (const pending of Array.from(this.pendingRetryDelays)) {
350
+ pending.cancel();
351
+ }
244
352
  const queued = [...this.queue];
245
353
  const active = [...this.active];
246
354
  this.queue.length = 0;
@@ -312,7 +420,7 @@ class IterationObservationReporter {
312
420
  this.recordWarning("sink_iteration_batches_unsupported", sink.name, "", -1, 1n, "The reporting sink does not support raw iteration batches.");
313
421
  return false;
314
422
  })
315
- .map((target) => new SinkDispatcher(target, this.settings.sinkQueueDepth, this.settings.sinkParallelism, (code, sinkName, observations) => this.recordSinkDrop(code, sinkName, observations)));
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)));
316
424
  if (this.dispatchers.length > 0) {
317
425
  this.buffer.register(this.streamKey, this.settings.maxBufferBytes);
318
426
  }
@@ -368,11 +476,14 @@ class IterationObservationReporter {
368
476
  this.sealed = true;
369
477
  this.clock.clearInterval(this.timer);
370
478
  this.flushNow();
371
- const started = Date.now();
479
+ const started = node_perf_hooks_1.performance.now();
372
480
  const deadline = started + this.settings.drainTimeoutMs;
481
+ for (const dispatcher of this.dispatchers) {
482
+ dispatcher.beginDrain(deadline);
483
+ }
373
484
  const drainResults = new Array(this.dispatchers.length).fill(false);
374
485
  const completionResults = await Promise.all(this.dispatchers.map(async (dispatcher, index) => {
375
- const drained = await dispatcher.waitForIdle(Math.max(deadline - Date.now(), 1));
486
+ const drained = await dispatcher.waitForIdle(Math.max(deadline - node_perf_hooks_1.performance.now(), 1));
376
487
  drainResults[index] = drained;
377
488
  if (!dispatcher.canPublishCompletion) {
378
489
  return false;
@@ -387,19 +498,7 @@ class IterationObservationReporter {
387
498
  && dispatcher.droppedCount === 0n
388
499
  && dispatcher.deliveredCount === this.captured;
389
500
  const completion = this.createCompletion(dispatcher, sinkReportingComplete);
390
- let timer;
391
- const outcome = await Promise.race([
392
- Promise.resolve()
393
- .then(() => complete(completion))
394
- .then(() => "success", () => "failed"),
395
- new Promise((resolve) => {
396
- timer = setTimeout(() => resolve("timeout"), Math.max(deadline - Date.now(), 1));
397
- timer.unref?.();
398
- })
399
- ]);
400
- if (timer) {
401
- clearTimeout(timer);
402
- }
501
+ const outcome = await this.completeWithRetries(complete, completion, deadline, dispatcher.target.name);
403
502
  if (outcome === "success") {
404
503
  return true;
405
504
  }
@@ -435,6 +534,76 @@ class IterationObservationReporter {
435
534
  };
436
535
  return { ...this.finalResult };
437
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
+ }
438
607
  buildWarnings() {
439
608
  return Array.from(this.warnings.values())
440
609
  .sort((left, right) => left.code.localeCompare(right.code)
@@ -698,6 +867,10 @@ function truncateUtf8(value, maxBytes) {
698
867
  }
699
868
  return output;
700
869
  }
870
+ function delayCanFit(delayMs, deadlineMs) {
871
+ const remainingMs = deadlineMs - node_perf_hooks_1.performance.now();
872
+ return remainingMs > 0 && delayMs < remainingMs;
873
+ }
701
874
  function deepFreezeObservation(observation) {
702
875
  for (const step of observation.steps) {
703
876
  Object.freeze(step);
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.emptyLocalReportInput = emptyLocalReportInput;
4
+ exports.distributedLocalReportInput = distributedLocalReportInput;
5
+ function emptyLocalReportInput() {
6
+ return {
7
+ history: {
8
+ status: "available",
9
+ points: []
10
+ }
11
+ };
12
+ }
13
+ function distributedLocalReportInput() {
14
+ return {
15
+ history: {
16
+ status: "unavailable",
17
+ reasonCategory: "distributed_temporal_aggregation_unavailable",
18
+ points: []
19
+ }
20
+ };
21
+ }
package/dist/cjs/local.js CHANGED
@@ -1418,9 +1418,20 @@ function readTrackingId(payload, selector) {
1418
1418
  return null;
1419
1419
  }
1420
1420
  let current = body;
1421
- for (const segment of path.split(".").filter(Boolean)) {
1421
+ let segments;
1422
+ try {
1423
+ segments = safeJsonPathSegments(path);
1424
+ }
1425
+ catch {
1426
+ return null;
1427
+ }
1428
+ for (const segment of segments) {
1422
1429
  if (current && typeof current === "object" && !Array.isArray(current)) {
1423
- current = current[segment];
1430
+ const record = current;
1431
+ if (!Object.prototype.hasOwnProperty.call(record, segment)) {
1432
+ return null;
1433
+ }
1434
+ current = record[segment];
1424
1435
  }
1425
1436
  else {
1426
1437
  return null;
@@ -1453,23 +1464,57 @@ function readOptionalTrackingSelectorValue(value) {
1453
1464
  return undefined;
1454
1465
  }
1455
1466
  function setJsonPathValue(body, path, value) {
1456
- const target = body && typeof body === "object" && !Array.isArray(body)
1457
- ? { ...body }
1458
- : {};
1459
- const segments = path.split(".").filter(Boolean);
1467
+ const target = cloneJsonRecord(body);
1468
+ const segments = safeJsonPathSegments(path);
1460
1469
  if (!segments.length) {
1461
1470
  return target;
1462
1471
  }
1463
1472
  let current = target;
1464
1473
  for (let i = 0; i < segments.length - 1; i += 1) {
1465
1474
  const segment = segments[i];
1466
- const next = current[segment];
1475
+ const next = readOwnJsonProperty(current, segment);
1476
+ let child;
1467
1477
  if (!next || typeof next !== "object" || Array.isArray(next)) {
1468
- current[segment] = {};
1478
+ child = {};
1469
1479
  }
1470
- current = current[segment];
1480
+ else {
1481
+ child = cloneJsonRecord(next);
1482
+ }
1483
+ defineJsonProperty(current, segment, child);
1484
+ current = child;
1485
+ }
1486
+ defineJsonProperty(current, segments[segments.length - 1], value);
1487
+ return target;
1488
+ }
1489
+ const FORBIDDEN_JSON_PATH_SEGMENTS = new Set(["__proto__", "prototype", "constructor"]);
1490
+ function safeJsonPathSegments(path) {
1491
+ const segments = path.split(".").filter(Boolean);
1492
+ const forbidden = segments.find((segment) => FORBIDDEN_JSON_PATH_SEGMENTS.has(segment));
1493
+ if (forbidden) {
1494
+ throw new Error(`Tracking selector contains forbidden JSON path segment '${forbidden}'.`);
1495
+ }
1496
+ return segments;
1497
+ }
1498
+ function defineJsonProperty(target, key, value) {
1499
+ Object.defineProperty(target, key, {
1500
+ configurable: true,
1501
+ enumerable: true,
1502
+ value,
1503
+ writable: true
1504
+ });
1505
+ }
1506
+ function readOwnJsonProperty(target, key) {
1507
+ const descriptor = Object.getOwnPropertyDescriptor(target, key);
1508
+ return descriptor && "value" in descriptor ? descriptor.value : undefined;
1509
+ }
1510
+ function cloneJsonRecord(value) {
1511
+ const target = {};
1512
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1513
+ return target;
1514
+ }
1515
+ for (const [key, entry] of Object.entries(value)) {
1516
+ defineJsonProperty(target, key, entry);
1471
1517
  }
1472
- current[segments[segments.length - 1]] = value;
1473
1518
  return target;
1474
1519
  }
1475
1520
  function mapCorrelationStore(tracking, runNamespace) {