@mark1russell7/harness 0.11.24 → 0.11.25

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
@@ -252,6 +252,8 @@ ioredis sets the keepalive of its socket itself, after it connects. Thus a cell
252
252
  - **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, a stop of a proxy and PAUSE on the console of PgBouncer.
253
253
  - A signal goes to each process of `ProxyHop.program`: HUP, TERM, INT, STOP or CONT (`HopSignal`). 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.
254
254
  - STOP blocks the event loop of the proxy: its processes stop, and its kernel keeps each socket. With a hold (`Cell.hold`), CONT thaws it at that time after the stop, from inside the VM (`--at`). The lift thaws a proxy that its hold did not thaw.
255
+ - A write (`HopWrite`) changes a file of the proxy, as an edit of its configuration does. The sidecar writes it through the root of the program (`inject write NAME PATH CONTENT`): first a file beside it, then a rename in one step. Thus a program that watches the folder reads the whole file.
256
+ - A write can name a line of the output of the proxy that the change gives (`confirm`), for example the log line of a reload. The event waits 10 s at most for a new such line, else the run fails. Its `undo` writes the first content back after the run.
255
257
  - 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.
256
258
  - 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.
257
259
  - **A fault on a group** acts on the part that `Cell.faultAt` names by its role:
@@ -142,6 +142,8 @@ type HopSignal = "HUP" | "TERM" | "INT" | "STOP" | "CONT";
142
142
  * that the console trusts. Its `undo` lifts it after the run. Without `inFlight`, the event
143
143
  * comes first, and the request of the client meets it. With `inFlight`, the request goes first,
144
144
  * and the event comes while it is in flight.
145
+ *
146
+ * A write changes a file of the hop, as an edit of its configuration does (`HopWrite`).
145
147
  */
146
148
  type HopEvent = {
147
149
  readonly hop: string;
@@ -157,7 +159,27 @@ type HopEvent = {
157
159
  readonly undo?: string;
158
160
  };
159
161
  readonly inFlight?: true;
162
+ } | {
163
+ readonly hop: string;
164
+ readonly write: HopWrite;
165
+ readonly inFlight?: true;
160
166
  };
167
+ /**
168
+ * A hop write puts `content` at `path` in the root of the program of the hop. The content goes to a
169
+ * file beside it first, and a rename puts it in place in one step. Thus a program that watches the
170
+ * folder reads the whole file, not half of it. Its `undo` writes the file of the start again
171
+ * after the run, thus each run makes the same change.
172
+ *
173
+ * `confirm` is a line of the output of the hop that the change gives, for example the log line of
174
+ * a reload. The event waits for a new such line, and a write that the hop does not confirm in 10 s
175
+ * fails the run. Thus a cell that predicts no effect knows that the change took effect.
176
+ */
177
+ interface HopWrite {
178
+ readonly path: string;
179
+ readonly content: string;
180
+ readonly undo?: string;
181
+ readonly confirm?: string;
182
+ }
161
183
  /**
162
184
  * A hop is one box between the client and the target. Each hop is on two networks: the network
163
185
  * of the side of the client, and the network of the side of the target.
@@ -1131,5 +1153,5 @@ interface RunTopology {
1131
1153
  }>>;
1132
1154
  }
1133
1155
  //#endregion
1134
- export { Subject as $, Prediction as A, RequestPrediction as B, KernelRecord as C, MiddleboxForm as D, MiddleboxExpectation as E, Recovers as F, RunPart as G, RttRange as H, Reference as I, SentinelStage as J, RunTopology as K, ReplicaSet as L, ProxyHop as M, ProxyTime as N, Observer as O, Reconnects as P, StageBand as Q, ReplicaSetStage as R, ImageSource as S, MiddleboxEvent as T, Run as U, RouterHop as V, RunGroup as W, ShippedImage as X, Shipped as Y, Stage as Z, Hop as _, Cluster as a, latestEndOf as at, HopSignal as b, DriverBand as c, FaultPlace as d, Target as et, Group as f, HarnessImage as g, GroupRole as h, ClientImage as i, isHopPlace as it, ProbeEvent as j, PinnedImage as k, DriverStep as l, GroupPrediction as m, Cell as n, Toxic as nt, ClusterClientAt as o, GroupPlace as p, SentinelEvent as q, Client as r, WsProbe as rt, ClusterStage as s, BuildImage as t, Topology as tt, Fault as u, HopEvent as v, LineMatch as w, HttpProbe as x, HopPlace as y, Replies as z };
1135
- //# sourceMappingURL=cell-wInJC-GH.d.ts.map
1156
+ export { StageBand as $, PinnedImage as A, Replies as B, ImageSource 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, HttpProbe as S, LineMatch as T, RttRange as U, RequestPrediction as V, Run as W, Shipped as X, SentinelStage as Y, ShippedImage as Z, Hop as _, Cluster as a, isHopPlace as at, HopSignal as b, DriverBand as c, FaultPlace as d, Subject as et, Group as f, HarnessImage 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, HopEvent as v, KernelRecord as w, HopWrite as x, HopPlace as y, ReplicaSetStage as z };
1157
+ //# sourceMappingURL=cell-D4HZ1off.d.ts.map
package/build/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as Subject, A as Prediction, B as RequestPrediction, C as KernelRecord, D as MiddleboxForm, E as MiddleboxExpectation, F as Recovers, G as RunPart, H as RttRange, I as Reference, J as SentinelStage, K as RunTopology, L as ReplicaSet, M as ProxyHop, N as ProxyTime, O as Observer, P as Reconnects, Q as StageBand, R as ReplicaSetStage, S as ImageSource, T as MiddleboxEvent, U as Run, V as RouterHop, W as RunGroup, X as ShippedImage, Y as Shipped, Z as Stage, _ as Hop, a as Cluster, at as latestEndOf, b as HopSignal, c as DriverBand, d as FaultPlace, et as Target, f as Group, g as HarnessImage, h as GroupRole, i as ClientImage, it as isHopPlace, j as ProbeEvent, k as PinnedImage, l as DriverStep, m as GroupPrediction, n as Cell, nt as Toxic, o as ClusterClientAt, p as GroupPlace, q as SentinelEvent, r as Client, rt as WsProbe, s as ClusterStage, t as BuildImage, tt as Topology, u as Fault, v as HopEvent, w as LineMatch, x as HttpProbe, y as HopPlace, z as Replies } from "./cell-wInJC-GH.js";
1
+ import { $ as StageBand, A as PinnedImage, B as Replies, C as ImageSource, 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 HttpProbe, T as LineMatch, U as RttRange, V as RequestPrediction, W as Run, X as Shipped, Y as SentinelStage, Z as ShippedImage, _ as Hop, a as Cluster, at as isHopPlace, b as HopSignal, c as DriverBand, d as FaultPlace, et as Subject, f as Group, g as HarnessImage, 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 HopEvent, w as KernelRecord, x as HopWrite, y as HopPlace, z as ReplicaSetStage } from "./cell-D4HZ1off.js";
2
2
  import { Quantity } from "@mark1russell7/quantity";
3
3
  import "testcontainers";
4
4
  //#region src/application.d.ts
@@ -1633,5 +1633,5 @@ export declare const spread: (runs: number, environments: number) => number[];
1633
1633
  */
