@mark1russell7/harness 0.11.14 → 0.11.15
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 +4 -0
- package/build/{cell-COlAmUZ-.d.ts → cell-DOoU1LRf.d.ts} +32 -3
- package/build/index.d.ts +12 -2
- package/build/index.js +49 -3
- package/build/index.js.map +1 -1
- package/build/schema.d.ts +1 -1
- package/image/native/src/bin/inject.rs +123 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -213,6 +213,10 @@ ioredis sets the keepalive of its socket itself, after it connects. Thus a cell
|
|
|
213
213
|
- **A toxic** (`toxic`, `Cell.toxic`) is one toxic of Toxiproxy, added through its HTTP API by `inject http`, with monotonic stamps around the request. Its name is `fault`, and the harness removes it after the run. A cell whose fault is another one adds its toxic first: a slow close, then the close of the server.
|
|
214
214
|
- **A close of the server** (`server-close`) makes the server close the connection of each client, with `Target.close`: `CLIENT KILL` for Redis.
|
|
215
215
|
- **A stall of the application** (`app-stall`) injects nothing. The server process holds the reply, because the request asks for it (`HttpProbe.hold`), and its kernel stays up.
|
|
216
|
+
- **An event at a proxy** (`hop-event`, `Cell.event`) is a signal to the program of the proxy, or a query on its console. Examples are a reload of the configuration and PAUSE on the console of PgBouncer.
|
|
217
|
+
- A signal goes to each process of `ProxyHop.program`. A sidecar in the network and process namespaces of the proxy sends it (`inject signal NAME HUP`). The lab starts that sidecar at the first signal.
|
|
218
|
+
- A query goes from the injector of the proxy to its console on 127.0.0.1, as a user that the console trusts (`inject query`). The injector speaks the simple query of the PostgreSQL protocol. Its stamps bracket the query and its ReadyForQuery. After the run, the harness sends the `undo` of the query, for example RESUME after PAUSE.
|
|
219
|
+
- Without `inFlight`, the event comes first, and the request meets it. With `inFlight`, the request goes first, and the event comes while it is in flight. A run whose order is not that order is void.
|
|
216
220
|
- **A fault on a group** acts on the part that `Cell.faultAt` names by its role:
|
|
217
221
|
- A black hole (`blackhole`) drops each segment between the part and each peer. A reset (`rst`) answers each segment from a peer with a reset, thus the part refuses each connection.
|
|
218
222
|
- A partial partition (`partition`) is a black hole between the part and some of its peers (`GroupPlace.cuts`). `group` cuts the part from the other members and the Sentinels, and `clients` cuts it from the client.
|
|
@@ -30,6 +30,9 @@ import { KernelRecord } from "@mark1russell7/prober";
|
|
|
30
30
|
* target (`Target.close`): `CLIENT KILL` in Redis.
|
|
31
31
|
* - `none` injects nothing. A calibration cell measures the path itself: for example the time of a
|
|
32
32
|
* proxy between its two sockets (`Cell.calibrates`).
|
|
33
|
+
* - `hop-event` is an event at a proxy of the topology (`Cell.event`): a signal to its program, or a
|
|
34
|
+
* query on its console. Examples are a reload of the configuration (SIGHUP) and PAUSE on the
|
|
35
|
+
* console of PgBouncer.
|
|
33
36
|
*
|
|
34
37
|
* A fault at a router of the topology (`Cell.faultAt`) acts on what the router forwards toward
|
|
35
38
|
* one side. It is a black hole, a reset or an ICMP unreachable. It can also be a simplex drop, an
|
|
@@ -51,7 +54,7 @@ import { KernelRecord } from "@mark1russell7/prober";
|
|
|
51
54
|
* This fault is of a replica set (`Cell.replicaSet`):
|
|
52
55
|
* - `step-down` sends `replSetStepDown` to the primary, as a client sends a command.
|
|
53
56
|
*/
|
|
54
|
-
type Fault = "blackhole" | "freeze-process" | "rst" | "stall" | "fin" | "unreachable" | "simplex" | "mtu-blackhole" | "netem" | "middlebox-forget" | "proxy-idle" | "toxic" | "server-close" | "partition" | "planned-failover" | "app-stall" | "step-down" | "none";
|
|
57
|
+
type Fault = "blackhole" | "freeze-process" | "rst" | "stall" | "fin" | "unreachable" | "simplex" | "mtu-blackhole" | "netem" | "middlebox-forget" | "proxy-idle" | "toxic" | "server-close" | "partition" | "planned-failover" | "app-stall" | "step-down" | "hop-event" | "none";
|
|
55
58
|
/**
|
|
56
59
|
* A middlebox form tells what a router does with a flow that it forgot, when a late packet comes:
|
|
57
60
|
* - `reset-both` sends a reset to the two ends when it forgets the flow.
|
|
@@ -115,7 +118,31 @@ interface ProxyHop {
|
|
|
115
118
|
readonly sysctls?: Readonly<Record<string, string>>;
|
|
116
119
|
/** These are the settings of the proxy, for the manifest: for example its idle timeouts. */
|
|
117
120
|
readonly settings?: Readonly<Record<string, string>>;
|
|
121
|
+
/** This name is the process of the proxy, as /proc/PID/comm holds it, for a signal of a `hop-event`. */
|
|
122
|
+
readonly program?: string;
|
|
118
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* An event at a proxy hop (`hop-event`). A signal goes to each process of the program of the hop
|
|
126
|
+
* (`ProxyHop.program`). A query goes to the console of the hop from its own namespace, as a user
|
|
127
|
+
* that the console trusts. Its `undo` lifts it after the run. Without `inFlight`, the event
|
|
128
|
+
* comes first, and the request of the client meets it. With `inFlight`, the request goes first,
|
|
129
|
+
* and the event comes while it is in flight.
|
|
130
|
+
*/
|
|
131
|
+
type HopEvent = {
|
|
132
|
+
readonly hop: string;
|
|
133
|
+
readonly signal: "HUP";
|
|
134
|
+
readonly inFlight?: true;
|
|
135
|
+
} | {
|
|
136
|
+
readonly hop: string;
|
|
137
|
+
readonly console: {
|
|
138
|
+
readonly port: number;
|
|
139
|
+
readonly user: string;
|
|
140
|
+
readonly database: string;
|
|
141
|
+
readonly query: string;
|
|
142
|
+
readonly undo?: string;
|
|
143
|
+
};
|
|
144
|
+
readonly inFlight?: true;
|
|
145
|
+
};
|
|
119
146
|
/**
|
|
120
147
|
* A hop is one box between the client and the target. Each hop is on two networks: the network
|
|
121
148
|
* of the side of the client, and the network of the side of the target.
|
|
@@ -828,6 +855,8 @@ interface Cell {
|
|
|
828
855
|
readonly idleBefore?: Quantity;
|
|
829
856
|
/** This toxic is the toxic of a `toxic` fault, which the Toxiproxy hop gets. */
|
|
830
857
|
readonly toxic?: Toxic;
|
|
858
|
+
/** This event is the event of a `hop-event` fault, at a proxy of the topology. */
|
|
859
|
+
readonly event?: HopEvent;
|
|
831
860
|
/** This image, or this client of the harness, replaces the TCP client of the harness for each run. An HTTP client is one. */
|
|
832
861
|
readonly client?: ClientImage | Client;
|
|
833
862
|
/**
|
|
@@ -1022,5 +1051,5 @@ interface RunTopology {
|
|
|
1022
1051
|
}>>;
|
|
1023
1052
|
}
|
|
1024
1053
|
//#endregion
|
|
1025
|
-
export {
|
|
1026
|
-
//# sourceMappingURL=cell-
|
|
1054
|
+
export { WsProbe as $, Reconnects as A, RunGroup as B, MiddleboxForm as C, ProbeEvent as D, Prediction as E, Replies as F, Shipped as G, RunTopology as H, RequestPrediction as I, StageBand as J, ShippedImage as K, RouterHop as L, Reference as M, ReplicaSet as N, ProxyHop as O, ReplicaSetStage as P, Toxic as Q, RttRange as R, MiddleboxExpectation as S, PinnedImage as T, SentinelEvent as U, RunPart as V, SentinelStage as W, Target as X, Subject as Y, Topology as Z, HttpProbe as _, DriverBand as a, LineMatch as b, FaultPlace as c, GroupPrediction as d, isHopPlace as et, GroupRole as f, HopPlace as g, HopEvent as h, ClientImage as i, Recovers as j, ProxyTime as k, Group as l, Hop as m, Cell as n, DriverStep as o, HarnessImage as p, Stage as q, Client as r, Fault as s, BuildImage as t, latestEndOf as tt, GroupPlace as u, ImageSource as v, Observer as w, MiddleboxEvent as x, KernelRecord as y, Run as z };
|
|
1055
|
+
//# sourceMappingURL=cell-DOoU1LRf.d.ts.map
|
package/build/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as WsProbe, A as Reconnects, B as RunGroup, C as MiddleboxForm, D as ProbeEvent, E as Prediction, F as Replies, G as Shipped, H as RunTopology, I as RequestPrediction, J as StageBand, K as ShippedImage, L as RouterHop, M as Reference, N as ReplicaSet, O as ProxyHop, P as ReplicaSetStage, Q as Toxic, R as RttRange, S as MiddleboxExpectation, T as PinnedImage, U as SentinelEvent, V as RunPart, W as SentinelStage, X as Target, Y as Subject, Z as Topology, _ as HttpProbe, a as DriverBand, b as LineMatch, c as FaultPlace, d as GroupPrediction, et as isHopPlace, f as GroupRole, g as HopPlace, h as HopEvent, i as ClientImage, j as Recovers, k as ProxyTime, l as Group, m as Hop, n as Cell, o as DriverStep, p as HarnessImage, q as Stage, r as Client, s as Fault, t as BuildImage, tt as latestEndOf, u as GroupPlace, v as ImageSource, w as Observer, x as MiddleboxEvent, y as KernelRecord, z as Run } from "./cell-DOoU1LRf.js";
|
|
2
2
|
import { Quantity } from "@mark1russell7/quantity";
|
|
3
3
|
import "testcontainers";
|
|
4
4
|
//#region src/application.d.ts
|
|
@@ -911,6 +911,16 @@ interface Lab {
|
|
|
911
911
|
t0: number;
|
|
912
912
|
t1: number;
|
|
913
913
|
}>;
|
|
914
|
+
/**
|
|
915
|
+
* This method applies an event at a proxy hop (`HopEvent`), or its undo. A signal goes from a
|
|
916
|
+
* sidecar in the network and process namespaces of the hop. The lab starts that sidecar at the
|
|
917
|
+
* first signal of the hop. A query goes from the injector of the hop to its console on 127.0.0.1. The
|
|
918
|
+
* injector stamps each one. An event with no undo gives null for its undo.
|
|
919
|
+
*/
|
|
920
|
+
event(event: HopEvent, undo?: boolean): Promise<{
|
|
921
|
+
t0: number;
|
|
922
|
+
t1: number;
|
|
923
|
+
} | null>;
|
|
914
924
|
probe(args: readonly string[]): Promise<Probe>;
|
|
915
925
|
/**
|
|
916
926
|
* This method waits for the next clock line of the prober, which gives the count of lost
|
|
@@ -1422,5 +1432,5 @@ export declare const spread: (runs: number, environments: number) => number[];
|
|
|
1422
1432
|
*/
|
|
1423
1433
|
export declare const runCell: (cell: Cell, say?: (line: string) => void) => Promise<CellResult>;
|
|
1424
1434
|
//#endregion
|
|
1425
|
-
export { type Agent, type BuildImage, type CallReading, type Cell, type CellResult, type Client, type ClientImage, type Crossings, type Cut, type Decision, type DriverBand, type DriverSocket, type DriverStep, type DriverStepTime, type ErrorOnTheWay, type Fault, type FaultPlace, type FaultSide, type Group, type GroupClient, type GroupLab, type GroupMeasured, type GroupNetworkFault, type GroupPlace, type GroupPrediction, type GroupReading, type GroupRole, type GroupVerdict, type GroupView, type HarnessImage, type Hop, type HopPlace, type HttpProbe, type ImagePlan, type ImageSource, type Interval, type KernelRecord, type Lab, type LineMatch, type Manifest, type ManifestHop, type Measured, type MemberLine, type MiddleboxEvent, type MiddleboxExpectation, type MiddleboxForm, type NetworkFault, type Observer, type Outcome, type PinnedImage, type Prediction, type Probe, type ProbeEvent, type ProgramEvent, type ProxyHop, type ProxyTime, type ReconnectReading, type ReconnectVerdict, type Reconnects, type Recovers, type Reference, type Reordering, type ReplicaSet, type ReplicaSetLab, type ReplicaSetStage, type Replies, type RequestPrediction, type RequestReading, type RouterHop, type RttRange, type Run, type RunGroup, type RunPart, type RunTopology, type SentinelEvent, type SentinelStage, type Shipped, type ShippedImage, type SocketPlace, type Spread, type Stage, type StageBand, type StageVerdict, type Stamp, type Status, type Subject, type Target, type Topology, type Toxic, type Verdict, type Writes, type WsProbe, isHopPlace, latestEndOf };
|
|
1435
|
+
export { type Agent, type BuildImage, type CallReading, type Cell, type CellResult, type Client, type ClientImage, type Crossings, type Cut, type Decision, type DriverBand, type DriverSocket, type DriverStep, type DriverStepTime, type ErrorOnTheWay, type Fault, type FaultPlace, type FaultSide, type Group, type GroupClient, type GroupLab, type GroupMeasured, type GroupNetworkFault, type GroupPlace, type GroupPrediction, type GroupReading, type GroupRole, type GroupVerdict, type GroupView, type HarnessImage, type Hop, type HopEvent, type HopPlace, type HttpProbe, type ImagePlan, type ImageSource, type Interval, type KernelRecord, type Lab, type LineMatch, type Manifest, type ManifestHop, type Measured, type MemberLine, type MiddleboxEvent, type MiddleboxExpectation, type MiddleboxForm, type NetworkFault, type Observer, type Outcome, type PinnedImage, type Prediction, type Probe, type ProbeEvent, type ProgramEvent, type ProxyHop, type ProxyTime, type ReconnectReading, type ReconnectVerdict, type Reconnects, type Recovers, type Reference, type Reordering, type ReplicaSet, type ReplicaSetLab, type ReplicaSetStage, type Replies, type RequestPrediction, type RequestReading, type RouterHop, type RttRange, type Run, type RunGroup, type RunPart, type RunTopology, type SentinelEvent, type SentinelStage, type Shipped, type ShippedImage, type SocketPlace, type Spread, type Stage, type StageBand, type StageVerdict, type Stamp, type Status, type Subject, type Target, type Topology, type Toxic, type Verdict, type Writes, type WsProbe, isHopPlace, latestEndOf };
|
|
1426
1436
|
//# sourceMappingURL=index.d.ts.map
|
package/build/index.js
CHANGED
|
@@ -5,6 +5,11 @@ import { createHash } from "node:crypto";
|
|
|
5
5
|
/** This function tells if a fault place is a hop of a topology. */
|
|
6
6
|
const isHopPlace = (place) => typeof place === "object" && "hop" in place;
|
|
7
7
|
/**
|
|
8
|
+
* This function tells if the request of a client goes before the fault of its cell. That is so for a
|
|
9
|
+
* hop event that comes while the request is in flight (`HopEvent.inFlight`). Then a request after the fault is void.
|
|
10
|
+
*/
|
|
11
|
+
const requestFirst = (cell) => cell.fault === "hop-event" && cell.event?.inFlight === true;
|
|
12
|
+
/**
|
|
8
13
|
* This function gives the latest end of a band in seconds, from the reply of the client. It is
|
|
9
14
|
* the high bound, after the latest reference of the band (`startsBy`).
|
|
10
15
|
*/
|
|
@@ -898,7 +903,9 @@ const measureDriver = (cell, run) => {
|
|
|
898
903
|
let voided = null;
|
|
899
904
|
if (cell.probe.mode !== "idle" && cell.before !== true) {
|
|
900
905
|
if (request === void 0) voided = "the operation was never sent into the fault";
|
|
901
|
-
else if (
|
|
906
|
+
else if (requestFirst(cell)) {
|
|
907
|
+
if (run.fault !== null && run.fault.t0 < request.t) voided = "the event came before the operation";
|
|
908
|
+
} else if (run.fault !== null && run.fault.t1 > request.t) voided = "the operation went out before the fault held";
|
|
902
909
|
}
|
|
903
910
|
const keepalive = records.find((record) => record.ev === "probe" && record.what === "keepalive");
|
|
904
911
|
if (voided === null && run.fault !== null && keepalive !== void 0 && keepalive.mono < run.fault.t1) voided = "a keepalive probe went out before the fault held";
|
|
@@ -1705,7 +1712,7 @@ const measureSocket = (cell, run) => {
|
|
|
1705
1712
|
};
|
|
1706
1713
|
let voided = null;
|
|
1707
1714
|
if (run.fault !== null) {
|
|
1708
|
-
const replyFirst = server && inflight && cell.fault !== "mtu-blackhole";
|
|
1715
|
+
const replyFirst = server && inflight && cell.fault !== "mtu-blackhole" || requestFirst(cell);
|
|
1709
1716
|
const replied = events.some((event) => event.ev === "data" && event.t > request.t && event.t < run.fault.t1);
|
|
1710
1717
|
if (inflight && !replyFirst && run.fault.t1 > request.t) voided = "the request went out before the fault held";
|
|
1711
1718
|
else if (replyFirst && run.fault.t1 < request.t) voided = "the request went out into the fault, so the server never had it";
|
|
@@ -2429,6 +2436,7 @@ const openLab = async (target, kind = "tcp", topology) => {
|
|
|
2429
2436
|
const paused = runtime.container.getById(server.getId());
|
|
2430
2437
|
let targetAdmin;
|
|
2431
2438
|
let signaller;
|
|
2439
|
+
const hopSignallers = /* @__PURE__ */ new Map();
|
|
2432
2440
|
const hopNamed = (name) => {
|
|
2433
2441
|
const found = inChain.find((hop) => hop.hop.name === name);
|
|
2434
2442
|
if (found === void 0) throw new Error(`the topology has no hop ${name}`);
|
|
@@ -2704,6 +2712,38 @@ const openLab = async (target, kind = "tcp", topology) => {
|
|
|
2704
2712
|
...when
|
|
2705
2713
|
]);
|
|
2706
2714
|
},
|
|
2715
|
+
async event(event, undo = false) {
|
|
2716
|
+
const hop = hopNamed(event.hop);
|
|
2717
|
+
if (hop.hop.kind !== "proxy") throw new Error(`the hop ${event.hop} is a router: an event goes to a proxy`);
|
|
2718
|
+
if ("signal" in event) {
|
|
2719
|
+
if (undo) return null;
|
|
2720
|
+
const program = hop.hop.program;
|
|
2721
|
+
if (program === void 0) throw new Error(`the hop ${event.hop} names no program to signal`);
|
|
2722
|
+
let sidecar = hopSignallers.get(event.hop);
|
|
2723
|
+
if (sidecar === void 0) {
|
|
2724
|
+
sidecar = await withPid(new GenericContainer(image).withNetworkMode(`container:${hop.box.getId()}`).withAddedCapabilities("NET_ADMIN", "NET_RAW"), `container:${hop.box.getId()}`).start();
|
|
2725
|
+
hopSignallers.set(event.hop, sidecar);
|
|
2726
|
+
opened.push(sidecar);
|
|
2727
|
+
}
|
|
2728
|
+
return stamped(sidecar, [
|
|
2729
|
+
"/opt/harness/inject",
|
|
2730
|
+
"signal",
|
|
2731
|
+
program,
|
|
2732
|
+
event.signal
|
|
2733
|
+
]);
|
|
2734
|
+
}
|
|
2735
|
+
const query = undo ? event.console.undo : event.console.query;
|
|
2736
|
+
if (query === void 0) return null;
|
|
2737
|
+
const { port, user, database } = event.console;
|
|
2738
|
+
return stamped(hop.injector, [
|
|
2739
|
+
"/opt/harness/inject",
|
|
2740
|
+
"query",
|
|
2741
|
+
`127.0.0.1:${port}`,
|
|
2742
|
+
user,
|
|
2743
|
+
database,
|
|
2744
|
+
query
|
|
2745
|
+
]);
|
|
2746
|
+
},
|
|
2707
2747
|
async probe(args) {
|
|
2708
2748
|
const events = lines();
|
|
2709
2749
|
const container = await new GenericContainer(clientImage).withNetworkMode(`container:${client.getId()}`).withCommand([...command, ...args]).withLogConsumer(events.consume).start();
|
|
@@ -4396,7 +4436,7 @@ const once = async (lab, cell, environment, index, dry = false) => {
|
|
|
4396
4436
|
if (cell.probe.mode !== "idle") await go();
|
|
4397
4437
|
await sleep(500);
|
|
4398
4438
|
} else {
|
|
4399
|
-
const replyInFlight = cell.probe.mode === "inflight" && (server && cell.fault !== "mtu-blackhole" || cell.fault === "fin");
|
|
4439
|
+
const replyInFlight = cell.probe.mode === "inflight" && (server && cell.fault !== "mtu-blackhole" || cell.fault === "fin" || requestFirst(cell));
|
|
4400
4440
|
if (replyInFlight) {
|
|
4401
4441
|
await go();
|
|
4402
4442
|
await sleep(200);
|
|
@@ -4447,6 +4487,9 @@ const once = async (lab, cell, environment, index, dry = false) => {
|
|
|
4447
4487
|
case "server-close":
|
|
4448
4488
|
await lab.serverClose();
|
|
4449
4489
|
return null;
|
|
4490
|
+
case "hop-event":
|
|
4491
|
+
if (cell.event === void 0) throw new Error(`${cell.name} names no event`);
|
|
4492
|
+
return lab.event(cell.event);
|
|
4450
4493
|
default: return lab.fault("blackhole", faultAt);
|
|
4451
4494
|
}
|
|
4452
4495
|
};
|
|
@@ -4465,6 +4508,9 @@ const once = async (lab, cell, environment, index, dry = false) => {
|
|
|
4465
4508
|
case "server-close":
|
|
4466
4509
|
case "app-stall":
|
|
4467
4510
|
case "none": return;
|
|
4511
|
+
case "hop-event":
|
|
4512
|
+
if (cell.event !== void 0) await lab.event(cell.event, true);
|
|
4513
|
+
return;
|
|
4468
4514
|
case "unreachable": return lab.lift("reject", faultAt, { code: cell.icmp ?? "host" });
|
|
4469
4515
|
case "simplex": return lab.lift("blackhole", faultAt, options);
|
|
4470
4516
|
case "mtu-blackhole": return lab.lift("mtu", cell.faultAt ?? "client", { above: cell.mtu ?? 1200 });
|