@mark1russell7/harness 0.13.0 → 0.14.1
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/build/{cell-D2QNOxE8.d.ts → cell-BccWKiXh.d.ts} +8 -1
- package/build/index.d.ts +7 -3
- package/build/index.js +40 -33
- package/build/index.js.map +1 -1
- package/build/schema.d.ts +1 -1
- package/package.json +4 -3
|
@@ -429,6 +429,9 @@ interface StageBand {
|
|
|
429
429
|
* - `errors` are the errors that the client must meet on the way, each with the band of its
|
|
430
430
|
* first occurrence. An example is the error of the offline queue when it gives up.
|
|
431
431
|
* - `quiet` are the stages that must not occur after the fault. Each one that a run shows is a finding.
|
|
432
|
+
* - `lost` is the band of the count of accepted writes that the group did not keep, in each run. A
|
|
433
|
+
* count outside it is a finding. A measurement-only cell with this band gets its decision from
|
|
434
|
+
* the findings of its runs, thus the law of the count judges the cell.
|
|
432
435
|
*/
|
|
433
436
|
interface GroupPrediction {
|
|
434
437
|
readonly lands: "new" | "same";
|
|
@@ -441,6 +444,10 @@ interface GroupPrediction {
|
|
|
441
444
|
readonly high: Quantity;
|
|
442
445
|
}>;
|
|
443
446
|
readonly quiet?: readonly Stage[];
|
|
447
|
+
readonly lost?: {
|
|
448
|
+
readonly low: number;
|
|
449
|
+
readonly high: number;
|
|
450
|
+
};
|
|
444
451
|
}
|
|
445
452
|
/**
|
|
446
453
|
* A middlebox expectation tells what the router of a topology does with the flow in the window of
|
|
@@ -1163,4 +1170,4 @@ interface RunTopology {
|
|
|
1163
1170
|
}
|
|
1164
1171
|
//#endregion
|
|
1165
1172
|
export { StageBand as $, PinnedImage as A, Replies as B, Jitter as C, MiddleboxExpectation as D, MiddleboxEvent as E, Reconnects as F, RunGroup as G, RouterHop as H, Recovers as I, SentinelEvent as J, RunPart as K, Reference as L, ProbeEvent as M, ProxyHop as N, MiddleboxForm as O, ProxyTime as P, Stage as Q, ReplicaSet as R, ImageSource as S, LineMatch as T, RttRange as U, RequestPrediction as V, Run as W, Shipped as X, SentinelStage as Y, ShippedImage as Z, HopEvent as _, Cluster as a, isHopPlace as at, HopWrite as b, DriverBand as c, FaultPlace as d, Subject as et, Group as f, Hop as g, GroupRole as h, ClientImage as i, WsProbe as it, Prediction as j, Observer as k, DriverStep as l, GroupPrediction as m, Cell as n, Topology as nt, ClusterClientAt as o, latestEndOf as ot, GroupPlace as p, RunTopology as q, Client as r, Toxic as rt, ClusterStage as s, BuildImage as t, Target as tt, Fault as u, HopPlace as v, KernelRecord as w, HttpProbe as x, HopSignal as y, ReplicaSetStage as z };
|
|
1166
|
-
//# sourceMappingURL=cell-
|
|
1173
|
+
//# sourceMappingURL=cell-BccWKiXh.d.ts.map
|
package/build/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as StageBand, A as PinnedImage, B as Replies, C as Jitter, D as MiddleboxExpectation, E as MiddleboxEvent, F as Reconnects, G as RunGroup, H as RouterHop, I as Recovers, J as SentinelEvent, K as RunPart, L as Reference, M as ProbeEvent, N as ProxyHop, O as MiddleboxForm, P as ProxyTime, Q as Stage, R as ReplicaSet, S as ImageSource, T as LineMatch, U as RttRange, V as RequestPrediction, W as Run, X as Shipped, Y as SentinelStage, Z as ShippedImage, _ as HopEvent, a as Cluster, at as isHopPlace, b as HopWrite, c as DriverBand, d as FaultPlace, et as Subject, f as Group, g as Hop, h as GroupRole, i as ClientImage, it as WsProbe, j as Prediction, k as Observer, l as DriverStep, m as GroupPrediction, n as Cell, nt as Topology, o as ClusterClientAt, ot as latestEndOf, p as GroupPlace, q as RunTopology, r as Client, rt as Toxic, s as ClusterStage, t as BuildImage, tt as Target, u as Fault, v as HopPlace, w as KernelRecord, x as HttpProbe, y as HopSignal, z as ReplicaSetStage } from "./cell-
|
|
1
|
+
import { $ as StageBand, A as PinnedImage, B as Replies, C as Jitter, D as MiddleboxExpectation, E as MiddleboxEvent, F as Reconnects, G as RunGroup, H as RouterHop, I as Recovers, J as SentinelEvent, K as RunPart, L as Reference, M as ProbeEvent, N as ProxyHop, O as MiddleboxForm, P as ProxyTime, Q as Stage, R as ReplicaSet, S as ImageSource, T as LineMatch, U as RttRange, V as RequestPrediction, W as Run, X as Shipped, Y as SentinelStage, Z as ShippedImage, _ as HopEvent, a as Cluster, at as isHopPlace, b as HopWrite, c as DriverBand, d as FaultPlace, et as Subject, f as Group, g as Hop, h as GroupRole, i as ClientImage, it as WsProbe, j as Prediction, k as Observer, l as DriverStep, m as GroupPrediction, n as Cell, nt as Topology, o as ClusterClientAt, ot as latestEndOf, p as GroupPlace, q as RunTopology, r as Client, rt as Toxic, s as ClusterStage, t as BuildImage, tt as Target, u as Fault, v as HopPlace, w as KernelRecord, x as HttpProbe, y as HopSignal, z as ReplicaSetStage } from "./cell-BccWKiXh.js";
|
|
2
2
|
import { Quantity } from "@mark1russell7/quantity";
|
|
3
3
|
import "testcontainers";
|
|
4
4
|
//#region src/application.d.ts
|
|
@@ -530,14 +530,18 @@ interface GroupVerdict {
|
|
|
530
530
|
}>;
|
|
531
531
|
readonly lags?: Spread | null;
|
|
532
532
|
}
|
|
533
|
-
/**
|
|
533
|
+
/**
|
|
534
|
+
* This function gives the value at quantile p of sorted values, by nearest rank: x_(⌈p·n⌉). It is
|
|
535
|
+
* `quantileSorted` of @mark1russell7/rigor-statistics with the method `"nearest-rank"`.
|
|
536
|
+
*/
|
|
534
537
|
export declare const quantileOf: (sorted: readonly number[], p: number) => number;
|
|
535
538
|
/** This function gives the spread of a set of times, or null for none. */
|
|
536
539
|
export declare const spreadOf: (times: readonly Quantity[]) => Spread | null;
|
|
537
540
|
/**
|
|
538
541
|
* This function gives the distribution-free interval of a median: [x_(k), x_(n+1−k)], for the
|
|
539
542
|
* largest k whose two tails each hold 2.5 % or less. For eleven runs, it gives [x_(2), x_(10)],
|
|
540
|
-
* at 98.8 %.
|
|
543
|
+
* at 98.8 %. It is `medianInterval` of @mark1russell7/rigor-statistics at the level 0.95. An
|
|
544
|
+
* empty list throws a RangeError.
|
|
541
545
|
*/
|
|
542
546
|
export declare const medianInterval: (sorted: readonly number[]) => [number, number];
|
|
543
547
|
/**
|
package/build/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { C as targetImageOf, E as unpinnedBases, S as subjectProblems, T as unlockedManifests, _ as pgClientImage, a as SHIPPED_NAMES, b as routerImage, c as grpcClientImage, d as imageOf, f as imageProblems, g as nodeHttpImage, h as nodeClientImage, i as SHIPPED_CLIENTS, l as harnessImage, m as mongoClientImage, n as PINNED, o as amqpClientImage, p as inContext, r as REPOSITORY, s as goHttpImage, t as CLIENT_NAMES, u as hikariClientImage, v as planOf, w as targetSourceOf, x as subjectClient, y as programOf } from "./image-Lg8LL6C3.js";
|
|
2
2
|
import { quantity, valueIn } from "@mark1russell7/quantity";
|
|
3
|
+
import { median, medianInterval as medianInterval$1, quantileSorted } from "@mark1russell7/rigor-statistics";
|
|
3
4
|
import { createHash } from "node:crypto";
|
|
4
5
|
//#region src/cell.ts
|
|
5
6
|
/** This function tells if a fault place is a hop of a topology. */
|
|
@@ -86,15 +87,18 @@ const STEPS = [
|
|
|
86
87
|
];
|
|
87
88
|
const s = (value) => quantity(Math.round(value * 1e6) / 1e6, "s");
|
|
88
89
|
const name = (run) => `env ${run.environment} run ${run.index}`;
|
|
89
|
-
/**
|
|
90
|
-
|
|
90
|
+
/**
|
|
91
|
+
* This function gives the value at quantile p of sorted values, by nearest rank: x_(⌈p·n⌉). It is
|
|
92
|
+
* `quantileSorted` of @mark1russell7/rigor-statistics with the method `"nearest-rank"`.
|
|
93
|
+
*/
|
|
94
|
+
const quantileOf = (sorted, p) => quantileSorted(sorted, p, "nearest-rank");
|
|
91
95
|
/** This function gives the spread of a set of times, or null for none. */
|
|
92
96
|
const spreadOf = (times) => {
|
|
93
97
|
if (times.length === 0) return null;
|
|
94
98
|
const sorted = times.map((time) => valueIn(time, "s")).sort((a, b) => a - b);
|
|
95
99
|
return {
|
|
96
100
|
count: sorted.length,
|
|
97
|
-
median: s(
|
|
101
|
+
median: s(median(sorted)),
|
|
98
102
|
p99: s(quantileOf(sorted, .99)),
|
|
99
103
|
p999: s(quantileOf(sorted, .999)),
|
|
100
104
|
max: s(sorted.at(-1))
|
|
@@ -135,30 +139,15 @@ const spreadWords = (spread) => {
|
|
|
135
139
|
const ms = (time) => Math.round(valueIn(time, "s") * 1e6) / 1e3;
|
|
136
140
|
return `median ${ms(spread.median)} ms, p99 ${ms(spread.p99)} ms, p99.9 ${ms(spread.p999)} ms, max ${ms(spread.max)} ms of ${spread.count}`;
|
|
137
141
|
};
|
|
138
|
-
/** This function gives P(X ≤ j) for X ~ Binomial(n, 1/2), exactly. */
|
|
139
|
-
const below = (n, j) => {
|
|
140
|
-
let term = 1;
|
|
141
|
-
let sum = 0;
|
|
142
|
-
for (let i = 0; i <= j; i += 1) {
|
|
143
|
-
sum += term;
|
|
144
|
-
term = term * (n - i) / (i + 1);
|
|
145
|
-
}
|
|
146
|
-
return sum / 2 ** n;
|
|
147
|
-
};
|
|
148
142
|
/**
|
|
149
143
|
* This function gives the distribution-free interval of a median: [x_(k), x_(n+1−k)], for the
|
|
150
144
|
* largest k whose two tails each hold 2.5 % or less. For eleven runs, it gives [x_(2), x_(10)],
|
|
151
|
-
* at 98.8 %.
|
|
145
|
+
* at 98.8 %. It is `medianInterval` of @mark1russell7/rigor-statistics at the level 0.95. An
|
|
146
|
+
* empty list throws a RangeError.
|
|
152
147
|
*/
|
|
153
148
|
const medianInterval = (sorted) => {
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
while (k + 1 <= n / 2 && below(n, k) <= .025) k += 1;
|
|
157
|
-
return k === 0 ? [sorted[0], sorted[n - 1]] : [sorted[k - 1], sorted[n - k]];
|
|
158
|
-
};
|
|
159
|
-
const middle = (sorted) => {
|
|
160
|
-
const half = Math.floor(sorted.length / 2);
|
|
161
|
-
return sorted.length % 2 === 1 ? sorted[half] : (sorted[half - 1] + sorted[half]) / 2;
|
|
149
|
+
const interval = medianInterval$1(sorted, .95);
|
|
150
|
+
return [interval.low, interval.high];
|
|
162
151
|
};
|
|
163
152
|
/**
|
|
164
153
|
* This function gives the verdict of a cell, from two tests:
|
|
@@ -241,7 +230,7 @@ const withRequests = (verdict, counted) => {
|
|
|
241
230
|
};
|
|
242
231
|
};
|
|
243
232
|
/** This function gives the median of some counts, or null for none. */
|
|
244
|
-
const medianCount = (values) => values.length === 0 ? null :
|
|
233
|
+
const medianCount = (values) => values.length === 0 ? null : median([...values].sort((a, b) => a - b));
|
|
245
234
|
/** This function sums what the group of a group cell did in its counted runs, and tests each stage that has a band. */
|
|
246
235
|
const groupVerdictOf = (prediction, counted, delta = NOISE) => {
|
|
247
236
|
const noise = valueIn(delta, "s");
|
|
@@ -274,7 +263,7 @@ const groupVerdictOf = (prediction, counted, delta = NOISE) => {
|
|
|
274
263
|
return {
|
|
275
264
|
stage: band.stage,
|
|
276
265
|
band: interval,
|
|
277
|
-
median: values.length === 0 ? null : s(
|
|
266
|
+
median: values.length === 0 ? null : s(median(values)),
|
|
278
267
|
outside,
|
|
279
268
|
decision: outside.length === 0 && values.length > 0 ? "holds" : "fails"
|
|
280
269
|
};
|
|
@@ -286,7 +275,7 @@ const groupVerdictOf = (prediction, counted, delta = NOISE) => {
|
|
|
286
275
|
const at = run.group?.stages[stage];
|
|
287
276
|
return at === void 0 ? [] : [valueIn(at, "s")];
|
|
288
277
|
});
|
|
289
|
-
medians[stage] = s(
|
|
278
|
+
medians[stage] = s(median(values.sort((a, b) => a - b)));
|
|
290
279
|
}
|
|
291
280
|
const lostCounts = counted.flatMap((run) => run.group?.writes.lost === null || run.group === void 0 ? [] : [run.group.writes.lost.length]);
|
|
292
281
|
const oldCounts = counted.flatMap((run) => run.group === void 0 ? [] : [run.group.writes.old]);
|
|
@@ -314,7 +303,7 @@ const groupVerdictOf = (prediction, counted, delta = NOISE) => {
|
|
|
314
303
|
errors: [...errors].map(([error, firsts]) => ({
|
|
315
304
|
name: error,
|
|
316
305
|
runs: firsts.length,
|
|
317
|
-
first: s(
|
|
306
|
+
first: s(median(firsts.sort((a, b) => a - b)))
|
|
318
307
|
})),
|
|
319
308
|
...lags.length === 0 ? {} : { lags: spreadOf(lags) }
|
|
320
309
|
};
|
|
@@ -368,7 +357,19 @@ const verdictOfBand = (prediction, runs, delta = NOISE) => {
|
|
|
368
357
|
kinds.set(kind, (kinds.get(kind) ?? 0) + 1);
|
|
369
358
|
}
|
|
370
359
|
const outcomes = [...kinds].map(([kind, n]) => `${n} ${kind}`).join(", ");
|
|
371
|
-
const timed = times.length === 0 ? "no time" : `median ${s(
|
|
360
|
+
const timed = times.length === 0 ? "no time" : `median ${s(median(times)).value} s from ${prediction.from}, range [${s(times[0]).value}, ${s(times.at(-1)).value}] s`;
|
|
361
|
+
const counts = prediction.group?.lost;
|
|
362
|
+
if (counts !== void 0) {
|
|
363
|
+
const off = new Set(outside.map((each) => each.run)).size;
|
|
364
|
+
return {
|
|
365
|
+
...empty,
|
|
366
|
+
decision: off === 0 ? "holds" : "fails",
|
|
367
|
+
band: null,
|
|
368
|
+
outside,
|
|
369
|
+
median: times.length === 0 ? null : s(median(times)),
|
|
370
|
+
why: `${counted.length - off} of ${counted.length} runs inside the band of lost writes [${counts.low}, ${counts.high}]; ${outcomes}; ${timed}. ${prediction.why}`
|
|
371
|
+
};
|
|
372
|
+
}
|
|
372
373
|
const missed = outside.filter((each) => counted.some((run) => name(run) === each.run && run.reconnect !== void 0));
|
|
373
374
|
const late = missed.length === 0 ? "" : ` ${missed.length} runs had a wait of the reconnect outside its band.`;
|
|
374
375
|
return {
|
|
@@ -376,7 +377,7 @@ const verdictOfBand = (prediction, runs, delta = NOISE) => {
|
|
|
376
377
|
decision: "measured",
|
|
377
378
|
band: null,
|
|
378
379
|
outside: missed,
|
|
379
|
-
median: times.length === 0 ? null : s(
|
|
380
|
+
median: times.length === 0 ? null : s(median(times)),
|
|
380
381
|
why: `${counted.length} runs measured: ${outcomes}; ${timed}.${late} ${prediction.why}`
|
|
381
382
|
};
|
|
382
383
|
}
|
|
@@ -393,13 +394,13 @@ const verdictOfBand = (prediction, runs, delta = NOISE) => {
|
|
|
393
394
|
reordering.fast,
|
|
394
395
|
reordering.seen
|
|
395
396
|
].some((count) => (count ?? 0) > 0) ? ` Reordering: ${reorderingWords(reordering)}.` : "";
|
|
396
|
-
const why = samples.length === 0 ? `${counted.length} runs measured, with no reply: ${prediction.why}` : `${counted.length} runs, ${samples.length} replies: median ${s(
|
|
397
|
+
const why = samples.length === 0 ? `${counted.length} runs measured, with no reply: ${prediction.why}` : `${counted.length} runs, ${samples.length} replies: median ${s(median(samples)).value} s, p99 ${s(quantileOf(samples, .99)).value} s, slowest ${s(samples.at(-1)).value} s, ${retransmissions} retransmissions, ${ended.length} connections ended.${reordered} ${prediction.why}`;
|
|
397
398
|
return {
|
|
398
399
|
...empty,
|
|
399
400
|
decision: "measured",
|
|
400
401
|
band: null,
|
|
401
402
|
outside: [],
|
|
402
|
-
median: samples.length === 0 ? null : s(
|
|
403
|
+
median: samples.length === 0 ? null : s(median(samples)),
|
|
403
404
|
...reordering === void 0 ? {} : { reordering },
|
|
404
405
|
why
|
|
405
406
|
};
|
|
@@ -442,7 +443,7 @@ const verdictOfBand = (prediction, runs, delta = NOISE) => {
|
|
|
442
443
|
why: "no run measured its time"
|
|
443
444
|
};
|
|
444
445
|
const [from, to] = medianInterval(values);
|
|
445
|
-
const median = s(
|
|
446
|
+
const median$1 = s(median(values));
|
|
446
447
|
const interval = {
|
|
447
448
|
low: s(from),
|
|
448
449
|
high: s(to)
|
|
@@ -455,13 +456,13 @@ const verdictOfBand = (prediction, runs, delta = NOISE) => {
|
|
|
455
456
|
const disjoint = to < wide.low || from > wide.high;
|
|
456
457
|
const off = new Set(outside.map((each) => each.run)).size;
|
|
457
458
|
const counts = `${counted.length - off} of ${counted.length} runs inside [${band.low.value}, ${band.high.value}] s`;
|
|
458
|
-
const centre = `median ${median.value} s, its interval [${interval.low.value}, ${interval.high.value}] s`;
|
|
459
|
+
const centre = `median ${median$1.value} s, its interval [${interval.low.value}, ${interval.high.value}] s`;
|
|
459
460
|
const result = {
|
|
460
461
|
counted: counted.length,
|
|
461
462
|
voided,
|
|
462
463
|
band,
|
|
463
464
|
outside,
|
|
464
|
-
median,
|
|
465
|
+
median: median$1,
|
|
465
466
|
medianInterval: interval
|
|
466
467
|
};
|
|
467
468
|
if (outside.length > 0 || disjoint) return {
|
|
@@ -1531,6 +1532,12 @@ const measureGroup = (cell, run) => {
|
|
|
1531
1532
|
if (met === void 0) findings.push(`the client never met ${error.name}`);
|
|
1532
1533
|
else if (met.first.value < valueIn(error.low, "s") - noise || met.first.value > valueIn(error.high, "s") + noise) findings.push(`the client met ${error.name} ${met.first.value} s after the fault, outside [${valueIn(error.low, "s")}, ${valueIn(error.high, "s")}] s`);
|
|
1533
1534
|
}
|
|
1535
|
+
if (expected?.lost !== void 0) {
|
|
1536
|
+
const { low, high } = expected.lost;
|
|
1537
|
+
const lost = group.writes.lost;
|
|
1538
|
+
if (lost === null) findings.push("the group did not converge, thus the count of its lost writes is not known");
|
|
1539
|
+
else if (lost.length < low || lost.length > high) findings.push(`the group lost ${lost.length} accepted writes, outside [${low}, ${high}]`);
|
|
1540
|
+
}
|
|
1534
1541
|
if (accepted === void 0) {
|
|
1535
1542
|
if (prediction.detects !== null) findings.push(lands === "new" ? "the client never wrote to a new primary in the window" : "the client never wrote again in the window");
|
|
1536
1543
|
return {
|