1634
1634
  export declare const runCell: (cell: Cell, say?: (line: string) => void) => Promise<CellResult>;
1635
1635
  //#endregion
1636
- export { type Agent, type BuildImage, type CallReading, type Cell, type CellResult, type Client, type ClientImage, type Cluster, type ClusterClientAt, type ClusterLab, type ClusterStage, 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 HopSignal, 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 NodeLine, 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 StatusRow, type Subject, type Target, type Topology, type Toxic, type Verdict, type WallStamp, type Writes, type WsProbe, isHopPlace, latestEndOf };
1636
+ export { type Agent, type BuildImage, type CallReading, type Cell, type CellResult, type Client, type ClientImage, type Cluster, type ClusterClientAt, type ClusterLab, type ClusterStage, 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 HopSignal, type HopWrite, 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 NodeLine, 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 StatusRow, type Subject, type Target, type Topology, type Toxic, type Verdict, type WallStamp, type Writes, type WsProbe, isHopPlace, latestEndOf };
1637
1637
  //# sourceMappingURL=index.d.ts.map
package/build/index.js CHANGED
@@ -2092,6 +2092,11 @@ const PUMP_SLACK = {
2092
2092
  };
2093
2093
  /** This count is how many of the last lines of a proxy hop the lab keeps. The error of a hop that does not start gives them. */
2094
2094
  const TAIL_LINES = 60;
2095
+ /** This count is how many lines of the output of a proxy hop the lab keeps, for a write that the proxy confirms. */
2096
+ const OUTPUT_LINES = 2e3;
2097
+ /** This time is how long a write of a hop waits for the line that confirms it, in milliseconds. */
2098
+ const CONFIRM_MS = 1e4;
2099
+ const sleep$4 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2095
2100
  /** This function gives the arguments of the injector for the options of a fault. */
2096
2101
  const optionArgs = (options) => [
2097
2102
  ...options.direction === void 0 ? [] : ["--direction", options.direction],
@@ -2328,6 +2333,7 @@ const openLab = async (target, kind = "tcp", topology) => {
2328
2333
  opened.push(server);
2329
2334
  const serverIp = await ipOn(server, last);
2330
2335
  const middlebox = [];
2336
+ const hopOutput = /* @__PURE__ */ new Map();
2331
2337
  const inChain = [];
2332
2338
  const endpointOf = (element) => {
2333
2339
  const at = endpointAfter(hops, element);
@@ -2386,6 +2392,11 @@ const openLab = async (target, kind = "tcp", topology) => {
2386
2392
  })));
