@mark1russell7/harness 0.3.0 → 0.4.0
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 +26 -10
- package/build/index.d.ts +139 -10
- package/build/index.js +252 -26
- package/build/index.js.map +1 -1
- package/image/native/src/bin/client.rs +63 -3
- package/image/native/src/bin/inject.rs +48 -6
- package/package.json +2 -2
package/build/index.js
CHANGED
|
@@ -24,6 +24,55 @@ const STEPS = [
|
|
|
24
24
|
];
|
|
25
25
|
const s = (value) => quantity(Math.round(value * 1e6) / 1e6, "s");
|
|
26
26
|
const name = (run) => `env ${run.environment} run ${run.index}`;
|
|
27
|
+
/** This function gives the value at quantile p of sorted values, by nearest rank: x_(⌈p·n⌉). */
|
|
28
|
+
const quantileOf = (sorted, p) => sorted[Math.min(sorted.length, Math.max(1, Math.ceil(p * sorted.length))) - 1];
|
|
29
|
+
/** This function gives the spread of a set of times, or null for none. */
|
|
30
|
+
const spreadOf = (times) => {
|
|
31
|
+
if (times.length === 0) return null;
|
|
32
|
+
const sorted = times.map((time) => valueIn(time, "s")).sort((a, b) => a - b);
|
|
33
|
+
return {
|
|
34
|
+
count: sorted.length,
|
|
35
|
+
median: s(middle(sorted)),
|
|
36
|
+
p99: s(quantileOf(sorted, .99)),
|
|
37
|
+
p999: s(quantileOf(sorted, .999)),
|
|
38
|
+
max: s(sorted.at(-1))
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
/** This function adds the reordering records of runs: the counts add, and each largest value is the largest of all. */
|
|
42
|
+
const reorderingTotal = (runs) => {
|
|
43
|
+
const each = runs.flatMap((run) => run.reordering === void 0 ? [] : [run.reordering]);
|
|
44
|
+
if (each.length === 0) return void 0;
|
|
45
|
+
const sum = (key) => each.reduce((total, one) => total + one[key], 0);
|
|
46
|
+
const add = (key) => each.some((one) => one[key] !== null) ? each.reduce((total, one) => total + (one[key] ?? 0), 0) : null;
|
|
47
|
+
const top = (key) => each.some((one) => one[key] !== null) ? Math.max(...each.map((one) => one[key] ?? 0)) : null;
|
|
48
|
+
return {
|
|
49
|
+
outOfOrder: sum("outOfOrder"),
|
|
50
|
+
duplicates: sum("duplicates"),
|
|
51
|
+
ooo: add("ooo"),
|
|
52
|
+
sacked: top("sacked"),
|
|
53
|
+
lost: top("lost"),
|
|
54
|
+
dsacks: add("dsacks"),
|
|
55
|
+
fast: sum("fast"),
|
|
56
|
+
degree: top("degree"),
|
|
57
|
+
seen: add("seen")
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
/** This function says a reordering record in words. */
|
|
61
|
+
const reorderingWords = (total) => [
|
|
62
|
+
`${total.outOfOrder} arrivals out of order and ${total.duplicates} duplicate arrivals at the client`,
|
|
63
|
+
...total.ooo === null ? [] : [`its kernel counted ${total.ooo} segments out of order`],
|
|
64
|
+
...total.sacked === null ? [] : [`the server held up to ${total.sacked} segments SACKed`],
|
|
65
|
+
...total.dsacks === null ? [] : [`got ${total.dsacks} DSACKs`],
|
|
66
|
+
`sent ${total.fast} fast retransmissions`,
|
|
67
|
+
...total.seen === null ? [] : [`saw ${total.seen} reorderings`],
|
|
68
|
+
...total.degree === null ? [] : [`and its reordering degree went to ${total.degree}`]
|
|
69
|
+
].join(", ");
|
|
70
|
+
/** This function says a spread in words, in milliseconds. */
|
|
71
|
+
const spreadWords = (spread) => {
|
|
72
|
+
if (spread === null) return "no time";
|
|
73
|
+
const ms = (time) => Math.round(valueIn(time, "s") * 1e6) / 1e3;
|
|
74
|
+
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}`;
|
|
75
|
+
};
|
|
27
76
|
/** This function gives P(X ≤ j) for X ~ Binomial(n, 1/2), exactly. */
|
|
28
77
|
const below = (n, j) => {
|
|
29
78
|
let term = 1;
|
|
@@ -82,6 +131,25 @@ const verdictOf = (prediction, runs) => {
|
|
|
82
131
|
why: "no run counted"
|
|
83
132
|
};
|
|
84
133
|
if (prediction.detects === null) {
|
|
134
|
+
const crossed = counted.flatMap((run) => run.crossings === void 0 ? [] : [run.crossings]);
|
|
135
|
+
if (crossed.length > 0) {
|
|
136
|
+
const hop = crossed[0].hop;
|
|
137
|
+
const up = spreadOf(crossed.flatMap((each) => each.up));
|
|
138
|
+
const down = spreadOf(crossed.flatMap((each) => each.down));
|
|
139
|
+
return {
|
|
140
|
+
...empty,
|
|
141
|
+
decision: "measured",
|
|
142
|
+
band: null,
|
|
143
|
+
outside: [],
|
|
144
|
+
median: up?.median ?? null,
|
|
145
|
+
proxy: {
|
|
146
|
+
hop,
|
|
147
|
+
up,
|
|
148
|
+
down
|
|
149
|
+
},
|
|
150
|
+
why: `${counted.length} runs measured ${hop}: toward the target ${spreadWords(up)}; toward the client ${spreadWords(down)}. ${prediction.why}`
|
|
151
|
+
};
|
|
152
|
+
}
|
|
85
153
|
if (prediction.from !== void 0) {
|
|
86
154
|
const times = counted.flatMap((run) => run.elapsed === null ? [] : [valueIn(run.elapsed, "s")]).sort((a, b) => a - b);
|
|
87
155
|
const kinds = /* @__PURE__ */ new Map();
|
|
@@ -103,13 +171,24 @@ const verdictOf = (prediction, runs) => {
|
|
|
103
171
|
const samples = counted.flatMap((run) => (run.samples ?? []).map((sample) => valueIn(sample, "s"))).sort((a, b) => a - b);
|
|
104
172
|
const retransmissions = counted.reduce((sum, run) => sum + run.retransmissions, 0);
|
|
105
173
|
const ended = counted.filter((run) => run.outcome.kind === "error" || run.outcome.kind === "eof");
|
|
106
|
-
const
|
|
174
|
+
const reordering = reorderingTotal(counted);
|
|
175
|
+
const reordered = reordering !== void 0 && [
|
|
176
|
+
reordering.outOfOrder,
|
|
177
|
+
reordering.duplicates,
|
|
178
|
+
reordering.ooo,
|
|
179
|
+
reordering.sacked,
|
|
180
|
+
reordering.dsacks,
|
|
181
|
+
reordering.fast,
|
|
182
|
+
reordering.seen
|
|
183
|
+
].some((count) => (count ?? 0) > 0) ? ` Reordering: ${reorderingWords(reordering)}.` : "";
|
|
184
|
+
const why = samples.length === 0 ? `${counted.length} runs measured, with no reply: ${prediction.why}` : `${counted.length} runs, ${samples.length} replies: median ${s(middle(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}`;
|
|
107
185
|
return {
|
|
108
186
|
...empty,
|
|
109
187
|
decision: "measured",
|
|
110
188
|
band: null,
|
|
111
189
|
outside: [],
|
|
112
190
|
median: samples.length === 0 ? null : s(middle(samples)),
|
|
191
|
+
...reordering === void 0 ? {} : { reordering },
|
|
113
192
|
why
|
|
114
193
|
};
|
|
115
194
|
}
|
|
@@ -277,6 +356,65 @@ const peerOf = (cell, run) => {
|
|
|
277
356
|
cookie
|
|
278
357
|
};
|
|
279
358
|
};
|
|
359
|
+
/** This function tells if sequence number a is after b, in the 32-bit space of TCP (`after` in include/net/tcp.h). */
|
|
360
|
+
const after32 = (a, b) => (a - b | 0) > 0;
|
|
361
|
+
/** This function gives the largest value of a count of the kernel in records, or null when no record has it. */
|
|
362
|
+
const most = (records, count) => records.reduce((top, record) => {
|
|
363
|
+
const value = count(record);
|
|
364
|
+
return value === void 0 ? top : Math.max(top ?? 0, value);
|
|
365
|
+
}, null);
|
|
366
|
+
/** This function gives what reordering did in a run, from the records of the receiver and of the sender. */
|
|
367
|
+
const reorderingOf = (receiver, sender) => {
|
|
368
|
+
const arrivals = receiver.filter((record) => record.ev === "rx" && (record.seg?.len.value ?? 0) > 0 && record.tcp?.rcv_nxt !== void 0);
|
|
369
|
+
return {
|
|
370
|
+
outOfOrder: arrivals.filter((record) => after32(record.seg.seq, record.tcp.rcv_nxt)).length,
|
|
371
|
+
duplicates: arrivals.filter((record) => !after32(record.seg.seq + record.seg.len.value, record.tcp.rcv_nxt)).length,
|
|
372
|
+
ooo: most(receiver, (record) => record.tcp?.rcv_ooopack),
|
|
373
|
+
sacked: most(sender, (record) => record.tcp?.sacked),
|
|
374
|
+
lost: most(sender, (record) => record.tcp?.lost),
|
|
375
|
+
dsacks: most(sender, (record) => record.tcp?.dsack_dups),
|
|
376
|
+
fast: sender.filter((record) => record.ev === "retrans" && record.why === "fast").length,
|
|
377
|
+
degree: most(sender, (record) => record.tcp?.reordering),
|
|
378
|
+
seen: most(sender, (record) => record.tcp?.reord_seen)
|
|
379
|
+
};
|
|
380
|
+
};
|
|
381
|
+
/**
|
|
382
|
+
* This function gives the times of a proxy in a run, for each request after the first. The time
|
|
383
|
+
* of a request starts at its arrival on the socket of the proxy toward the client. It stops at the
|
|
384
|
+
* next write on the socket toward the target. The time of its reply starts at its arrival on the
|
|
385
|
+
* socket toward the target, and it stops at the next write toward the client. A request without
|
|
386
|
+
* each record gives no time.
|
|
387
|
+
*/
|
|
388
|
+
const crossingsOf = (run, hop, toClient, toTarget) => {
|
|
389
|
+
const up = [];
|
|
390
|
+
const down = [];
|
|
391
|
+
if (toClient === void 0 || toTarget === void 0) return {
|
|
392
|
+
hop,
|
|
393
|
+
up,
|
|
394
|
+
down
|
|
395
|
+
};
|
|
396
|
+
const of = (place) => run.kernel.filter((record) => record.netns === place.netns && record.cookie === place.cookie);
|
|
397
|
+
const near = of(toClient);
|
|
398
|
+
const far = of(toTarget);
|
|
399
|
+
const arrival = (records, from, to) => records.find((record) => record.ev === "rx" && (record.seg?.len.value ?? 0) > 0 && record.mono > from && record.mono < to);
|
|
400
|
+
const write = (records, from, to) => records.find((record) => record.ev === "tx" && record.mono >= from && record.mono < to);
|
|
401
|
+
const sends = run.events.filter((event) => event.ev === "sent").slice(1);
|
|
402
|
+
sends.forEach((send, n) => {
|
|
403
|
+
const next = sends[n + 1]?.t ?? Infinity;
|
|
404
|
+
const request = arrival(near, send.t, next);
|
|
405
|
+
const relayed = request === void 0 ? void 0 : write(far, request.mono, next);
|
|
406
|
+
if (request === void 0 || relayed === void 0) return;
|
|
407
|
+
up.push(between(request.mono, relayed.mono));
|
|
408
|
+
const reply = arrival(far, relayed.mono, next);
|
|
409
|
+
const answered = reply === void 0 ? void 0 : write(near, reply.mono, next);
|
|
410
|
+
if (reply !== void 0 && answered !== void 0) down.push(between(reply.mono, answered.mono));
|
|
411
|
+
});
|
|
412
|
+
return {
|
|
413
|
+
hop,
|
|
414
|
+
up,
|
|
415
|
+
down
|
|
416
|
+
};
|
|
417
|
+
};
|
|
280
418
|
/**
|
|
281
419
|
* This function reads one run against its cell, from the records of the kernel first.
|
|
282
420
|
* - The socket of the run is the socket of its observer. It is the socket that the client connected
|
|
@@ -321,12 +459,15 @@ const measure = (cell, run) => {
|
|
|
321
459
|
return firings.find((record) => timer === "keepalive" && record.owned === true) === void 0 ? null : "its keepalive timer found the socket held by its owner and put itself off by HZ/20: the run measured the owner, not the timer";
|
|
322
460
|
};
|
|
323
461
|
const acks = request === void 0 ? 0 : socket.filter((record) => record.ev === "rx" && record.mono > request.t).length;
|
|
462
|
+
const srtt = socket.find((record) => record.tcp?.srtt !== void 0)?.tcp?.srtt;
|
|
463
|
+
const rtt = srtt === void 0 ? null : quantity(valueIn(srtt, "us") / 1e3, "ms");
|
|
324
464
|
const base = {
|
|
325
465
|
environment: run.environment,
|
|
326
466
|
index: run.index,
|
|
327
467
|
probes: keepalives.length,
|
|
328
468
|
retransmissions: retransmitted.length,
|
|
329
|
-
acks
|
|
469
|
+
acks,
|
|
470
|
+
rtt
|
|
330
471
|
};
|
|
331
472
|
const nothing = {
|
|
332
473
|
outcome: { kind: "silent" },
|
|
@@ -370,6 +511,8 @@ const measure = (cell, run) => {
|
|
|
370
511
|
else if (replyFirst && replied) voided = "the server's reply arrived before the fault held";
|
|
371
512
|
else if (keepalives[0] !== void 0 && keepalives[0].mono < run.fault.t1) voided = "a keepalive probe went out before the fault held";
|
|
372
513
|
}
|
|
514
|
+
const assumed = cell.prediction.detects === null ? void 0 : cell.prediction.rtt;
|
|
515
|
+
if (voided === null && assumed !== void 0 && rtt !== null && valueIn(rtt, "ms") > valueIn(assumed.high, "ms")) voided = `rtt-over-assumption: its first round trip was ${rtt.value} ms, above the ${valueIn(assumed.high, "ms")} ms that the prediction assumes`;
|
|
373
516
|
const remote = cell.observer !== void 0 && cell.observer !== "client";
|
|
374
517
|
const reported = socket.find((record) => record.ev === "error" && (record.err ?? 0) !== 0);
|
|
375
518
|
const seen = remote ? void 0 : events.find((event) => event.ev === "error" || event.ev === "eof");
|
|
@@ -397,13 +540,29 @@ const measure = (cell, run) => {
|
|
|
397
540
|
};
|
|
398
541
|
if (prediction.detects === null) {
|
|
399
542
|
const sends = events.filter((event) => event.ev === "sent").slice(1);
|
|
543
|
+
const answers = events.some((event) => event.ev === "replied") ? "replied" : "data";
|
|
400
544
|
const samples = sends.flatMap((send, n) => {
|
|
401
545
|
const next = sends[n + 1]?.t ?? Infinity;
|
|
402
|
-
const reply = events.find((event) => event.ev ===
|
|
546
|
+
const reply = events.find((event) => event.ev === answers && event.t > send.t && event.t < next);
|
|
403
547
|
return reply === void 0 ? [] : [between(send.t, reply.t)];
|
|
404
548
|
});
|
|
405
549
|
const from = prediction.from === void 0 ? void 0 : referenceAt(prediction.from);
|
|
406
550
|
const until = ended ?? after.at(-1)?.t;
|
|
551
|
+
const peerRecords = peerCookie === void 0 ? [] : kernel.filter((record) => record.netns === peerNetns && record.cookie === peerCookie);
|
|
552
|
+
const proxy = cell.calibrates === void 0 ? void 0 : run.topology?.hops[cell.calibrates];
|
|
553
|
+
const crossings = cell.calibrates === void 0 ? {} : { crossings: crossingsOf(run, cell.calibrates, proxy === void 0 ? void 0 : socketOf({
|
|
554
|
+
...cell,
|
|
555
|
+
observer: {
|
|
556
|
+
hop: cell.calibrates,
|
|
557
|
+
toward: "client"
|
|
558
|
+
}
|
|
559
|
+
}, run), proxy === void 0 ? void 0 : socketOf({
|
|
560
|
+
...cell,
|
|
561
|
+
observer: {
|
|
562
|
+
hop: cell.calibrates,
|
|
563
|
+
toward: "target"
|
|
564
|
+
}
|
|
565
|
+
}, run)) };
|
|
407
566
|
return {
|
|
408
567
|
...base,
|
|
409
568
|
...nothing,
|
|
@@ -413,6 +572,8 @@ const measure = (cell, run) => {
|
|
|
413
572
|
noticed: prediction.from === void 0 ? null : noticed,
|
|
414
573
|
steps: retransmitted.filter((record) => record.mono > request.t).map((record) => between(request.t, record.mono)),
|
|
415
574
|
samples,
|
|
575
|
+
reordering: reorderingOf(socket.filter((record) => record.mono > request.t), peerRecords.filter((record) => record.mono > request.t)),
|
|
576
|
+
...crossings,
|
|
416
577
|
void: voided
|
|
417
578
|
};
|
|
418
579
|
}
|
|
@@ -794,6 +955,12 @@ const clientOf = async (kind, image) => {
|
|
|
794
955
|
clientKind: kind
|
|
795
956
|
};
|
|
796
957
|
};
|
|
958
|
+
/** This function gives the device of a route from its line in `ip -o route get`: "10.0.0.2 dev eth0 src …". */
|
|
959
|
+
const deviceOf = (route) => {
|
|
960
|
+
const device = /\bdev (\S+)/.exec(route)?.[1];
|
|
961
|
+
if (device === void 0) throw new Error(`no device in the route: ${route}`);
|
|
962
|
+
return device;
|
|
963
|
+
};
|
|
797
964
|
/**
|
|
798
965
|
* This function opens one environment:
|
|
799
966
|
* - a chain of new networks, and the stock target on the last one. A direct pair has one network.
|
|
@@ -871,10 +1038,12 @@ const openLab = async (target, kind = "tcp", topology) => {
|
|
|
871
1038
|
await join(box, targetSide);
|
|
872
1039
|
const ready = await seen.until(["mbx-ready"], 3e4, `router ${hop.name}`);
|
|
873
1040
|
await seen.until(hop.form === "reset-both" ? ["resetter"] : ["ct-watch"], 3e4, `router ${hop.name}`);
|
|
1041
|
+
const injector = await new GenericContainer(image).withNetworkMode(`container:${box.getId()}`).withAddedCapabilities("NET_ADMIN", "NET_RAW").start();
|
|
1042
|
+
opened.push(injector);
|
|
874
1043
|
inChain[at] = {
|
|
875
1044
|
hop,
|
|
876
1045
|
box,
|
|
877
|
-
injector
|
|
1046
|
+
injector,
|
|
878
1047
|
ip: {
|
|
879
1048
|
client: await ipOn(box, clientSide),
|
|
880
1049
|
target: await ipOn(box, targetSide)
|
|
@@ -883,6 +1052,10 @@ const openLab = async (target, kind = "tcp", topology) => {
|
|
|
883
1052
|
client: "",
|
|
884
1053
|
target: endpointOf(at + 1)
|
|
885
1054
|
},
|
|
1055
|
+
devices: {
|
|
1056
|
+
client: "",
|
|
1057
|
+
target: ""
|
|
1058
|
+
},
|
|
886
1059
|
netns: await netnsOf(box),
|
|
887
1060
|
sysctls: { "net.netfilter.nf_conntrack_tcp_timeout_established": String(ready["idle"]) }
|
|
888
1061
|
};
|
|
@@ -913,6 +1086,10 @@ const openLab = async (target, kind = "tcp", topology) => {
|
|
|
913
1086
|
client: "",
|
|
914
1087
|
target: next
|
|
915
1088
|
},
|
|
1089
|
+
devices: {
|
|
1090
|
+
client: "",
|
|
1091
|
+
target: ""
|
|
1092
|
+
},
|
|
916
1093
|
netns: await netnsOf(injector),
|
|
917
1094
|
sysctls: {}
|
|
918
1095
|
};
|
|
@@ -961,7 +1138,21 @@ const openLab = async (target, kind = "tcp", topology) => {
|
|
|
961
1138
|
}
|
|
962
1139
|
for (let at = 0; at < inChain.length; at += 1) {
|
|
963
1140
|
const hop = inChain[at];
|
|
964
|
-
|
|
1141
|
+
const toward = async (address) => deviceOf(await output(hop.injector, [
|
|
1142
|
+
"ip",
|
|
1143
|
+
"-o",
|
|
1144
|
+
"route",
|
|
1145
|
+
"get",
|
|
1146
|
+
address.split(":")[0]
|
|
1147
|
+
]));
|
|
1148
|
+
inChain[at] = {
|
|
1149
|
+
...hop,
|
|
1150
|
+
devices: {
|
|
1151
|
+
client: await toward(hop.peer.client),
|
|
1152
|
+
target: await toward(hop.peer.target)
|
|
1153
|
+
}
|
|
1154
|
+
};
|
|
1155
|
+
if (hop.hop.sysctls === void 0) continue;
|
|
965
1156
|
const where = await adminOf(at + 1);
|
|
966
1157
|
const settings = Object.entries(hop.hop.sysctls).map(([key, value]) => `${key}=${value}`);
|
|
967
1158
|
await output(where, [
|
|
@@ -1010,13 +1201,38 @@ const openLab = async (target, kind = "tcp", topology) => {
|
|
|
1010
1201
|
if (found === void 0) throw new Error(`the topology has no hop ${name}`);
|
|
1011
1202
|
return found;
|
|
1012
1203
|
};
|
|
1013
|
-
/**
|
|
1204
|
+
/**
|
|
1205
|
+
* This function gives the namespace and the peer of the rules of a fault at a place, and the
|
|
1206
|
+
* other arguments of the injector. At a proxy, the rules are INPUT and OUTPUT rules, which act
|
|
1207
|
+
* on its sockets. A router forwards past INPUT and OUTPUT, thus its rules are FORWARD rules on
|
|
1208
|
+
* its device toward the side (`--via`).
|
|
1209
|
+
*/
|
|
1014
1210
|
const placeOf = (at) => {
|
|
1015
|
-
if (at === "client") return [
|
|
1016
|
-
|
|
1211
|
+
if (at === "client") return [
|
|
1212
|
+
admin,
|
|
1213
|
+
targetIp,
|
|
1214
|
+
[]
|
|
1215
|
+
];
|
|
1216
|
+
if (at === "target") return [
|
|
1217
|
+
injector,
|
|
1218
|
+
targetPeer,
|
|
1219
|
+
[]
|
|
1220
|
+
];
|
|
1017
1221
|
const hop = hopNamed(at.hop);
|
|
1018
|
-
|
|
1019
|
-
|
|
1222
|
+
const peer = at.toward === "client" ? hop.peer.client : hop.peer.target.split(":")[0];
|
|
1223
|
+
const via = hop.hop.kind === "router" ? ["--via", hop.devices[at.toward]] : [];
|
|
1224
|
+
return [
|
|
1225
|
+
hop.injector,
|
|
1226
|
+
peer,
|
|
1227
|
+
via
|
|
1228
|
+
];
|
|
1229
|
+
};
|
|
1230
|
+
/** This function gives the namespace and the device of netem at a place: an end, or the device of a hop toward a side. */
|
|
1231
|
+
const deviceAt = (at) => {
|
|
1232
|
+
if (at === "client") return [admin, "eth0"];
|
|
1233
|
+
if (at === "target") return [injector, "eth0"];
|
|
1234
|
+
const hop = hopNamed(at.hop);
|
|
1235
|
+
return [hop.injector, hop.devices[at.toward]];
|
|
1020
1236
|
};
|
|
1021
1237
|
const toxiproxy = () => {
|
|
1022
1238
|
const found = inChain.find((hop) => hop.hop.kind === "proxy" && hop.hop.api !== void 0);
|
|
@@ -1090,42 +1306,46 @@ const openLab = async (target, kind = "tcp", topology) => {
|
|
|
1090
1306
|
return read;
|
|
1091
1307
|
},
|
|
1092
1308
|
async fault(kind, at = "target", options = {}) {
|
|
1093
|
-
const [where, peer] = placeOf(at);
|
|
1309
|
+
const [where, peer, via] = placeOf(at);
|
|
1094
1310
|
return stamped(where, [
|
|
1095
1311
|
"/opt/harness/inject",
|
|
1096
1312
|
kind,
|
|
1097
1313
|
peer,
|
|
1098
|
-
...optionArgs(options)
|
|
1314
|
+
...optionArgs(options),
|
|
1315
|
+
...via
|
|
1099
1316
|
]);
|
|
1100
1317
|
},
|
|
1101
1318
|
async lift(kind, at = "target", options = {}) {
|
|
1102
|
-
const [where, peer] = placeOf(at);
|
|
1319
|
+
const [where, peer, via] = placeOf(at);
|
|
1103
1320
|
await output(where, [
|
|
1104
1321
|
"/opt/harness/inject",
|
|
1105
1322
|
LIFT[kind],
|
|
1106
1323
|
peer,
|
|
1107
|
-
...optionArgs(options)
|
|
1324
|
+
...optionArgs(options),
|
|
1325
|
+
...via
|
|
1108
1326
|
]);
|
|
1109
1327
|
},
|
|
1110
1328
|
async netem(spec, at = "target") {
|
|
1111
|
-
const where = at
|
|
1329
|
+
const [where, device] = deviceAt(at);
|
|
1112
1330
|
const args = spec === null ? [
|
|
1113
1331
|
"/opt/harness/inject",
|
|
1114
1332
|
"clear-netem",
|
|
1115
|
-
|
|
1333
|
+
device
|
|
1116
1334
|
] : [
|
|
1117
1335
|
"/opt/harness/inject",
|
|
1118
1336
|
"netem",
|
|
1119
|
-
|
|
1337
|
+
device,
|
|
1120
1338
|
...spec.split(/\s+/)
|
|
1121
1339
|
];
|
|
1122
1340
|
return stamped(where, args);
|
|
1123
1341
|
},
|
|
1124
1342
|
async offload(on, at = "target") {
|
|
1125
|
-
|
|
1343
|
+
const devices = typeof at === "object" ? Object.values(hopNamed(at.hop).devices) : [deviceAt(at)[1]];
|
|
1344
|
+
const [where] = deviceAt(at);
|
|
1345
|
+
for (const device of devices) await output(where, [
|
|
1126
1346
|
"/opt/harness/inject",
|
|
1127
1347
|
"offload",
|
|
1128
|
-
|
|
1348
|
+
device,
|
|
1129
1349
|
on ? "on" : "off"
|
|
1130
1350
|
]);
|
|
1131
1351
|
},
|
|
@@ -1199,13 +1419,14 @@ const openLab = async (target, kind = "tcp", topology) => {
|
|
|
1199
1419
|
for (const argv of commands) await output(server, [...argv]);
|
|
1200
1420
|
},
|
|
1201
1421
|
async heal(at, when) {
|
|
1202
|
-
const [where, peer] = placeOf(at);
|
|
1422
|
+
const [where, peer, via] = placeOf(at);
|
|
1203
1423
|
return stamped(where, [
|
|
1204
1424
|
"/opt/harness/inject",
|
|
1205
1425
|
"clear",
|
|
1206
1426
|
peer,
|
|
1207
1427
|
"--at",
|
|
1208
|
-
String(Math.round(when))
|
|
1428
|
+
String(Math.round(when)),
|
|
1429
|
+
...via
|
|
1209
1430
|
]);
|
|
1210
1431
|
},
|
|
1211
1432
|
async forget() {
|
|
@@ -1469,6 +1690,8 @@ const once = async (lab, cell, environment, index, dry = false) => {
|
|
|
1469
1690
|
};
|
|
1470
1691
|
const toxicFirst = cell.toxic !== void 0 && cell.fault !== "toxic";
|
|
1471
1692
|
if (toxicFirst) await lab.toxic(cell.toxic);
|
|
1693
|
+
const netemAt = cell.faultAt ?? "target";
|
|
1694
|
+
const netemOn = (fault) => netemAt === "target" ? lab.netem(netemSpec(cell, fault), "target") : lab.netem(fault ?? null, netemAt);
|
|
1472
1695
|
const apply = async () => {
|
|
1473
1696
|
switch (cell.fault) {
|
|
1474
1697
|
case "freeze-process":
|
|
@@ -1480,10 +1703,11 @@ const once = async (lab, cell, environment, index, dry = false) => {
|
|
|
1480
1703
|
case "unreachable": return lab.fault("reject", faultAt, { code: cell.icmp ?? "host" });
|
|
1481
1704
|
case "simplex": return lab.fault("blackhole", faultAt, options);
|
|
1482
1705
|
case "mtu-blackhole": return lab.fault("mtu", "client", { above: cell.mtu ?? 1200 });
|
|
1483
|
-
case "netem": return
|
|
1706
|
+
case "netem": return netemOn(cell.netem);
|
|
1484
1707
|
case "rst": return lab.fault("rst", faultAt);
|
|
1485
1708
|
case "middlebox-forget":
|
|
1486
|
-
case "proxy-idle":
|
|
1709
|
+
case "proxy-idle":
|
|
1710
|
+
case "none": return null;
|
|
1487
1711
|
case "toxic":
|
|
1488
1712
|
if (cell.toxic === void 0) throw new Error(`${cell.name} names no toxic`);
|
|
1489
1713
|
return lab.toxic(cell.toxic);
|
|
@@ -1499,12 +1723,13 @@ const once = async (lab, cell, environment, index, dry = false) => {
|
|
|
1499
1723
|
case "fin":
|
|
1500
1724
|
case "middlebox-forget":
|
|
1501
1725
|
case "proxy-idle":
|
|
1502
|
-
case "server-close":
|
|
1726
|
+
case "server-close":
|
|
1727
|
+
case "none": return;
|
|
1503
1728
|
case "unreachable": return lab.lift("reject", faultAt, { code: cell.icmp ?? "host" });
|
|
1504
1729
|
case "simplex": return lab.lift("blackhole", faultAt, options);
|
|
1505
1730
|
case "mtu-blackhole": return lab.lift("mtu", "client", { above: cell.mtu ?? 1200 });
|
|
1506
1731
|
case "netem":
|
|
1507
|
-
await
|
|
1732
|
+
await netemOn(void 0);
|
|
1508
1733
|
return;
|
|
1509
1734
|
case "rst": return lab.lift("rst", faultAt);
|
|
1510
1735
|
case "toxic":
|
|
@@ -1591,6 +1816,7 @@ const inEnvironment = async (cell, environment, count, say) => {
|
|
|
1591
1816
|
if (cell.fault === "mtu-blackhole" || cell.fault === "netem") {
|
|
1592
1817
|
await lab.offload(false, "target");
|
|
1593
1818
|
await lab.offload(false, "client");
|
|
1819
|
+
if (typeof cell.faultAt === "object") await lab.offload(false, cell.faultAt);
|
|
1594
1820
|
}
|
|
1595
1821
|
const latency = netemSpec(cell, void 0);
|
|
1596
1822
|
if (latency !== null) await lab.netem(latency, "target");
|
|
@@ -1665,6 +1891,6 @@ const runCell = async (cell, say = () => {}) => {
|
|
|
1665
1891
|
}
|
|
1666
1892
|
};
|
|
1667
1893
|
//#endregion
|
|
1668
|
-
export { LISTING, NOISE, PUMP, RELATIVE, STEPS, endpointAfter, harnessImage, judge, measure, medianInterval, nodeClientImage, openLab, routerImage, routesOf, runCell, socketOf, spread, templated, toxicBody, verdictOf };
|
|
1894
|
+
export { LISTING, NOISE, PUMP, RELATIVE, STEPS, crossingsOf, deviceOf, endpointAfter, harnessImage, judge, measure, medianInterval, nodeClientImage, openLab, quantileOf, reorderingOf, routerImage, routesOf, runCell, socketOf, spread, spreadOf, templated, toxicBody, verdictOf };
|
|
1669
1895
|
|
|
1670
1896
|
//# sourceMappingURL=index.js.map
|