@loadstrike/loadstrike-sdk 1.0.31001 → 1.0.32601
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 +16 -2
- package/dist/cjs/internal/prometheus-remote-write.js +37 -0
- package/dist/cjs/internal/reporting-sink-http-error.js +17 -0
- package/dist/cjs/internal/vendor-metric-payloads.js +390 -0
- package/dist/cjs/iteration-observations.js +24 -8
- package/dist/cjs/local-report-input.js +21 -0
- package/dist/cjs/local.js +48 -63
- package/dist/cjs/report-history.js +421 -0
- package/dist/cjs/reporting-containment.js +242 -0
- package/dist/cjs/reporting-svg.js +116 -0
- package/dist/cjs/reporting.js +413 -136
- package/dist/cjs/runtime.js +237 -8
- package/dist/cjs/sinks.js +1337 -38
- package/dist/cjs/transports.js +1339 -151
- package/dist/esm/internal/prometheus-remote-write.js +31 -0
- package/dist/esm/internal/reporting-sink-http-error.js +13 -0
- package/dist/esm/internal/vendor-metric-payloads.js +382 -0
- package/dist/esm/iteration-observations.js +24 -8
- package/dist/esm/local-report-input.js +17 -0
- package/dist/esm/local.js +49 -64
- package/dist/esm/report-history.js +413 -0
- package/dist/esm/reporting-containment.js +238 -0
- package/dist/esm/reporting-svg.js +113 -0
- package/dist/esm/reporting.js +413 -136
- package/dist/esm/runtime.js +239 -10
- package/dist/esm/sinks.js +1334 -35
- package/dist/esm/transports.js +1335 -151
- package/dist/types/contracts.d.ts +1 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/internal/prometheus-remote-write.d.ts +2 -0
- package/dist/types/internal/reporting-sink-http-error.d.ts +6 -0
- package/dist/types/internal/vendor-metric-payloads.d.ts +48 -0
- package/dist/types/local-report-input.d.ts +6 -0
- package/dist/types/local.d.ts +0 -6
- package/dist/types/report-history.d.ts +124 -0
- package/dist/types/reporting-containment.d.ts +2 -0
- package/dist/types/reporting-svg.d.ts +2 -0
- package/dist/types/reporting.d.ts +6 -3
- package/dist/types/runtime.d.ts +24 -0
- package/dist/types/sinks.d.ts +134 -17
- package/dist/types/transports.d.ts +2 -0
- package/package.json +9 -3
- package/dist/cjs/internal-build.js +0 -4
- package/dist/esm/internal-build.js +0 -1
- package/dist/types/internal-build.d.ts +0 -1
package/dist/esm/runtime.js
CHANGED
|
@@ -5,15 +5,20 @@ import { resolve } from "node:path";
|
|
|
5
5
|
import { LoadStrikeLocalClient } from "./local.js";
|
|
6
6
|
import { DistributedClusterAgent, DistributedClusterCoordinator, buildLoadEngineV2ReservedStepOtherIdentityKey, buildLoadEngineV2GlobalInvocationId, buildLoadEngineV2ScenarioIdentityKey, buildLoadEngineV2SchedulerIdentityKey, buildLoadEngineV2StatusIdentityKey, buildLoadEngineV2StepIdentityKey, resolveLoadEngineV2StepIdentityKey, buildLoadEngineV2Plan } from "./cluster.js";
|
|
7
7
|
import { CorrelationStoreConfiguration, CrossPlatformTrackingRuntime, RedisCorrelationStore, RedisCorrelationStoreOptions, TrackingFieldSelector } from "./correlation.js";
|
|
8
|
-
import { EndpointAdapterFactory, LOADSTRIKE_TRACE_ID_TRACKING_FIELD } from "./transports.js";
|
|
8
|
+
import { EndpointAdapterFactory, LOADSTRIKE_TRACE_ID_TRACKING_FIELD, validateNativeEndpointExecutionSupport } from "./transports.js";
|
|
9
9
|
import { buildDotnetCsvReport, buildDotnetHtmlReport, buildDotnetMarkdownReport, buildDotnetTxtReport } from "./reporting.js";
|
|
10
|
-
import {
|
|
10
|
+
import { ReportHistoryCollector, ReportHistoryLifecycleCoordinator, ReportHistoryWorker, sanitizedExceptionClassChain } from "./report-history.js";
|
|
11
|
+
import { distributedLocalReportInput, emptyLocalReportInput } from "./local-report-input.js";
|
|
12
|
+
import { PortalReportingSink, PrometheusRemoteWriteReportingSink, cloneReportingSinkForRun } from "./sinks.js";
|
|
13
|
+
import { ReportingSinkHttpError } from "./internal/reporting-sink-http-error.js";
|
|
14
|
+
import { assertNoUnsupportedReportingSinkGraph } from "./reporting-containment.js";
|
|
11
15
|
import { LoadEngineV2ExecutionBudget, buildLoadEngineV2TrafficMixSeedId, LoadStrikeHistogramV1, parseLoadEngineV2HistogramArtifact, serializeLoadEngineV2HistogramArtifact, classifyLoadEngineV2Arrival, loadEngineV2FixedArrivalCount, loadEngineV2FixedDeadlineNs, loadEngineV2LatenessToleranceNs, loadEngineV2TrafficMixLaneUnitCount, loadEngineV2TrafficMixOwnedUnits, planRampingInjectionDeadlines, planRampingConstantDeadlines, planRandomInjectionDeadlines, fnv1a32 } from "./load-engine-v2.js";
|
|
12
16
|
import { DEFAULT_ITERATION_OBSERVATION_SETTINGS, IterationObservationReporter, createIterationObservation, createIterationStepObservation, utcNowNs, validateIterationObservationSettings } from "./iteration-observations.js";
|
|
13
17
|
import { logIterationObservationFailure, logIterationObservationRecovery } from "./iteration-observation-diagnostics.js";
|
|
14
18
|
import { normalizeSinkRetryBackoffMs, normalizeSinkRetryCount, sinkRetryDelayMs, waitForSinkRetryDelay } from "./sink-retry-policy.js";
|
|
15
19
|
const LOAD_ENGINE_V2_SCHEDULER_DISTRIBUTIONS = Symbol("loadstrike.load-engine-v2.scheduler-distributions");
|
|
16
20
|
const PORTAL_RUN_TOKEN_PROVIDER = Symbol.for("loadstrike.internal.portal-run-token-provider");
|
|
21
|
+
const REPORTING_INTERVAL_SECONDS = Symbol.for("loadstrike.internal.reporting-interval-seconds");
|
|
17
22
|
export const LoadStrikeNodeType = {
|
|
18
23
|
SingleNode: "SingleNode",
|
|
19
24
|
Coordinator: "Coordinator",
|
|
@@ -283,6 +288,28 @@ class MeasurementAccumulator {
|
|
|
283
288
|
moreOrEq1200: left.moreOrEq1200 + right.moreOrEq1200
|
|
284
289
|
}, allRequestCount, durationMs);
|
|
285
290
|
}
|
|
291
|
+
/**
|
|
292
|
+
* Projects only the cumulative fields used by the private HTML-report
|
|
293
|
+
* history. Legacy distributions deliberately omit latency so this cadence
|
|
294
|
+
* path never scans their retained per-request arrays.
|
|
295
|
+
*/
|
|
296
|
+
buildReportHistoryMeasurement() {
|
|
297
|
+
return projectNativeReportHistoryMeasurement(this.count, this.allBytes, this.useHistogram ? this.latencyHistogram : undefined);
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Combines only bounded native latency state plus exact counters. It does
|
|
301
|
+
* not materialize status rows or any public measurement DTO.
|
|
302
|
+
*/
|
|
303
|
+
buildCombinedReportHistoryMeasurement(other) {
|
|
304
|
+
const count = this.count + other.count;
|
|
305
|
+
const bytes = this.allBytes + other.allBytes;
|
|
306
|
+
if (!this.useHistogram || !other.useHistogram) {
|
|
307
|
+
return projectNativeReportHistoryMeasurement(count, bytes);
|
|
308
|
+
}
|
|
309
|
+
const latency = this.latencyHistogram.clone();
|
|
310
|
+
latency.merge(other.latencyHistogram);
|
|
311
|
+
return projectNativeReportHistoryMeasurement(count, bytes, latency);
|
|
312
|
+
}
|
|
286
313
|
histogramSnapshot() {
|
|
287
314
|
return {
|
|
288
315
|
count: this.count,
|
|
@@ -296,6 +323,24 @@ class MeasurementAccumulator {
|
|
|
296
323
|
};
|
|
297
324
|
}
|
|
298
325
|
}
|
|
326
|
+
function projectNativeReportHistoryMeasurement(count, bytes, latency) {
|
|
327
|
+
const measurement = {
|
|
328
|
+
count,
|
|
329
|
+
bytes,
|
|
330
|
+
approximate: latency?.maxRelativeError !== undefined
|
|
331
|
+
&& latency.maxRelativeError > 0
|
|
332
|
+
};
|
|
333
|
+
if (!latency || latency.count === 0n) {
|
|
334
|
+
return measurement;
|
|
335
|
+
}
|
|
336
|
+
return {
|
|
337
|
+
...measurement,
|
|
338
|
+
percent50Ms: Number(latency.percentile(0.5)) / 1000,
|
|
339
|
+
percent75Ms: Number(latency.percentile(0.75)) / 1000,
|
|
340
|
+
percent95Ms: Number(latency.percentile(0.95)) / 1000,
|
|
341
|
+
percent99Ms: Number(latency.percentile(0.99)) / 1000
|
|
342
|
+
};
|
|
343
|
+
}
|
|
299
344
|
function buildHistogramMeasurement(snapshot, allRequestCount, durationMs) {
|
|
300
345
|
const count = snapshot.count;
|
|
301
346
|
const totalDurationMs = Math.max(durationMs, 0);
|
|
@@ -634,6 +679,21 @@ class ScenarioStatsAccumulator {
|
|
|
634
679
|
step.record(reply, observedLatencyMs);
|
|
635
680
|
return step.sortIndex;
|
|
636
681
|
}
|
|
682
|
+
/**
|
|
683
|
+
* Captures the bounded, private history view without building scenario
|
|
684
|
+
* steps, status rows, plugins, aliases, or any other public result shape.
|
|
685
|
+
*/
|
|
686
|
+
buildReportHistorySnapshot() {
|
|
687
|
+
const ok = this.ok.buildReportHistoryMeasurement();
|
|
688
|
+
const failed = this.fail.buildReportHistoryMeasurement();
|
|
689
|
+
return {
|
|
690
|
+
scenarioName: this.scenarioName,
|
|
691
|
+
sortIndex: this.sortIndex,
|
|
692
|
+
all: this.ok.buildCombinedReportHistoryMeasurement(this.fail),
|
|
693
|
+
...(ok.count > 0 ? { ok } : {}),
|
|
694
|
+
...(failed.count > 0 ? { failed } : {})
|
|
695
|
+
};
|
|
696
|
+
}
|
|
637
697
|
/**
|
|
638
698
|
* Builds the configured payload or helper object.
|
|
639
699
|
* Use this when all builder inputs are ready to be materialized.
|
|
@@ -682,6 +742,15 @@ class ScenarioStatsAccumulator {
|
|
|
682
742
|
return attachScenarioStatsAliases(scenario);
|
|
683
743
|
}
|
|
684
744
|
}
|
|
745
|
+
function isRetryableReportingSinkError(sink, error) {
|
|
746
|
+
if (!(sink instanceof PrometheusRemoteWriteReportingSink)) {
|
|
747
|
+
return true;
|
|
748
|
+
}
|
|
749
|
+
if (!(error instanceof ReportingSinkHttpError)) {
|
|
750
|
+
return true;
|
|
751
|
+
}
|
|
752
|
+
return error.status === 429 || error.status < 400 || error.status >= 500;
|
|
753
|
+
}
|
|
685
754
|
export class LoadStrikeResponse {
|
|
686
755
|
/**
|
|
687
756
|
* Creates a successful reply.
|
|
@@ -3410,6 +3479,8 @@ export class LoadStrikeRunner {
|
|
|
3410
3479
|
return this.buildContext();
|
|
3411
3480
|
}
|
|
3412
3481
|
async run(args = []) {
|
|
3482
|
+
assertNoUnsupportedReportingSinkGraph(this.options.reportingSinks ?? []);
|
|
3483
|
+
assertNoUnsupportedNativeTrackingScenarios(this.scenarios);
|
|
3413
3484
|
if (this.contextConfigurators.length) {
|
|
3414
3485
|
return new LoadStrikeRunner(this.scenarios, this.buildContext().toRunnerOptions(), [], this.internalOptions).run(args);
|
|
3415
3486
|
}
|
|
@@ -3421,7 +3492,7 @@ export class LoadStrikeRunner {
|
|
|
3421
3492
|
const scenarioAccumulators = new Map();
|
|
3422
3493
|
const scenarioDurationsMs = new Map();
|
|
3423
3494
|
const stepStats = new Map();
|
|
3424
|
-
const sinks = (this.options.reportingSinks ?? []).map((sink) => cloneReportingSinkForRun(sink));
|
|
3495
|
+
const sinks = (this.options.reportingSinks ?? []).map((sink) => cloneReportingSinkForRun(sink, this.options.infraConfig ?? {}));
|
|
3425
3496
|
const sinkStates = sinks.map((sink, index) => ({
|
|
3426
3497
|
sink,
|
|
3427
3498
|
disabled: false,
|
|
@@ -3481,6 +3552,58 @@ export class LoadStrikeRunner {
|
|
|
3481
3552
|
licenseSession = await licenseClient.acquireLicenseLease(licensePayload);
|
|
3482
3553
|
const loggerSetup = createLoggerSetup(this.options.loggerConfig, this.options.minimumLogLevel, this.options, testInfo, nodeInfo);
|
|
3483
3554
|
const runLogger = loggerSetup.logger;
|
|
3555
|
+
const htmlReportHistoryRequested = (this.options.reportsEnabled ?? true)
|
|
3556
|
+
&& normalizeReportFormats(this.options.reportFormats ?? ["html", "txt", "csv", "md"]).includes("html");
|
|
3557
|
+
const distributedReportHistory = isDistributedReportHistoryExecution(clusterMode, this.options);
|
|
3558
|
+
let localReportInput = htmlReportHistoryRequested
|
|
3559
|
+
&& distributedReportHistory
|
|
3560
|
+
? distributedLocalReportInput()
|
|
3561
|
+
: emptyLocalReportInput();
|
|
3562
|
+
let reportHistoryCollector;
|
|
3563
|
+
let reportHistoryLifecycle;
|
|
3564
|
+
if (htmlReportHistoryRequested && !distributedReportHistory) {
|
|
3565
|
+
const warnReportHistoryFailure = (category, error) => {
|
|
3566
|
+
const exceptionClasses = error === undefined
|
|
3567
|
+
? ""
|
|
3568
|
+
: `; exception_classes=${sanitizedExceptionClassChain(error)}`;
|
|
3569
|
+
try {
|
|
3570
|
+
runLogger.warn(`LoadStrike report history unavailable: ${category}${exceptionClasses}.`);
|
|
3571
|
+
}
|
|
3572
|
+
catch {
|
|
3573
|
+
// Report-only diagnostics are best-effort.
|
|
3574
|
+
}
|
|
3575
|
+
};
|
|
3576
|
+
reportHistoryCollector = new ReportHistoryCollector({
|
|
3577
|
+
scenarioCount: selectedScenarios.length,
|
|
3578
|
+
onCaptureError: (error) => warnReportHistoryFailure("capture_failure", error)
|
|
3579
|
+
});
|
|
3580
|
+
if (!reportHistoryCollector.available) {
|
|
3581
|
+
warnReportHistoryFailure("budget_pressure");
|
|
3582
|
+
}
|
|
3583
|
+
const snapshotFactory = () => Array.from(scenarioAccumulators.values())
|
|
3584
|
+
.map((value) => value.buildReportHistorySnapshot())
|
|
3585
|
+
.sort((left, right) => (left.sortIndex ?? 0) - (right.sortIndex ?? 0)
|
|
3586
|
+
|| left.scenarioName.localeCompare(right.scenarioName));
|
|
3587
|
+
const reportHistoryWorker = new ReportHistoryWorker({
|
|
3588
|
+
collector: reportHistoryCollector,
|
|
3589
|
+
cadenceSeconds: resolvedReportHistoryCadenceSeconds(this.options.reportingIntervalSeconds ?? 5),
|
|
3590
|
+
snapshotFactory,
|
|
3591
|
+
onFailure: (category, exceptionClasses) => {
|
|
3592
|
+
try {
|
|
3593
|
+
runLogger.warn(`LoadStrike report history unavailable: ${category}; exception_classes=${exceptionClasses}.`);
|
|
3594
|
+
}
|
|
3595
|
+
catch {
|
|
3596
|
+
// Report-only diagnostics are best-effort.
|
|
3597
|
+
}
|
|
3598
|
+
}
|
|
3599
|
+
});
|
|
3600
|
+
reportHistoryLifecycle = new ReportHistoryLifecycleCoordinator({
|
|
3601
|
+
scenarioCount: selectedScenarios.length,
|
|
3602
|
+
start: () => reportHistoryWorker.start(),
|
|
3603
|
+
stopAndFinalize: () => reportHistoryWorker.stopAndFinalize(),
|
|
3604
|
+
stopWithoutFinalizing: () => reportHistoryWorker.stopWithoutFinalizing()
|
|
3605
|
+
});
|
|
3606
|
+
}
|
|
3484
3607
|
const scenarioStartInfos = selectedScenarios.map((scenario, index) => {
|
|
3485
3608
|
const startInfo = {
|
|
3486
3609
|
scenarioName: scenario.name,
|
|
@@ -3494,6 +3617,12 @@ export class LoadStrikeRunner {
|
|
|
3494
3617
|
testInfo,
|
|
3495
3618
|
getNodeInfo: () => attachNodeInfoAliases({ ...nodeInfo })
|
|
3496
3619
|
};
|
|
3620
|
+
Object.defineProperty(baseContext, REPORTING_INTERVAL_SECONDS, {
|
|
3621
|
+
value: this.options.reportingIntervalSeconds ?? 5,
|
|
3622
|
+
enumerable: false,
|
|
3623
|
+
configurable: false,
|
|
3624
|
+
writable: false
|
|
3625
|
+
});
|
|
3497
3626
|
attachBaseContextAliases(baseContext);
|
|
3498
3627
|
const sinkSession = {
|
|
3499
3628
|
startedUtc: createdUtc,
|
|
@@ -3548,6 +3677,7 @@ export class LoadStrikeRunner {
|
|
|
3548
3677
|
name: state.name,
|
|
3549
3678
|
iterationObservationPortalSink: Boolean(state.sink.iterationObservationPortalSink),
|
|
3550
3679
|
iterationObservationShapeLimited: Boolean(state.sink.iterationObservationShapeLimited),
|
|
3680
|
+
retryableErrorClassifier: (error) => isRetryableReportingSinkError(state.sink, error),
|
|
3551
3681
|
saveIterationBatch: resolveSinkSaveIterationBatch(state.sink),
|
|
3552
3682
|
completeIterationObservationStream: resolveSinkCompleteIterationObservationStream(state.sink)
|
|
3553
3683
|
}));
|
|
@@ -3590,7 +3720,7 @@ export class LoadStrikeRunner {
|
|
|
3590
3720
|
realtimeInFlight = false;
|
|
3591
3721
|
}
|
|
3592
3722
|
};
|
|
3593
|
-
const reportingIntervalMs =
|
|
3723
|
+
const reportingIntervalMs = boundedReportingIntervalMs(this.options.reportingIntervalSeconds ?? 5);
|
|
3594
3724
|
const triggerRealtimeSnapshot = () => {
|
|
3595
3725
|
if (realtimeCurrent) {
|
|
3596
3726
|
return;
|
|
@@ -3677,6 +3807,7 @@ export class LoadStrikeRunner {
|
|
|
3677
3807
|
iterationObservationRunId,
|
|
3678
3808
|
iterationObservationResultOwnerId,
|
|
3679
3809
|
iterationObservationProcessGroup,
|
|
3810
|
+
reportHistoryLifecycle,
|
|
3680
3811
|
executeScenarioInvocation: (targetScenario, context, operation) => this.executeScenarioInvocation(targetScenario, context, operation),
|
|
3681
3812
|
invokeBeforeScenario: (runtimePolicies, scenarioName) => this.invokeBeforeScenario(runtimePolicies, scenarioName, policyErrors, runtimePolicyErrorMode),
|
|
3682
3813
|
invokeAfterScenario: (runtimePolicies, scenarioName, stats) => this.invokeAfterScenario(runtimePolicies, scenarioName, stats, policyErrors, runtimePolicyErrorMode),
|
|
@@ -3764,9 +3895,14 @@ export class LoadStrikeRunner {
|
|
|
3764
3895
|
...iterationObservationReporter.buildWarnings()
|
|
3765
3896
|
];
|
|
3766
3897
|
result.pluginsData = mergePluginData(result.pluginsData, await this.collectPluginData(plugins, attachRunResultAliases(result), pluginLifecycleErrors));
|
|
3898
|
+
if (reportHistoryCollector) {
|
|
3899
|
+
localReportInput = {
|
|
3900
|
+
history: reportHistoryCollector.toProjection()
|
|
3901
|
+
};
|
|
3902
|
+
}
|
|
3767
3903
|
const finalizedResult = attachRunResultAliases(result);
|
|
3768
3904
|
finalizedResult.logFiles = mergeStringArrays(finalizedResult.logFiles, loggerSetup.logFiles);
|
|
3769
|
-
finalizedResult.reportFiles = this.writeReports(finalizedResult);
|
|
3905
|
+
finalizedResult.reportFiles = this.writeReports(finalizedResult, localReportInput);
|
|
3770
3906
|
finalizedResult.disabledSinks = sinkStates.filter((x) => x.disabled).map((x) => x.name);
|
|
3771
3907
|
await this.emitRunResult(sinkStates, finalizedResult, sinkRetryCount, sinkRetryBackoffMs, sinkErrors, testInfo.sessionId, runLogger);
|
|
3772
3908
|
await this.stopSinks(sinkStates, sinkRetryCount, sinkRetryBackoffMs, sinkErrors, testInfo.sessionId, runLogger);
|
|
@@ -3777,6 +3913,7 @@ export class LoadStrikeRunner {
|
|
|
3777
3913
|
return finalizedResult;
|
|
3778
3914
|
}
|
|
3779
3915
|
finally {
|
|
3916
|
+
reportHistoryLifecycle?.stopWithoutFinalizing();
|
|
3780
3917
|
await stopRealtimeReporting();
|
|
3781
3918
|
if (!iterationObservationsFinalized) {
|
|
3782
3919
|
await iterationObservationReporter.sealAndDrain().catch(() => { });
|
|
@@ -4175,7 +4312,9 @@ export class LoadStrikeRunner {
|
|
|
4175
4312
|
return;
|
|
4176
4313
|
}
|
|
4177
4314
|
catch (error) {
|
|
4178
|
-
const
|
|
4315
|
+
const retryable = isRetryableReportingSinkError(state.sink, error);
|
|
4316
|
+
const exhausted = attempts >= maximumAttempts || !retryable;
|
|
4317
|
+
const reportedMaximumAttempts = retryable ? maximumAttempts : attempts;
|
|
4179
4318
|
const nextDelayMs = exhausted ? 0 : sinkRetryDelayMs(backoffMs, attempts);
|
|
4180
4319
|
logIterationObservationFailure(logger, exhausted ? "error" : "warn", {
|
|
4181
4320
|
sinkName: state.name,
|
|
@@ -4185,7 +4324,7 @@ export class LoadStrikeRunner {
|
|
|
4185
4324
|
resultOwnerId: "",
|
|
4186
4325
|
observationCount: 0,
|
|
4187
4326
|
attempt: attempts,
|
|
4188
|
-
maximumAttempts,
|
|
4327
|
+
maximumAttempts: reportedMaximumAttempts,
|
|
4189
4328
|
nextDelayMs
|
|
4190
4329
|
}, error);
|
|
4191
4330
|
if (exhausted) {
|
|
@@ -4254,7 +4393,7 @@ export class LoadStrikeRunner {
|
|
|
4254
4393
|
message: String(error ?? "runtime policy callback failed")
|
|
4255
4394
|
});
|
|
4256
4395
|
}
|
|
4257
|
-
writeReports(result) {
|
|
4396
|
+
writeReports(result, localReportInput = emptyLocalReportInput()) {
|
|
4258
4397
|
const reportsEnabled = this.options.reportsEnabled ?? true;
|
|
4259
4398
|
if (!reportsEnabled) {
|
|
4260
4399
|
return [];
|
|
@@ -4279,7 +4418,7 @@ export class LoadStrikeRunner {
|
|
|
4279
4418
|
writeFileSync(path, buildDotnetMarkdownReport(nodeStats), "utf8");
|
|
4280
4419
|
}
|
|
4281
4420
|
else if (format === "html") {
|
|
4282
|
-
writeFileSync(path, buildDotnetHtmlReport(nodeStats), "utf8");
|
|
4421
|
+
writeFileSync(path, buildDotnetHtmlReport(nodeStats, localReportInput), "utf8");
|
|
4283
4422
|
}
|
|
4284
4423
|
written.push(path);
|
|
4285
4424
|
}
|
|
@@ -4312,6 +4451,19 @@ function hasPluginRows(value) {
|
|
|
4312
4451
|
}
|
|
4313
4452
|
return value.tables.some((table) => Array.isArray(table.rows) && table.rows.length > 0);
|
|
4314
4453
|
}
|
|
4454
|
+
function boundedReportingIntervalMs(seconds) {
|
|
4455
|
+
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
4456
|
+
return 1;
|
|
4457
|
+
}
|
|
4458
|
+
const milliseconds = seconds * 1000;
|
|
4459
|
+
if (!Number.isFinite(milliseconds) || milliseconds >= 2147483647) {
|
|
4460
|
+
return 2147483647;
|
|
4461
|
+
}
|
|
4462
|
+
return Math.max(Math.trunc(milliseconds), 1);
|
|
4463
|
+
}
|
|
4464
|
+
function resolvedReportHistoryCadenceSeconds(seconds) {
|
|
4465
|
+
return boundedReportingIntervalMs(seconds) / 1000;
|
|
4466
|
+
}
|
|
4315
4467
|
async function executeV2FixedArrivals(args) {
|
|
4316
4468
|
const { rate, intervalNs, totalArrivals, budget, cancellationToken, shouldStopNow, nextInstanceInfo, runBombingInvocation, logger, scenarioName, deadlineOffsetsNs, tolerancesNs, ownedOrdinals, shardIndex = 0, shardCount = 1, telemetry, segment } = args;
|
|
4317
4469
|
const segmentStartNs = process.hrtime.bigint();
|
|
@@ -4448,7 +4600,15 @@ function maxBigInt(left, right) {
|
|
|
4448
4600
|
return left > right ? left : right;
|
|
4449
4601
|
}
|
|
4450
4602
|
async function executeScenarioRuntime(args) {
|
|
4451
|
-
const { scenario, scenarioIndex, scenarioCount, options, logger, nodeInfo, testInfo, policies, restartIterationMaxAttempts, allRegisteredMetrics, scenarioRuntimes, stepRuntimes, scenarioAccumulators, scenarioDurationsMs, stopTestState, testAbortController, loadEngineV2Budget, loadEngineV2Telemetry, iterationObservationReporter, iterationObservationRunId = testInfo.sessionId, iterationObservationResultOwnerId = "", iterationObservationProcessGroup = 0, executeScenarioInvocation, invokeBeforeScenario, invokeAfterScenario, invokeBeforeStep, invokeAfterStep } = args;
|
|
4603
|
+
const { scenario, scenarioIndex, scenarioCount, options, logger, nodeInfo, testInfo, policies, restartIterationMaxAttempts, allRegisteredMetrics, scenarioRuntimes, stepRuntimes, scenarioAccumulators, scenarioDurationsMs, stopTestState, testAbortController, loadEngineV2Budget, loadEngineV2Telemetry, iterationObservationReporter, iterationObservationRunId = testInfo.sessionId, iterationObservationResultOwnerId = "", iterationObservationProcessGroup = 0, reportHistoryLifecycle, executeScenarioInvocation, invokeBeforeScenario, invokeAfterScenario, invokeBeforeStep, invokeAfterStep } = args;
|
|
4604
|
+
let reportHistoryExecutionEnded = false;
|
|
4605
|
+
const endReportHistoryExecution = async () => {
|
|
4606
|
+
if (reportHistoryExecutionEnded) {
|
|
4607
|
+
return;
|
|
4608
|
+
}
|
|
4609
|
+
reportHistoryExecutionEnded = true;
|
|
4610
|
+
await reportHistoryLifecycle?.scenarioExecutionEnded();
|
|
4611
|
+
};
|
|
4452
4612
|
const scenarioStartedMs = Date.now();
|
|
4453
4613
|
const scenarioContextData = {};
|
|
4454
4614
|
const registeredMetrics = [];
|
|
@@ -5269,6 +5429,7 @@ async function executeScenarioRuntime(args) {
|
|
|
5269
5429
|
})));
|
|
5270
5430
|
await invokeBeforeScenario(policies, scenario.name);
|
|
5271
5431
|
await runWarmUpAsync();
|
|
5432
|
+
reportHistoryLifecycle?.scenarioBombingStarted();
|
|
5272
5433
|
accumulator.setCurrentOperation("Bombing");
|
|
5273
5434
|
const simulations = scenario.getSimulations();
|
|
5274
5435
|
if (!simulations.length) {
|
|
@@ -5302,6 +5463,7 @@ async function executeScenarioRuntime(args) {
|
|
|
5302
5463
|
}
|
|
5303
5464
|
}
|
|
5304
5465
|
accumulator.setCurrentOperation(stopScenario || stopTestState.value ? "Stop" : "Complete");
|
|
5466
|
+
await endReportHistoryExecution();
|
|
5305
5467
|
await invokeAfterScenario(policies, scenario.name, runtime);
|
|
5306
5468
|
}
|
|
5307
5469
|
catch (error) {
|
|
@@ -5318,6 +5480,7 @@ async function executeScenarioRuntime(args) {
|
|
|
5318
5480
|
}
|
|
5319
5481
|
}
|
|
5320
5482
|
finally {
|
|
5483
|
+
await endReportHistoryExecution();
|
|
5321
5484
|
scenarioDurationsMs.set(scenario.name, Math.max(Date.now() - scenarioStartedMs, 0));
|
|
5322
5485
|
try {
|
|
5323
5486
|
await scenario.invokeClean({
|
|
@@ -7251,6 +7414,11 @@ function resolveClusterExecutionMode(options) {
|
|
|
7251
7414
|
}
|
|
7252
7415
|
return "single";
|
|
7253
7416
|
}
|
|
7417
|
+
function isDistributedReportHistoryExecution(clusterMode, options) {
|
|
7418
|
+
return clusterMode !== "single"
|
|
7419
|
+
|| options.loadEngineV2SegmentLifecycleOverride !== undefined
|
|
7420
|
+
|| Math.max(Math.trunc(options.clusterShardCount ?? 1), 1) > 1;
|
|
7421
|
+
}
|
|
7254
7422
|
function buildEmptyNodeStats(args) {
|
|
7255
7423
|
return attachNodeStatsAliases({
|
|
7256
7424
|
startedUtc: args.startedUtc,
|
|
@@ -8722,6 +8890,7 @@ class ManagedScenarioTrackingRuntime {
|
|
|
8722
8890
|
}
|
|
8723
8891
|
async dispose() {
|
|
8724
8892
|
this.shutdown = true;
|
|
8893
|
+
this.interruptEndpointAdapters();
|
|
8725
8894
|
this.rejectOutstandingWaiters();
|
|
8726
8895
|
await Promise.all([
|
|
8727
8896
|
this.sourceLoop,
|
|
@@ -8787,6 +8956,7 @@ class ManagedScenarioTrackingRuntime {
|
|
|
8787
8956
|
failureSeen = failureSeen || this.isFailedObservationOutcome(outcome);
|
|
8788
8957
|
}
|
|
8789
8958
|
this.shutdown = true;
|
|
8959
|
+
this.interruptEndpointAdapters();
|
|
8790
8960
|
await Promise.all([
|
|
8791
8961
|
this.sourceLoop,
|
|
8792
8962
|
this.destinationLoop,
|
|
@@ -8803,6 +8973,20 @@ class ManagedScenarioTrackingRuntime {
|
|
|
8803
8973
|
? LoadStrikeResponse.fail("tracking_failures", "One or more observed source or destination events did not correlate successfully.", 0)
|
|
8804
8974
|
: LoadStrikeResponse.ok("observed");
|
|
8805
8975
|
}
|
|
8976
|
+
interruptEndpointAdapters() {
|
|
8977
|
+
try {
|
|
8978
|
+
this.sourceAdapter.interrupt?.();
|
|
8979
|
+
}
|
|
8980
|
+
catch {
|
|
8981
|
+
// Shutdown remains best-effort and still disposes every adapter below.
|
|
8982
|
+
}
|
|
8983
|
+
try {
|
|
8984
|
+
this.destinationAdapter?.interrupt?.();
|
|
8985
|
+
}
|
|
8986
|
+
catch {
|
|
8987
|
+
// Shutdown remains best-effort and still disposes every adapter below.
|
|
8988
|
+
}
|
|
8989
|
+
}
|
|
8806
8990
|
async consumeSourceLoop() {
|
|
8807
8991
|
while (!this.shutdown) {
|
|
8808
8992
|
try {
|
|
@@ -9394,6 +9578,20 @@ function mapRuntimeTrackingEndpointSpec(spec, useLoadStrikeTraceIdHeader = false
|
|
|
9394
9578
|
azureEventHubs: asTrackingRecord(pickTrackingValue(spec, "AzureEventHubs", "azureEventHubs")),
|
|
9395
9579
|
sqs: asTrackingRecord(pickTrackingValue(spec, "Sqs", "sqs")),
|
|
9396
9580
|
pushDiffusion: asTrackingRecord(pickTrackingValue(spec, "PushDiffusion", "pushDiffusion")),
|
|
9581
|
+
grpc: mapRuntimeEndpointProtocolOptions(spec, ["Grpc", "grpc"], [
|
|
9582
|
+
"Target", "target", "ServiceName", "serviceName", "MethodName", "methodName",
|
|
9583
|
+
"MethodType", "methodType", "Deadline", "deadline", "DeadlineSeconds", "deadlineSeconds",
|
|
9584
|
+
"DeadlineMs", "deadlineMs", "Metadata", "metadata", "Produce", "produce", "Consume", "consume",
|
|
9585
|
+
"ProduceAsync", "produceAsync", "ConsumeAsync", "consumeAsync", "ConnectionMetadata", "connectionMetadata",
|
|
9586
|
+
"NativeClient", "nativeClient"
|
|
9587
|
+
]),
|
|
9588
|
+
webSocket: mapRuntimeEndpointProtocolOptions(spec, ["WebSocket", "webSocket"], [
|
|
9589
|
+
"Url", "url", "Subprotocols", "subprotocols", "ConnectTimeout", "connectTimeout",
|
|
9590
|
+
"ConnectTimeoutSeconds", "connectTimeoutSeconds", "ConnectTimeoutMs", "connectTimeoutMs",
|
|
9591
|
+
"CloseTimeout", "closeTimeout", "CloseTimeoutSeconds", "closeTimeoutSeconds", "CloseTimeoutMs", "closeTimeoutMs",
|
|
9592
|
+
"Produce", "produce", "Consume", "consume", "ProduceAsync", "produceAsync", "ConsumeAsync", "consumeAsync",
|
|
9593
|
+
"ConnectionMetadata", "connectionMetadata", "NativeClient", "nativeClient"
|
|
9594
|
+
]),
|
|
9397
9595
|
delegate: typeof delegateProduce === "function"
|
|
9398
9596
|
|| typeof delegateConsume === "function"
|
|
9399
9597
|
|| typeof delegateProduceAsync === "function"
|
|
@@ -9416,6 +9614,19 @@ function mapRuntimeTrackingEndpointSpec(spec, useLoadStrikeTraceIdHeader = false
|
|
|
9416
9614
|
: undefined
|
|
9417
9615
|
};
|
|
9418
9616
|
}
|
|
9617
|
+
function mapRuntimeEndpointProtocolOptions(spec, nestedKeys, flatKeys) {
|
|
9618
|
+
const nested = asTrackingRecord(pickTrackingValue(spec, ...nestedKeys));
|
|
9619
|
+
if (Object.keys(nested).length > 0) {
|
|
9620
|
+
return nested;
|
|
9621
|
+
}
|
|
9622
|
+
const options = {};
|
|
9623
|
+
for (const key of flatKeys) {
|
|
9624
|
+
if (Object.prototype.hasOwnProperty.call(spec, key)) {
|
|
9625
|
+
options[key] = pickTrackingValue(spec, key);
|
|
9626
|
+
}
|
|
9627
|
+
}
|
|
9628
|
+
return options;
|
|
9629
|
+
}
|
|
9419
9630
|
function normalizeRuntimeTrackingPayload(payload, endpoint, index) {
|
|
9420
9631
|
const normalized = {
|
|
9421
9632
|
headers: {
|
|
@@ -10179,6 +10390,20 @@ function assertNoDisableLicenseEnforcementOption(value, source) {
|
|
|
10179
10390
|
}
|
|
10180
10391
|
}
|
|
10181
10392
|
}
|
|
10393
|
+
function assertNoUnsupportedNativeTrackingScenarios(scenarios) {
|
|
10394
|
+
for (const scenario of scenarios) {
|
|
10395
|
+
const tracking = scenario.getTrackingConfiguration();
|
|
10396
|
+
if (!tracking) {
|
|
10397
|
+
continue;
|
|
10398
|
+
}
|
|
10399
|
+
for (const field of ["Source", "Destination"]) {
|
|
10400
|
+
const endpoint = asTrackingRecord(pickTrackingValue(tracking, field, field.toLowerCase()));
|
|
10401
|
+
if (Object.keys(endpoint).length > 0) {
|
|
10402
|
+
validateNativeEndpointExecutionSupport(endpoint);
|
|
10403
|
+
}
|
|
10404
|
+
}
|
|
10405
|
+
}
|
|
10406
|
+
}
|
|
10182
10407
|
function normalizeRunContextCollectionShapes(values) {
|
|
10183
10408
|
assertNoDisableLicenseEnforcementOption(values, "LoadStrikeContext");
|
|
10184
10409
|
const normalized = {
|
|
@@ -11082,6 +11307,7 @@ function buildGroupedCorrelationRows(rows) {
|
|
|
11082
11307
|
LatencyMinMs: latencySamples.length ? formatOptionalLatency(latencySamples[0]) : "",
|
|
11083
11308
|
LatencyMeanMs: latencySamples.length ? formatOptionalLatency(latencySamples.reduce((sum, value) => sum + value, 0) / latencySamples.length) : "",
|
|
11084
11309
|
LatencyP50Ms: latencySamples.length ? formatOptionalLatency(percentile(latencySamples, 0.5)) : "",
|
|
11310
|
+
LatencyP75Ms: latencySamples.length ? formatOptionalLatency(percentile(latencySamples, 0.75)) : "",
|
|
11085
11311
|
LatencyP80Ms: latencySamples.length ? formatOptionalLatency(percentile(latencySamples, 0.8)) : "",
|
|
11086
11312
|
LatencyP85Ms: latencySamples.length ? formatOptionalLatency(percentile(latencySamples, 0.85)) : "",
|
|
11087
11313
|
LatencyP90Ms: latencySamples.length ? formatOptionalLatency(percentile(latencySamples, 0.9)) : "",
|
|
@@ -11294,6 +11520,8 @@ export const __loadstrikeTestExports = {
|
|
|
11294
11520
|
buildThresholdCheckExpression,
|
|
11295
11521
|
buildTrackingLeaseKey,
|
|
11296
11522
|
buildTrackingRunNamespace,
|
|
11523
|
+
boundedReportingIntervalMs,
|
|
11524
|
+
resolvedReportHistoryCadenceSeconds,
|
|
11297
11525
|
clusterNodeResultToNodeStats,
|
|
11298
11526
|
combineAbortSignals,
|
|
11299
11527
|
computeScenarioRequestCount,
|
|
@@ -11313,6 +11541,7 @@ export const __loadstrikeTestExports = {
|
|
|
11313
11541
|
formatUtcReportTimestamp,
|
|
11314
11542
|
hasPluginRows,
|
|
11315
11543
|
inferRuntimeLegacyHttpResponseSource,
|
|
11544
|
+
isDistributedReportHistoryExecution,
|
|
11316
11545
|
isComparisonFailed,
|
|
11317
11546
|
loadJsonObject,
|
|
11318
11547
|
logLevelOrder,
|