2387
2393
  if (hop.ready !== void 0) proxy = proxy.withWaitStrategy(Wait.forLogMessage(hop.ready));
2388
2394
  const tail = [];
2395
+ const output = {
2396
+ lines: [],
2397
+ total: 0
2398
+ };
2399
+ hopOutput.set(hop.name, output);
2389
2400
  proxy = proxy.withLogConsumer((stream) => {
2390
2401
  let rest = "";
2391
2402
  stream.on("data", (chunk) => {
@@ -2394,6 +2405,9 @@ const openLab = async (target, kind = "tcp", topology) => {
2394
2405
  for (const line of lines) {
2395
2406
  tail.push(line);
2396
2407
  if (tail.length > TAIL_LINES) tail.shift();
2408
+ output.lines.push(line);
2409
+ output.total += 1;
2410
+ if (output.lines.length > OUTPUT_LINES) output.lines.shift();
2397
2411
  }
2398
2412
  });
2399
2413
  });
@@ -2806,18 +2820,21 @@ const openLab = async (target, kind = "tcp", topology) => {
2806
2820
  async event(event, undo = false, at) {
2807
2821
  const hop = hopNamed(event.hop);
2808
2822
  if (hop.hop.kind !== "proxy") throw new Error(`the hop ${event.hop} is a router: an event goes to a proxy`);
2809
- if ("signal" in event) {
2810
- if (undo) return null;
2811
- const program = hop.hop.program;
2812
- if (program === void 0) throw new Error(`the hop ${event.hop} names no program to signal`);
2823
+ const program = hop.hop.program;
2824
+ const sidecarOf = async () => {
2813
2825
  let sidecar = hopSignallers.get(event.hop);
2814
2826
  if (sidecar === void 0) {
2815
- sidecar = await withPid(new GenericContainer(image).withNetworkMode(`container:${hop.box.getId()}`).withAddedCapabilities("NET_ADMIN", "NET_RAW"), `container:${hop.box.getId()}`).start();
2827
+ sidecar = await withPid(new GenericContainer(image).withNetworkMode(`container:${hop.box.getId()}`).withAddedCapabilities("NET_ADMIN", "NET_RAW", "SYS_PTRACE"), `container:${hop.box.getId()}`).start();
2816
2828
  hopSignallers.set(event.hop, sidecar);
2817
2829
  opened.push(sidecar);
2818
2830
  }
2831
+ return sidecar;
2832
+ };
2833
+ if ("signal" in event) {
2834
+ if (undo) return null;
2835
+ if (program === void 0) throw new Error(`the hop ${event.hop} names no program to signal`);
2819
2836
  const when = at === void 0 ? [] : ["--at", String(Math.round(at))];
2820
- return stamped(sidecar, [
2837
+ return stamped(await sidecarOf(), [
2821
2838
  "/opt/harness/inject",
2822
2839
  "signal",
2823
2840
  program,
@@ -2825,6 +2842,34 @@ const openLab = async (target, kind = "tcp", topology) => {
2825
2842
  ...when
2826
2843
  ]);
2827
2844
  }
2845
+ if ("write" in event) {
2846
+ const content = undo ? event.write.undo : event.write.content;
2847
+ if (content === void 0) return null;
2848
+ if (program === void 0) throw new Error(`the hop ${event.hop} names no program whose root a write reaches`);
2849
+ const output = hopOutput.get(event.hop);
2850
+ const before = output?.total ?? 0;
2851
+ const stamp = await stamped(await sidecarOf(), [
2852
+ "/opt/harness/inject",
2853
+ "write",
2854
+ program,
2855
+ event.write.path,
2856
+ content
2857
+ ]);
2858
+ const confirm = event.write.confirm;
2859
+ if (confirm !== void 0) {
2860
+ const started = Date.now();
2861
+ const confirmed = () => {
2862
+ if (output === void 0) return false;
2863
+ const fresh = Math.min(output.total - before, output.lines.length);
2864
+ return output.lines.slice(output.lines.length - fresh).some((line) => line.includes(confirm));
2865
+ };
2866
+ while (!confirmed()) {
2867
+ if (Date.now() - started > CONFIRM_MS) throw new Error(`the hop ${event.hop} did not confirm the write of ${event.write.path}: no line "${confirm}" in ${CONFIRM_MS / 1e3} s`);
2868
+ await sleep$4(50);
2869
+ }
2870
+ }
2871
+ return stamp;
2872
+ }
2828
2873
  const query = undo ? event.console.undo : event.console.query;
2829
2874
  if (query === void 0) return null;
2830
2875
  const { port, user, database } = event.console;