@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 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 { isHopPlace as $, Recovers as A, RunPart as B, Observer as C, ProxyHop as D, ProbeEvent as E, RequestPrediction as F, ShippedImage as G, SentinelEvent as H, RouterHop as I, Subject as J, Stage as K, RttRange as L, ReplicaSet as M, ReplicaSetStage as N, ProxyTime as O, Replies as P, WsProbe as Q, Run as R, MiddleboxForm as S, Prediction as T, SentinelStage as U, RunTopology as V, Shipped as W, Topology as X, Target as Y, Toxic as Z, ImageSource as _, DriverBand as a, MiddleboxEvent as b, FaultPlace as c, GroupPrediction as d, latestEndOf as et, GroupRole as f, HttpProbe as g, HopPlace as h, ClientImage as i, Reference as j, Reconnects as k, Group as l, Hop as m, Cell as n, DriverStep as o, HarnessImage as p, StageBand as q, Client as r, Fault as s, BuildImage as t, GroupPlace as u, KernelRecord as v, PinnedImage as w, MiddleboxExpectation as x, LineMatch as y, RunGroup as z };
1026
- //# sourceMappingURL=cell-COlAmUZ-.d.ts.map
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 isHopPlace, A as Recovers, B as RunPart, C as Observer, D as ProxyHop, E as ProbeEvent, F as RequestPrediction, G as ShippedImage, H as SentinelEvent, I as RouterHop, J as Subject, K as Stage, L as RttRange, M as ReplicaSet, N as ReplicaSetStage, O as ProxyTime, P as Replies, Q as WsProbe, R as Run, S as MiddleboxForm, T as Prediction, U as SentinelStage, V as RunTopology, W as Shipped, X as Topology, Y as Target, Z as Toxic, _ as ImageSource, a as DriverBand, b as MiddleboxEvent, c as FaultPlace, d as GroupPrediction, et as latestEndOf, f as GroupRole, g as HttpProbe, h as HopPlace, i as ClientImage, j as Reference, k as Reconnects, l as Group, m as Hop, n as Cell, o as DriverStep, p as HarnessImage, q as StageBand, r as Client, s as Fault, t as BuildImage, u as GroupPlace, v as KernelRecord, w as PinnedImage, x as MiddleboxExpectation, y as LineMatch, z as RunGroup } from "./cell-COlAmUZ-.js";
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 (run.fault !== null && run.fault.t1 > request.t) voided = "the operation went out before the fault held";
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 });