@mark1russell7/kind 0.13.3 → 0.14.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/build/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as signalsOf, A as HBONE_PORT, At as secondsOf, B as isTunnelState, C as observationEnd, Ct as VIP_ANNOTATION, D as reusesReset, Dt as overshootBand, E as referenceOf, Et as drainBand, F as deletionOf, G as podsOf, H as outcomesOf, I as dropOf, J as restartOf, K as recordsOf, L as emptyOf, M as backendsAt, N as byClients, O as unitOf, Ot as requestBand, P as byNode, Q as signalOf, R as failuresOf, S as observationAfter, St as SERVER, T as propagationSource, Tt as bandOf, U as periodOf, V as kernelCounts, W as podsAt, X as rulesOf, Y as restartsAt, Z as sessionsAt, _ as checkRun, _t as LB_CLASS, a as calibrationOf, at as socketsOf, b as factsOf, bt as PORT, c as predictionText, ct as tunnelsOf, d as FORMAT_V1, dt as RELATIVE, et as sliceDropOf, f as judge, ft as medianInterval, g as LAST_SLACK, gt as HEALTH_PORT, h as KIND_NOISE, ht as CLIENT, i as readRecord, it as slicesOf, j as TCP_STATE, jt as showBand, k as updatesOf, kt as seconds, l as timeSpread, lt as ztunnelPodsOf, m as upgradeCell, mt as quantileOf, n as kindCellRecordSchema, nt as sliceReadyAt, o as cellsTable, ot as stoppingOf, p as upgrade, pt as middle, q as requestsOf, r as kindCellRecordV1Schema, rt as sliceReadyOf, s as medianText, st as transitionOf, t as STAGES, tt as sliceEmptyOf, u as FORMAT, ut as ztunnelReplaced, v as cyclesOf, vt as LOG_LABEL, w as propagationOf, wt as backlogEnd, x as judgeKind, xt as REDIS, y as deletionsOf, yt as OUTSIDE, z as intakeOf } from "./judge-DKrgfhAt.js";
1
+ import { $ as rulesOf, A as judgeReconnects, At as overshootBand, B as emptyOf, C as observationEnd, Ct as PORT, D as reusesReset, Dt as backlogEnd, E as referenceOf, Et as VIP_ANNOTATION, F as backendsAt, G as outcomesOf, H as intakeOf, I as byClients, J as podsOf, K as periodOf, L as byNode, M as triesOf, Mt as seconds, N as HBONE_PORT, Nt as secondsOf, O as unitOf, Ot as bandOf, P as TCP_STATE, Pt as showBand, Q as restartsAt, R as deletionOf, S as observationAfter, St as OUTSIDE, T as propagationSource, Tt as SERVER, U as isTunnelState, V as failuresOf, W as kernelCounts, X as requestsOf, Y as recordsOf, Z as restartOf, _ as checkRun, _t as quantileOf, a as calibrationOf, at as sliceReadyAt, b as factsOf, bt as LB_CLASS, c as predictionText, ct as socketsOf, d as FORMAT_V1, dt as tunnelsOf, et as sessionsAt, f as judge, ft as ztunnelPodsOf, g as LAST_SLACK, gt as middle, h as KIND_NOISE, ht as medianInterval, i as readRecord, it as sliceEmptyOf, j as latestReturn, jt as requestBand, k as updatesOf, kt as drainBand, l as timeSpread, lt as stoppingOf, m as upgradeCell, mt as RELATIVE, n as kindCellRecordSchema, nt as signalsOf, o as cellsTable, ot as sliceReadyOf, p as upgrade, pt as ztunnelReplaced, q as podsAt, r as kindCellRecordV1Schema, rt as sliceDropOf, s as medianText, st as slicesOf, t as STAGES, tt as signalOf, u as FORMAT, ut as transitionOf, v as cyclesOf, vt as CLIENT, w as propagationOf, wt as REDIS, x as judgeKind, xt as LOG_LABEL, y as deletionsOf, yt as HEALTH_PORT, z as dropOf } from "./judge-C6XgY2Pk.js";
2
2
  import { valueIn } from "@mark1russell7/quantity";
3
3
  import { chmodSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
4
4
  import { dirname, join } from "node:path";
@@ -10,7 +10,7 @@ import { gunzipSync } from "node:zlib";
10
10
  import { failSpan, lint, lintStop, recoverSpan, service } from "@mark1russell7/service";
11
11
  import { createRequire } from "node:module";
12
12
  import { tmpdir } from "node:os";
13
- import { harnessImage, imageOf } from "@mark1russell7/harness";
13
+ import { harnessImage, imageOf, substrateKey, substrateOf, substrateOn } from "@mark1russell7/harness";
14
14
  import { redisClient } from "@mark1russell7/service/redis";
15
15
  import { DRAIN_RULES, DRAIN_RULES as DRAIN_RULES$1, O04, O09, O10, O16 } from "@mark1russell7/service/rules";
16
16
  //#region src/backends.ts
@@ -1375,7 +1375,7 @@ const stepsOf = (cell) => {
1375
1375
  case "crash": return {
1376
1376
  inject: [{
1377
1377
  do: "signal",
1378
- target: "server",
1378
+ target: cell.pulsar === void 0 ? "server" : "broker",
1379
1379
  signal: "SIGKILL"
1380
1380
  }],
1381
1381
  hold: null,
@@ -1393,6 +1393,24 @@ const stepsOf = (cell) => {
1393
1393
  restore: []
1394
1394
  };
1395
1395
  case "freeze-process":
1396
+ if (cell.vip !== void 0) return {
1397
+ inject: [{
1398
+ do: "signal",
1399
+ target: "speaker",
1400
+ signal: "SIGSTOP"
1401
+ }],
1402
+ hold: needs(trigger.hold, "a hold, the time that the speaker is frozen"),
1403
+ heal: [{
1404
+ do: "signal",
1405
+ target: "speaker",
1406
+ signal: "SIGCONT"
1407
+ }],
1408
+ restore: [{
1409
+ do: "signal",
1410
+ target: "speaker",
1411
+ signal: "SIGCONT"
1412
+ }]
1413
+ };
1396
1414
  if (cell.template === "T07") return {
1397
1415
  inject: [
1398
1416
  {
@@ -1814,7 +1832,7 @@ const deletes = (cell) => [
1814
1832
  "rolling-update",
1815
1833
  "node-drain",
1816
1834
  "scale-in"
1817
- ].includes(cell.trigger.kind) || cell.template === "T07" && cell.trigger.kind === "freeze-process";
1835
+ ].includes(cell.trigger.kind) || cell.template === "T07" && cell.trigger.kind === "freeze-process" && cell.vip === void 0;
1818
1836
  //#endregion
1819
1837
  //#region src/image.ts
1820
1838
  /** This folder is the root of the package, and its `image/` folder has the files of the image. */
@@ -2511,7 +2529,7 @@ const vipOf = (subnet) => {
2511
2529
  0
2512
2530
  ].map((shift) => Math.floor(address / 2 ** shift) % 256).join(".");
2513
2531
  };
2514
- const sleep$2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2532
+ const sleep$3 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2515
2533
  /** This function gives the pods of kube-vip that are not deleted. */
2516
2534
  const kubeVipPods = async (cluster) => {
2517
2535
  return JSON.parse(await cluster.kubectl([
@@ -2540,7 +2558,7 @@ const until$1 = async (within, check) => {
2540
2558
  const end = Date.now() + within;
2541
2559
  let why = await check();
2542
2560
  while (why !== null && Date.now() < end) {
2543
- await sleep$2(500);
2561
+ await sleep$3(500);
2544
2562
  why = await check();
2545
2563
  }
2546
2564
  return why;
@@ -2657,8 +2675,8 @@ const vipServed = (cluster, namespace, service, vip, within = 12e4) => until$1(w
2657
2675
  * a usable speaker (speaker/layer2_controller.go, lines 83–129). A usable speaker is a member of
2658
2676
  * memberlist that is not dead (internal/speakerlist/speakerlist.go, lines 303–320).
2659
2677
  * - The speakers start memberlist from `DefaultLANConfig` (speakerlist.go, line 72). A file
2660
- * /etc/metallb/memberlist/config.json replaces that config (lines 77–84). The install makes no
2661
- * such file, thus each value is the default of hashicorp/memberlist v0.5.1.
2678
+ * /etc/metallb/memberlist/config.json replaces that config (lines 77–84). Without settings of
2679
+ * memberlist, the install makes no such file. Then each value is the default of memberlist v0.5.1.
2662
2680
  * - Each event of memberlist (a join, a leave or an update) calls `ForceSync` (speakerlist.go, lines
2663
2681
  * 339–349). `ForceSync` reconciles each Service at once, with no delay (internal/k8s/k8s.go, lines
2664
2682
  * 181–184, and internal/k8s/controllers/service_controller_reload.go, lines 62–118).
@@ -2719,6 +2737,60 @@ const MEMBERLIST = {
2719
2737
  unit: "ms"
2720
2738
  }
2721
2739
  };
2740
+ /**
2741
+ * These are the values of the config file of memberlist, as `DefaultLANConfig` of v0.5.1 gives them
2742
+ * (config.go, lines 302–341). The speaker needs each field of the file, and it reads a time as a
2743
+ * duration of Go (speakerlist.go, lines 355–432). The chart mounts the ConfigMap
2744
+ * `metallb-memberlist-config` at /etc/metallb/memberlist, if the ConfigMap exists
2745
+ * (charts/metallb/templates/speaker.yaml, lines 195–198 and 391–392). The key is `config.json`.
2746
+ */
2747
+ const MEMBERLIST_FILE = {
2748
+ TCPTimeout: "10s",
2749
+ IndirectChecks: 3,
2750
+ RetransmitMult: 4,
2751
+ SuspicionMult: 4,
2752
+ SuspicionMaxTimeoutMult: 6,
2753
+ PushPullInterval: "30s",
2754
+ ProbeTimeout: "500ms",
2755
+ ProbeInterval: "1s",
2756
+ DisableTcpPings: false,
2757
+ AwarenessMaxMultiplier: 8,
2758
+ GossipNodes: 3,
2759
+ GossipInterval: "200ms",
2760
+ GossipToTheDeadTime: "30s",
2761
+ GossipVerifyIncoming: true,
2762
+ GossipVerifyOutgoing: true,
2763
+ EnableCompression: true,
2764
+ HandoffQueueDepth: 1024,
2765
+ UDPBufferSize: 1400,
2766
+ QueueCheckInterval: "30s"
2767
+ };
2768
+ /** This name is the ConfigMap of the config file of memberlist. */
2769
+ const MEMBERLIST_CONFIG = "metallb-memberlist-config";
2770
+ /** This function gives a time as a duration of Go in whole milliseconds, for example "500ms". */
2771
+ const goDuration$1 = (time) => {
2772
+ const ms = valueIn(time, "ms");
2773
+ if (!Number.isInteger(ms) || ms <= 0) throw new RangeError(`a time of memberlist is a whole count of milliseconds above zero, not ${ms} ms`);
2774
+ return `${ms}ms`;
2775
+ };
2776
+ /** This function gives the config file of memberlist with `settings`. Each other value is the default of `MEMBERLIST_FILE`. */
2777
+ const memberlistFileOf = (settings) => {
2778
+ if (!Number.isInteger(settings.suspicionMult) || settings.suspicionMult < 1) throw new RangeError(`SuspicionMult is a whole number of 1 or more, not ${settings.suspicionMult}`);
2779
+ if (valueIn(settings.probeTimeout, "ms") >= valueIn(settings.probeInterval, "ms")) throw new RangeError("the probe timeout of memberlist is less than its probe interval");
2780
+ return {
2781
+ ...MEMBERLIST_FILE,
2782
+ ProbeInterval: goDuration$1(settings.probeInterval),
2783
+ ProbeTimeout: goDuration$1(settings.probeTimeout),
2784
+ SuspicionMult: settings.suspicionMult
2785
+ };
2786
+ };
2787
+ /** This function gives the terms of memberlist with `settings`, or the defaults (`MEMBERLIST`) without them. */
2788
+ const memberlistTermsOf = (settings) => settings === void 0 ? MEMBERLIST : {
2789
+ ...MEMBERLIST,
2790
+ probeInterval: settings.probeInterval,
2791
+ probeTimeout: settings.probeTimeout,
2792
+ suspicionMult: settings.suspicionMult
2793
+ };
2722
2794
  /** These are the gratuitous ARPs of the owner at each reconcile: one at once, then one each `every` until `for` (announcer.go, lines 172–182). */
2723
2795
  const METALLB_SPAM = {
2724
2796
  every: {
@@ -2829,13 +2901,13 @@ const metallbVipOf = (subnet, owner, nodes) => {
2829
2901
  }
2830
2902
  throw new Error(`the subnet ${subnet} has no VIP that ${owner} owns`);
2831
2903
  };
2832
- const sleep$1 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2904
+ const sleep$2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
2833
2905
  /** This function waits until a condition gives null, and gives null, or the last reason after `within` ms. */
2834
2906
  const until = async (within, check) => {
2835
2907
  const end = Date.now() + within;
2836
2908
  let why = await check();
2837
2909
  while (why !== null && Date.now() < end) {
2838
- await sleep$1(500);
2910
+ await sleep$2(500);
2839
2911
  why = await check();
2840
2912
  }
2841
2913
  return why;
@@ -2902,8 +2974,12 @@ const holdMetallb = async (cluster, owner, nodes, namespace, service, within = 1
2902
2974
  * and a pool of the VIP. The webhook of the controller checks the pool, thus the function tries the
2903
2975
  * pool again until the webhook answers. A failed install gives the pods of MetalLB and the tail of
2904
2976
  * their logs.
2977
+ *
2978
+ * With `memberlist`, the ConfigMap of the config file comes before the speakers. Then the function
2979
+ * checks that each speaker logged the load of the file (speakerlist.go, line 393). Without it, the
2980
+ * function checks that the ConfigMap does not exist.
2905
2981
  */
2906
- const installMetallb = async (cluster, images, nodes, vip) => {
2982
+ const installMetallb = async (cluster, images, nodes, vip, memberlist) => {
2907
2983
  try {
2908
2984
  for (const node of nodes) await cluster.kubectl([
2909
2985
  "label",
@@ -2912,24 +2988,66 @@ const installMetallb = async (cluster, images, nodes, vip) => {
2912
2988
  `${VIP_LABEL}=1`,
2913
2989
  "--overwrite"
2914
2990
  ]);
2991
+ if (memberlist !== void 0) {
2992
+ await cluster.apply([{
2993
+ apiVersion: "v1",
2994
+ kind: "Namespace",
2995
+ metadata: { name: METALLB_NAMESPACE }
2996
+ }]);
2997
+ await cluster.apply([{
2998
+ apiVersion: "v1",
2999
+ kind: "ConfigMap",
3000
+ metadata: {
3001
+ name: MEMBERLIST_CONFIG,
3002
+ namespace: METALLB_NAMESPACE
3003
+ },
3004
+ data: { "config.json": JSON.stringify(memberlistFileOf(memberlist)) }
3005
+ }]);
3006
+ }
2915
3007
  await helmInstall(cluster, "metallb", METALLB_CHART, METALLB_NAMESPACE, metallbValues(images), { createNamespace: true });
2916
3008
  let failure = null;
2917
3009
  const end = Date.now() + 18e4;
2918
3010
  do {
2919
3011
  failure = await cluster.apply(metallbObjects(vip)).then(() => null, (error) => error);
2920
- if (failure !== null) await sleep$1(2e3);
3012
+ if (failure !== null) await sleep$2(2e3);
2921
3013
  } while (failure !== null && Date.now() < end);
2922
3014
  if (failure !== null) throw failure;
2923
- if ((await cluster.kubectl([
2924
- "-n",
2925
- "metallb-system",
2926
- "get",
2927
- "configmap",
2928
- "metallb-memberlist-config",
2929
- "--ignore-not-found",
2930
- "-o",
2931
- "name"
2932
- ])).trim() !== "") throw new Error("the ConfigMap metallb-memberlist-config is there: memberlist would not use its defaults");
3015
+ if (memberlist === void 0) {
3016
+ if ((await cluster.kubectl([
3017
+ "-n",
3018
+ "metallb-system",
3019
+ "get",
3020
+ "configmap",
3021
+ MEMBERLIST_CONFIG,
3022
+ "--ignore-not-found",
3023
+ "-o",
3024
+ "name"
3025
+ ])).trim() !== "") throw new Error(`the ConfigMap ${MEMBERLIST_CONFIG} is there: memberlist would not use its defaults`);
3026
+ } else {
3027
+ const loaded = await until(12e4, async () => {
3028
+ const pods = JSON.parse(await cluster.kubectl([
3029
+ "-n",
3030
+ METALLB_NAMESPACE,
3031
+ "get",
3032
+ "pods",
3033
+ "-l",
3034
+ "app.kubernetes.io/component=speaker",
3035
+ "-o",
3036
+ "json"
3037
+ ]));
3038
+ if (pods.items.length !== nodes.length) return `${pods.items.length} speakers, not ${nodes.length}`;
3039
+ for (const pod of pods.items) if (!(await cluster.kubectl([
3040
+ "-n",
3041
+ "metallb-system",
3042
+ "logs",
3043
+ pod.metadata.name,
3044
+ "-c",
3045
+ "speaker"
3046
+ ]).catch((error) => String(error))).includes("loading memberlist config from file")) return `the speaker ${pod.metadata.name} did not log the load of the config file of memberlist`;
3047
+ return null;
3048
+ });
3049
+ if (loaded !== null) throw new Error(loaded);
3050
+ }
2933
3051
  } catch (error) {
2934
3052
  const state = async (args) => (await cluster.kubectl([
2935
3053
  "-n",
@@ -2954,7 +3072,7 @@ const installMetallb = async (cluster, images, nodes, vip) => {
2954
3072
  }
2955
3073
  };
2956
3074
  /** These words mark the lines of a speaker that the record keeps: the events of memberlist, the announces and the withdraws. */
2957
- const KEPT = /node event|Marking|Suspect|Refuting|serviceAnnounced|serviceWithdrawn|memberlist/;
3075
+ const KEPT$1 = /node event|Marking|Suspect|Refuting|serviceAnnounced|serviceWithdrawn|memberlist/;
2958
3076
  /**
2959
3077
  * This function gives the record of MetalLB after a run: the nodes that announce the Service, and
2960
3078
  * some lines of each speaker. The lines are about memberlist and the announces, with the times of the kubelet.
@@ -2974,7 +3092,7 @@ const metallbRecord = async (cluster, namespace, service) => {
2974
3092
  "--tail=4000",
2975
3093
  "--prefix",
2976
3094
  "--timestamps"
2977
- ]).catch((error) => String(error))).split("\n").filter((line) => KEPT.test(line)).slice(-120).join("\n").slice(-12e3)
3095
+ ]).catch((error) => String(error))).split("\n").filter((line) => KEPT$1.test(line)).slice(-600).join("\n").slice(-9e4)
2978
3096
  };
2979
3097
  };
2980
3098
  //#endregion
@@ -3025,10 +3143,10 @@ const METALLB_ANNOUNCER = {
3025
3143
  },
3026
3144
  settle: METALLB_SPAM.for,
3027
3145
  vipOf: (subnet, owner, nodes) => metallbVipOf(subnet, owner, nodes),
3028
- install: (cluster, images, _owner, nodes, vip) => installMetallb(cluster, {
3146
+ install: (cluster, images, _owner, nodes, vip, settings) => installMetallb(cluster, {
3029
3147
  speaker: local(images, "metallb-speaker"),
3030
3148
  controller: local(images, "metallb-controller")
3031
- }, nodes, vip),
3149
+ }, nodes, vip, settings?.memberlist),
3032
3150
  hold: (cluster, owner, nodes, namespace, service) => holdMetallb(cluster, owner, nodes, namespace, service),
3033
3151
  record: (cluster, namespace, service) => metallbRecord(cluster, namespace, service)
3034
3152
  };
@@ -3041,6 +3159,451 @@ const macsAt = (found, mac, count) => {
3041
3159
  return wrong.length === 0 ? null : wrong.map((each) => `${each.client} has ${each.mac ?? "no MAC"}`).join(", ");
3042
3160
  };
3043
3161
  //#endregion
3162
+ //#region src/pulsar.ts
3163
+ /**
3164
+ * This module installs Apache Pulsar 5.0.0 in a kind cluster (SYS-56): one ZooKeeper, one bookie and
3165
+ * two brokers, from the Helm chart 4.8.0. It also gives the producers of the cell: a Python script
3166
+ * in the image of Pulsar, with its client 3.13.0 over the C++ client 4.2.0. Each producer sends to
3167
+ * one topic, and a thread of it looks up the owner of that topic at each probe.
3168
+ *
3169
+ * The fault is a SIGKILL of the JVM of the broker that owns the topic. Each term of the return of a
3170
+ * producer comes from a pinned source:
3171
+ * - The owner of a bundle is an ephemeral znode of the session of its broker. A lookup gives the
3172
+ * owner from the cache of the ownership, with no check of its life (pulsar-broker
3173
+ * NamespaceService.java at v5.0.0, lines 489–508). Thus each lookup names the stopped broker
3174
+ * until its session expires.
3175
+ * - The session timeout is `metadataStoreSessionTimeoutMillis`, 30000 (conf/broker.conf at v5.0.0,
3176
+ * line 187, and the chart, values.yaml line 1474). ZKMetadataStore.java gives it to ZooKeeper (line 105).
3177
+ * - The client of ZooKeeper 3.9.6 pings after 9 s with no send (ClientCnxn.java, lines 422, 1184
3178
+ * and 1263–1268). That is half of the read timeout of 2/3 of the session, less 1 s.
3179
+ * The server rounds each expiry up to the next tick of 2000 ms (ExpiryQueue.java, lines 53–54
3180
+ * and 87, and conf/zookeeper.conf of Pulsar, line 21). Thus the session expires 21–32 s after the stop.
3181
+ * - The broker that stays then takes the bundle at the next lookup.
3182
+ * - The chart starts a broker again only after its old znode is gone (templates/broker-statefulset.yaml,
3183
+ * lines 296–302). Thus the stopped broker cannot take the bundle back first.
3184
+ */
3185
+ /** This version is the release of Pulsar of the lab. */
3186
+ const PULSAR_VERSION = "5.0.0";
3187
+ /** This chart is the Helm chart 4.8.0 of Pulsar. The sha256 is of the file of the release. */
3188
+ const PULSAR_CHART = {
3189
+ name: "pulsar",
3190
+ version: "4.8.0",
3191
+ url: "https://downloads.apache.org/pulsar/helm-chart/4.8.0/pulsar-4.8.0.tgz",
3192
+ sha256: "464f4a268463d187c9bc83b365b31de0b4a460533d948a04f846ba9b026068f5"
3193
+ };
3194
+ /**
3195
+ * This image is Pulsar 5.0.0 for linux/amd64, pinned by the digest of the manifest of the platform.
3196
+ * It has ZooKeeper 3.9.6, BookKeeper, the broker, Python 3 and the Python client 3.13.0. The lab
3197
+ * pulls it by its digest, tags it (`localPin`), and loads it into each node.
3198
+ */
3199
+ const PULSAR_IMAGE = "apachepulsar/pulsar:5.0.0@sha256:11e9a885121e16be91da77fdd3cfc3d4cbbece4700271d75e949585dbd3a5a5b";
3200
+ /** This namespace holds Pulsar. The release of the chart is `pulsar` too. */
3201
+ const PULSAR_NAMESPACE = "pulsar";
3202
+ /** This name is the app of the producers of a cell. */
3203
+ const PRODUCER = "producer";
3204
+ /** This topic is the topic of the producers. */
3205
+ const PULSAR_TOPIC = "persistent://public/default/lab";
3206
+ /** This URL is the Service of the brokers for the binary protocol. */
3207
+ const PULSAR_SERVICE = `pulsar://pulsar-broker.${PULSAR_NAMESPACE}.svc.cluster.local:6650`;
3208
+ /** This URL is the lookup of the owner of the topic, on the Service of the brokers. */
3209
+ const PULSAR_LOOKUP = `http://pulsar-broker.${PULSAR_NAMESPACE}.svc.cluster.local:8080/lookup/v2/topic/persistent/public/default/lab`;
3210
+ /** This time is the session timeout of each broker in ZooKeeper (`metadataStoreSessionTimeoutMillis`). */
3211
+ const SESSION = {
3212
+ value: 30,
3213
+ unit: "s"
3214
+ };
3215
+ /** This time is the tick of ZooKeeper (`tickTime`). The server rounds each expiry up to a tick. */
3216
+ const TICK = {
3217
+ value: 2,
3218
+ unit: "s"
3219
+ };
3220
+ /** This time is the longest idle time of the client of ZooKeeper before its ping: the half of 2/3 of the session, less 1 s. */
3221
+ const PING = {
3222
+ value: 9,
3223
+ unit: "s"
3224
+ };
3225
+ /**
3226
+ * This backoff is the backoff of a producer of the C++ client 4.2.0, with the defaults of the Python
3227
+ * client 3.13.0:
3228
+ * - The initial delay is 100 ms and the most is 60 s (lib/ClientConfigurationImpl.h, lines 39–40).
3229
+ * - The mandatory stop is the send timeout less 100 ms, and at least 100 ms (lib/ProducerImpl.cc,
3230
+ * lines 55–57). The send timeout of `create_producer` is 30000 ms (pulsar/__init__.py, line 1015).
3231
+ * - The jitter takes 0–9 % of each delay (lib/Backoff.cc, line 54).
3232
+ * - A try that fails takes the lookup and the connect, which a reset ends at once. The lab gives it 100 ms.
3233
+ */
3234
+ const PRODUCER_BACKOFF = {
3235
+ initial: {
3236
+ value: 100,
3237
+ unit: "ms"
3238
+ },
3239
+ max: {
3240
+ value: 60,
3241
+ unit: "s"
3242
+ },
3243
+ mandatoryStop: {
3244
+ value: 29.9,
3245
+ unit: "s"
3246
+ },
3247
+ jitter: .09,
3248
+ attempt: {
3249
+ value: 100,
3250
+ unit: "ms"
3251
+ }
3252
+ };
3253
+ /** This function gives the repository and the tag of a local image, for the values of a chart. */
3254
+ const repositoryOf = (local) => {
3255
+ const at = local.lastIndexOf(":");
3256
+ if (at <= 0) throw new Error(`the image ${local} has no tag`);
3257
+ return {
3258
+ repository: local.slice(0, at),
3259
+ tag: local.slice(at + 1)
3260
+ };
3261
+ };
3262
+ /**
3263
+ * This function gives the values of the chart: ZooKeeper, one bookie and two brokers, with no
3264
+ * proxy, no autorecovery, no toolset and no monitor. The bookie keeps one copy of each entry, and
3265
+ * the brokers do not move a bundle by load. Each image is the local image, with no pull.
3266
+ */
3267
+ const pulsarValues = (image) => {
3268
+ const { repository, tag } = repositoryOf(image);
3269
+ const noMonitor = { podMonitor: { enabled: false } };
3270
+ return {
3271
+ components: {
3272
+ zookeeper: true,
3273
+ bookkeeper: true,
3274
+ broker: true,
3275
+ autorecovery: false,
3276
+ proxy: false,
3277
+ toolset: false,
3278
+ functions: false,
3279
+ function_worker: false
3280
+ },
3281
+ "victoria-metrics-k8s-stack": { enabled: false },
3282
+ defaultPulsarImageRepository: repository,
3283
+ defaultPulsarImageTag: tag,
3284
+ defaultPullPolicy: "Never",
3285
+ affinity: { anti_affinity: false },
3286
+ volumes: { persistence: false },
3287
+ zookeeper: {
3288
+ replicaCount: 1,
3289
+ statefulsetUpgrade: { enabled: false },
3290
+ ...noMonitor
3291
+ },
3292
+ bookkeeper: {
3293
+ replicaCount: 1,
3294
+ ...noMonitor
3295
+ },
3296
+ autorecovery: noMonitor,
3297
+ proxy: noMonitor,
3298
+ broker: {
3299
+ replicaCount: 2,
3300
+ statefulsetUpgrade: { enabled: false },
3301
+ ...noMonitor,
3302
+ configData: {
3303
+ managedLedgerDefaultEnsembleSize: "1",
3304
+ managedLedgerDefaultWriteQuorum: "1",
3305
+ managedLedgerDefaultAckQuorum: "1",
3306
+ loadBalancerSheddingEnabled: "false",
3307
+ loadBalancerAutoBundleSplitEnabled: "false"
3308
+ }
3309
+ }
3310
+ };
3311
+ };
3312
+ /**
3313
+ * This script is a producer of the cell. Each line that it writes is JSON, with the time of
3314
+ * CLOCK_MONOTONIC in milliseconds, the pod and the node:
3315
+ * - `send`: one send, at its start, with its time to the answer and its outcome. A send is `ok` within
3316
+ * the timeout, `late` after it, or the name of the result of the client (for example `timeout`).
3317
+ * - `lookup`: one lookup of the owner of the topic, with the host of the owner.
3318
+ * - `pulsar`: one line of the C++ client about the connection of the producer.
3319
+ */
3320
+ const PRODUCER_SCRIPT = `import json, logging, os, sys, threading, time, urllib.request
3321
+ import pulsar
3322
+
3323
+ POD = os.environ["POD_NAME"]
3324
+ NODE = os.environ["NODE_NAME"]
3325
+ EVERY = int(os.environ["EVERY_MS"]) / 1000
3326
+ TIMEOUT = int(os.environ["TIMEOUT_MS"])
3327
+ PROBE = int(os.environ["PROBE_MS"]) / 1000
3328
+ LOCK = threading.Lock()
3329
+ KEPT = ("Schedule reconnection", "Getting connection from pool", "Created producer", "Connection closed", "Disconnected", "Lookup response", "Failed to", "Finished connecting")
3330
+
3331
+
3332
+ def now():
3333
+ return time.monotonic() * 1000
3334
+
3335
+
3336
+ def say(**line):
3337
+ text = json.dumps({**line, "p": POD, "n": NODE}, separators=(",", ":"))
3338
+ with LOCK:
3339
+ sys.stdout.write(text + "\\n")
3340
+ sys.stdout.flush()
3341
+
3342
+
3343
+ class Relay(logging.Handler):
3344
+ def emit(self, record):
3345
+ message = record.getMessage()
3346
+ if any(word in message for word in KEPT):
3347
+ say(ev="pulsar", t=now(), m=message[:400])
3348
+
3349
+
3350
+ def probe():
3351
+ while True:
3352
+ start = now()
3353
+ owner = None
3354
+ outcome = "ok"
3355
+ try:
3356
+ with urllib.request.urlopen(os.environ["LOOKUP"], timeout=1) as answer:
3357
+ data = json.loads(answer.read())
3358
+ url = data.get("brokerUrl") or ""
3359
+ owner = url.split("://", 1)[-1].rsplit(":", 1)[0] or None
3360
+ except Exception as error:
3361
+ outcome = type(error).__name__.lower()
3362
+ say(ev="lookup", t=start, d=round(now() - start, 3), o=outcome, owner=owner)
3363
+ time.sleep(max(0.0, PROBE - (now() - start) / 1000))
3364
+
3365
+
3366
+ def answered(start):
3367
+ def done(result, message_id):
3368
+ took = now() - start
3369
+ if result == pulsar.Result.Ok:
3370
+ outcome = "ok" if took <= TIMEOUT else "late"
3371
+ else:
3372
+ outcome = getattr(result, "name", str(result).split(".")[-1]).lower()
3373
+ say(ev="send", t=start, d=round(took, 3), o=outcome)
3374
+ return done
3375
+
3376
+
3377
+ logger = logging.getLogger("lab")
3378
+ logger.setLevel(logging.INFO)
3379
+ logger.propagate = False
3380
+ logger.addHandler(Relay())
3381
+ threading.Thread(target=probe, daemon=True).start()
3382
+ client = pulsar.Client(os.environ["SERVICE"], logger=logger)
3383
+ while True:
3384
+ try:
3385
+ producer = client.create_producer(os.environ["TOPIC"])
3386
+ break
3387
+ except Exception as error:
3388
+ say(ev="text", t=now(), text="create_producer: " + str(error)[:200])
3389
+ time.sleep(2)
3390
+ at = time.monotonic()
3391
+ while True:
3392
+ start = now()
3393
+ try:
3394
+ producer.send_async(b"lab", answered(start))
3395
+ except Exception as error:
3396
+ say(ev="send", t=start, d=0, o="refused-" + type(error).__name__.lower())
3397
+ at += EVERY
3398
+ time.sleep(max(0.0, at - time.monotonic()))
3399
+ `;
3400
+ /**
3401
+ * This function gives the producers of a cell in its namespace. They are the script in a ConfigMap,
3402
+ * and a Deployment of `clients` pods in the local image of Pulsar, one on each worker.
3403
+ */
3404
+ const pulsarObjects = (cell, namespace, image) => {
3405
+ if (cell.pulsar === void 0) throw new Error(`${cell.name} has no broker`);
3406
+ const env = {
3407
+ SERVICE: PULSAR_SERVICE,
3408
+ TOPIC: PULSAR_TOPIC,
3409
+ LOOKUP: PULSAR_LOOKUP,
3410
+ EVERY_MS: String(valueIn(cell.workload.every, "ms")),
3411
+ TIMEOUT_MS: String(valueIn(cell.workload.timeout, "ms")),
3412
+ PROBE_MS: String(valueIn(cell.pulsar.probe, "ms")),
3413
+ PYTHONUNBUFFERED: "1"
3414
+ };
3415
+ return [{
3416
+ apiVersion: "v1",
3417
+ kind: "ConfigMap",
3418
+ metadata: {
3419
+ name: PRODUCER,
3420
+ namespace
3421
+ },
3422
+ data: { "producer.py": PRODUCER_SCRIPT }
3423
+ }, {
3424
+ apiVersion: "apps/v1",
3425
+ kind: "Deployment",
3426
+ metadata: {
3427
+ name: PRODUCER,
3428
+ namespace,
3429
+ labels: { app: PRODUCER }
3430
+ },
3431
+ spec: {
3432
+ replicas: cell.pulsar.clients,
3433
+ selector: { matchLabels: { app: PRODUCER } },
3434
+ template: {
3435
+ metadata: { labels: {
3436
+ app: PRODUCER,
3437
+ [LOG_LABEL]: "1"
3438
+ } },
3439
+ spec: {
3440
+ terminationGracePeriodSeconds: 0,
3441
+ topologySpreadConstraints: [{
3442
+ maxSkew: 1,
3443
+ topologyKey: "kubernetes.io/hostname",
3444
+ whenUnsatisfiable: "DoNotSchedule",
3445
+ labelSelector: { matchLabels: { app: PRODUCER } }
3446
+ }],
3447
+ volumes: [{
3448
+ name: "script",
3449
+ configMap: { name: PRODUCER }
3450
+ }],
3451
+ containers: [{
3452
+ name: PRODUCER,
3453
+ image,
3454
+ imagePullPolicy: "Never",
3455
+ command: ["python3", "/lab/producer.py"],
3456
+ volumeMounts: [{
3457
+ name: "script",
3458
+ mountPath: "/lab"
3459
+ }],
3460
+ env: [
3461
+ ...Object.entries(env).map(([name, value]) => ({
3462
+ name,
3463
+ value
3464
+ })),
3465
+ {
3466
+ name: "POD_NAME",
3467
+ valueFrom: { fieldRef: { fieldPath: "metadata.name" } }
3468
+ },
3469
+ {
3470
+ name: "NODE_NAME",
3471
+ valueFrom: { fieldRef: { fieldPath: "spec.nodeName" } }
3472
+ }
3473
+ ]
3474
+ }]
3475
+ }
3476
+ }
3477
+ }
3478
+ }];
3479
+ };
3480
+ /** This function gives the pods of the brokers, in the sequence of their names. */
3481
+ const brokersOf = async (cluster) => {
3482
+ return JSON.parse(await cluster.kubectl([
3483
+ "-n",
3484
+ PULSAR_NAMESPACE,
3485
+ "get",
3486
+ "pods",
3487
+ "-l",
3488
+ "component=broker",
3489
+ "-o",
3490
+ "json"
3491
+ ])).items.filter((pod) => pod.metadata.deletionTimestamp === void 0).map((pod) => ({
3492
+ pod: pod.metadata.name,
3493
+ node: pod.spec.nodeName ?? "",
3494
+ ip: pod.status?.podIP ?? "",
3495
+ ready: (pod.status?.containerStatuses ?? []).length > 0 && pod.status.containerStatuses.every((status) => status.ready)
3496
+ })).sort((a, b) => a.pod.localeCompare(b.pod));
3497
+ };
3498
+ /** This function gives the broker that a host of a lookup names. Its pod is the first label of the host, or its address is the host. */
3499
+ const brokerOfHost = (brokers, host) => brokers.find((broker) => host.split(".")[0] === broker.pod || host === broker.ip);
3500
+ /**
3501
+ * This function tells why the producers are not at rest, or gives the host of the owner of the
3502
+ * topic. At rest, each of `clients` producers had an answer in the last second and no failure in
3503
+ * the last 2 s. The last lookup of each one is in the last 2 s, and each names the same owner.
3504
+ */
3505
+ const ownerAtRest = (state, clients) => {
3506
+ if (state.sends.size !== clients) return `${state.sends.size} producers send, not ${clients}`;
3507
+ for (const [pod, send] of state.sends) {
3508
+ if (state.last - send.ok > 1e3) return `the producer ${pod} has no answer in the last second`;
3509
+ if (state.last - send.failed < 2e3) return `the producer ${pod} failed in the last 2 s`;
3510
+ }
3511
+ const owners = /* @__PURE__ */ new Set();
3512
+ for (const pod of state.sends.keys()) {
3513
+ const seen = state.owners.get(pod);
3514
+ if (seen === void 0 || seen.owner === null || state.last - seen.t > 2e3) return `the producer ${pod} has no lookup with an owner in the last 2 s`;
3515
+ owners.add(seen.owner);
3516
+ }
3517
+ if (owners.size !== 1) return `the lookups name ${[...owners].join(" and ")}`;
3518
+ return { owner: [...owners][0] };
3519
+ };
3520
+ const sleep$1 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
3521
+ /**
3522
+ * This function waits until each broker is ready and the producers are at rest (`ownerAtRest`). It
3523
+ * gives the broker that owns the topic and the host that the lookups name, or why it could not in `within` ms.
3524
+ */
3525
+ const holdPulsar = async (cluster, state, clients, within = 3e5) => {
3526
+ const end = Date.now() + within;
3527
+ let why = "the hold did not start";
3528
+ do {
3529
+ const brokers = await brokersOf(cluster);
3530
+ const busy = brokers.find((broker) => !broker.ready);
3531
+ const rest = ownerAtRest(state(), clients);
3532
+ if (brokers.length !== 2) why = `${brokers.length} brokers, not 2`;
3533
+ else if (busy !== void 0) why = `the broker ${busy.pod} is not ready`;
3534
+ else if (typeof rest === "string") why = rest;
3535
+ else {
3536
+ const broker = brokerOfHost(brokers, rest.owner);
3537
+ if (broker !== void 0) return {
3538
+ broker,
3539
+ host: rest.owner
3540
+ };
3541
+ why = `the owner ${rest.owner} is no broker`;
3542
+ }
3543
+ await sleep$1(500);
3544
+ } while (Date.now() < end);
3545
+ return why;
3546
+ };
3547
+ /**
3548
+ * This function installs Pulsar in a cluster, with its image on each node. A failed install gives
3549
+ * the pods of Pulsar and the tail of their logs.
3550
+ */
3551
+ const installPulsar = async (cluster, image) => {
3552
+ try {
3553
+ await helmInstall(cluster, "pulsar", PULSAR_CHART, PULSAR_NAMESPACE, pulsarValues(image), { createNamespace: true });
3554
+ } catch (error) {
3555
+ const state = async (args) => (await cluster.kubectl([
3556
+ "-n",
3557
+ PULSAR_NAMESPACE,
3558
+ ...args
3559
+ ]).catch((why) => String(why))).slice(-4e3);
3560
+ const pods = await state([
3561
+ "get",
3562
+ "pods",
3563
+ "-o",
3564
+ "wide"
3565
+ ]);
3566
+ const logs = await state([
3567
+ "logs",
3568
+ "-l",
3569
+ "app=pulsar",
3570
+ "--tail=30",
3571
+ "--prefix",
3572
+ "--all-containers"
3573
+ ]);
3574
+ throw new Error(`the install of Pulsar failed: ${String(error)}\nthe pods of Pulsar:\n${pods}\nthe logs:\n${logs}`);
3575
+ }
3576
+ };
3577
+ /** These words mark the lines of a broker that the record keeps: the sessions, the leader, the ownership and the topics. */
3578
+ const KEPT = /[Ss]ession|[Ee]xpired|[Ll]eader|[Oo]wnership|[Bb]undle|Loaded|[Ll]oading topic|Created new producer|Disconnect|Closed connection|znode|ManagedLedger.*(open|recover)/;
3579
+ /**
3580
+ * This function gives the record of Pulsar after a run. It has the brokers, and the kept lines of
3581
+ * each broker and of its last container, with the times of the kubelet.
3582
+ */
3583
+ const pulsarRecord = async (cluster) => {
3584
+ const brokers = await brokersOf(cluster);
3585
+ const logs = {};
3586
+ for (const broker of brokers) for (const previous of [false, true]) {
3587
+ const text = await cluster.kubectl([
3588
+ "-n",
3589
+ PULSAR_NAMESPACE,
3590
+ "logs",
3591
+ broker.pod,
3592
+ "-c",
3593
+ "pulsar-broker",
3594
+ "--since=300s",
3595
+ "--tail=6000",
3596
+ "--timestamps",
3597
+ ...previous ? ["--previous"] : []
3598
+ ]).catch((error) => String(error));
3599
+ logs[`${broker.pod}${previous ? ", the last container" : ""}`] = text.split("\n").filter((line) => KEPT.test(line)).slice(-80).join("\n").slice(-8e3);
3600
+ }
3601
+ return {
3602
+ brokers,
3603
+ logs
3604
+ };
3605
+ };
3606
+ //#endregion
3044
3607
  //#region src/lds.ts
3045
3608
  /**
3046
3609
  * This filter of RE2 selects the two counters of the listener manager of Envoy that an update of LDS
@@ -4187,9 +4750,11 @@ const hasRedis = (cell) => cell.declaration !== null && cell.declaration.redis !
4187
4750
  * This function gives the key of the cluster of a cell: its kube-proxy mode, its node count, and the
4188
4751
  * flavor of its mesh. A zone loss has clusters of its own, with a zone on each node.
4189
4752
  */
4190
- const clusterKey = (cell) => (cell.mesh !== void 0 ? `${cell.mode}-${cell.workers}-${cell.mesh.flavor}` : zoned(cell) ? `${cell.mode}-${cell.workers}-zones` : `${cell.mode}-${cell.workers}`) + (cell.vip === void 0 ? "" : cell.vip.announcer === "metallb" ? "-vip-mlb" : "-vip");
4191
- /** This function tells if a cell needs a cluster with zones: a zone loss. */
4192
- const zoned = (cell) => cell.trigger.kind === "zone-loss";
4753
+ const clusterKey = (cell) => (cell.mesh !== void 0 ? `${cell.mode}-${cell.workers}-${cell.mesh.flavor}` : zoned(cell) ? `${cell.mode}-${cell.workers}-zones` : `${cell.mode}-${cell.workers}`) + (cell.vip === void 0 ? "" : cell.vip.announcer !== "metallb" ? "-vip" : cell.vip.memberlist === void 0 ? "-vip-mlb" : `-mlb${valueIn(cell.vip.memberlist.probeInterval, "ms")}x${cell.vip.memberlist.suspicionMult}`) + (cell.pulsar === void 0 ? "" : "-pul");
4754
+ /** This function gives the name of the cluster of a cell in an environment. Kind permits 32 characters at most: lowercase letters, digits and dashes. */
4755
+ const clusterNameOf = (cell, env) => `lab-${clusterKey(cell)}-${env}`;
4756
+ /** This function tells if a cell needs a cluster with zones. A zone loss needs one, and so does a cell with settings of memberlist, that shares the cluster of its zone loss. */
4757
+ const zoned = (cell) => cell.trigger.kind === "zone-loss" || cell.vip?.memberlist !== void 0;
4193
4758
  /**
4194
4759
  * This tracker keeps the latest state of the cell from the lines: each pod, each slice and each
4195
4760
  * client. It also has, for each node, the backends of its rules and the hosts of its client sidecar.
@@ -4202,7 +4767,13 @@ var Tracker = class {
4202
4767
  clients = /* @__PURE__ */ new Map();
4203
4768
  /** This map is the backend of the last answer of each client, by client. */
4204
4769
  answers = /* @__PURE__ */ new Map();
4770
+ /** This map is the last send of each producer of a broker that got an answer, and the last that failed. */
4771
+ sends = /* @__PURE__ */ new Map();
4772
+ /** This map is the last lookup of the owner of the topic of each producer, and the host that it names. */
4773
+ owners = /* @__PURE__ */ new Map();
4205
4774
  last = 0;
4775
+ /** This time is the last line of a producer. */
4776
+ lastSend = 0;
4206
4777
  see = (line) => {
4207
4778
  if (typeof line.t === "number" && line.t > this.last && line.ev === "req") this.last = line.t;
4208
4779
  if (line.ev === "pod") {
@@ -4225,6 +4796,23 @@ var Tracker = class {
4225
4796
  else client.failed = Math.max(client.failed, request.t);
4226
4797
  this.clients.set(request.p, client);
4227
4798
  if (request.o === "ok" && request.b !== void 0) this.answers.set(request.p, request.b);
4799
+ } else if (line.ev === "send") {
4800
+ const pod = String(line["p"]);
4801
+ const send = this.sends.get(pod) ?? {
4802
+ ok: 0,
4803
+ failed: 0
4804
+ };
4805
+ if (line["o"] === "ok") send.ok = Math.max(send.ok, line.t);
4806
+ else send.failed = Math.max(send.failed, line.t);
4807
+ this.sends.set(pod, send);
4808
+ this.lastSend = Math.max(this.lastSend, line.t);
4809
+ } else if (line.ev === "lookup") {
4810
+ const pod = String(line["p"]);
4811
+ const owner = line["o"] === "ok" && typeof line["owner"] === "string" ? line["owner"] : null;
4812
+ if (line.t >= (this.owners.get(pod)?.t ?? 0)) this.owners.set(pod, {
4813
+ t: line.t,
4814
+ owner
4815
+ });
4228
4816
  }
4229
4817
  };
4230
4818
  servers() {
@@ -4405,6 +4993,18 @@ const deploy = async (lab, cluster, cell, namespace, images, vip) => {
4405
4993
  `daemonset/${CLIENT}`,
4406
4994
  "--timeout=300s"
4407
4995
  ]);
4996
+ if (cell.pulsar !== void 0) {
4997
+ if (images.pulsar === void 0) throw new Error(`${cell.name}: the cluster has no image of Pulsar`);
4998
+ await cluster.apply(pulsarObjects(cell, namespace, images.pulsar));
4999
+ await cluster.kubectl([
5000
+ "-n",
5001
+ namespace,
5002
+ "rollout",
5003
+ "status",
5004
+ `deployment/${PRODUCER}`,
5005
+ "--timeout=300s"
5006
+ ]);
5007
+ }
4408
5008
  if (cell.vip !== void 0) {
4409
5009
  if (vip === null) throw new Error(`${cell.name}: the cluster has no VIP`);
4410
5010
  const served = await vipServed(cluster, namespace, "lab", vip);
@@ -4488,7 +5088,7 @@ const probeThePods = async (ctx) => {
4488
5088
  if (key === ctx.probed) return;
4489
5089
  const netns = [];
4490
5090
  for (const pod of pods) {
4491
- const container = pod.labels["app"] === "lab" ? "app" : pod.labels["app"] === "client" ? CLIENT : REDIS;
5091
+ const container = pod.labels["app"] === "lab" ? "app" : pod.labels["app"] === "client" ? CLIENT : pod.labels["app"] === "producer" ? PRODUCER : REDIS;
4492
5092
  const pid = await ctx.lab.pidOf(pod.node, ctx.namespace, pod.name, container);
4493
5093
  netns.push(await ctx.lab.netnsOf(pod.node, pid));
4494
5094
  }
@@ -4671,6 +5271,35 @@ const doStep = async (ctx, step, replicas, background) => {
4671
5271
  pid: await lab.pidOf(target.node, namespace, target.pod, "app"),
4672
5272
  pod: null
4673
5273
  };
5274
+ if (which === "speaker") {
5275
+ const owner = lostNodeOf(cluster);
5276
+ const pod = JSON.parse(await cluster.kubectl([
5277
+ "-n",
5278
+ METALLB_NAMESPACE,
5279
+ "get",
5280
+ "pods",
5281
+ "-l",
5282
+ "app.kubernetes.io/component=speaker",
5283
+ "--field-selector",
5284
+ `spec.nodeName=${owner}`,
5285
+ "-o",
5286
+ "json"
5287
+ ])).items[0];
5288
+ if (pod === void 0) throw new Error(`no speaker of MetalLB is on ${owner}`);
5289
+ return {
5290
+ node: owner,
5291
+ pid: await lab.pidOf(owner, METALLB_NAMESPACE, pod.metadata.name, "speaker"),
5292
+ pod: null
5293
+ };
5294
+ }
5295
+ if (which === "broker") {
5296
+ if (ctx.broker === null) throw new Error(`${ctx.cell.name}: the run has no broker that owns the topic`);
5297
+ return {
5298
+ node: ctx.broker.node,
5299
+ pid: await lab.pidOf(ctx.broker.node, PULSAR_NAMESPACE, ctx.broker.pod, "pulsar-broker"),
5300
+ pod: null
5301
+ };
5302
+ }
4674
5303
  if (which === "redis") {
4675
5304
  const redis = [...ctx.tracker.pods.values()].find((pod) => pod.labels["app"] === "redis" && !pod.del);
4676
5305
  return {
@@ -4719,7 +5348,8 @@ const doStep = async (ctx, step, replicas, background) => {
4719
5348
  }
4720
5349
  case "signal": {
4721
5350
  if (step.target === "clients") return signalClients(ctx, step.signal);
4722
- const at = await where(step.target);
5351
+ const at = (step.target === "speaker" && step.signal === "SIGCONT" ? ctx.speaker : void 0) ?? await where(step.target);
5352
+ if (step.target === "speaker") ctx.speaker = step.signal === "SIGSTOP" ? at : void 0;
4723
5353
  return lab.act(at.node, {
4724
5354
  op: "signal",
4725
5355
  pid: at.pid,
@@ -4878,6 +5508,19 @@ const recordVip = async (ctx) => {
4878
5508
  macs
4879
5509
  });
4880
5510
  };
5511
+ /** This function records the brokers after a run, with the kept lines of each broker. It stamps the wall clock and the monotonic clock. */
5512
+ const recordPulsar = async (ctx) => {
5513
+ const wall = Date.now();
5514
+ const t = Number(process.hrtime.bigint()) / 1e6;
5515
+ ctx.lab.record({
5516
+ ev: "pulsar-record",
5517
+ t,
5518
+ wall,
5519
+ version: PULSAR_VERSION,
5520
+ owner: ctx.broker?.pod ?? null,
5521
+ ...await pulsarRecord(ctx.cluster)
5522
+ });
5523
+ };
4881
5524
  /** This function puts the replicas of a node first, and keeps the sequence of the others. */
4882
5525
  const lostFirst = (replicas, node) => [...replicas.filter((replica) => replica.node === node), ...replicas.filter((replica) => replica.node !== node)];
4883
5526
  /** This function tells if a run is over: the trigger is done, and the cell is at rest again. */
@@ -4957,10 +5600,25 @@ const runOnce = async (ctx, env, index, warmup, ours) => {
4957
5600
  await sleep(valueIn(announcer.settle, "ms") + valueIn(cell.workload.timeout, "ms") + 1e3);
4958
5601
  }
4959
5602
  if (await steady(ctx, 3e5) !== null) return empty("the cell was not at rest after the wait for a quiet VM", await lab.now());
5603
+ ctx.broker = null;
5604
+ if (cell.pulsar !== void 0) {
5605
+ const tracker = ctx.tracker;
5606
+ const held = await holdPulsar(cluster, () => ({
5607
+ owners: tracker.owners,
5608
+ sends: tracker.sends,
5609
+ last: tracker.lastSend
5610
+ }), cell.pulsar.clients);
5611
+ if (typeof held === "string") return empty(`the producers were not at rest: ${held}`, await lab.now());
5612
+ ctx.broker = {
5613
+ pod: held.broker.pod,
5614
+ node: held.broker.node,
5615
+ host: held.host
5616
+ };
5617
+ }
4960
5618
  const replicas = zoneLoss ? lostFirst(replicasOf(ctx), lostNodeOf(cluster)) : replicasOf(ctx);
4961
5619
  if (zoneLoss && cell.vip === void 0 && replicas[0]?.node !== lostNodeOf(cluster)) return empty(`no replica is on the node of the lost zone, ${lostNodeOf(cluster)}`, await lab.now());
4962
5620
  const restarts = new Map(ctx.tracker.servers().map((pod) => [pod.name, pod.cs.reduce((sum, status) => sum + status.rc, 0)]));
4963
- const target = cell.vip !== void 0 || hasRedis(cell) && [
5621
+ const target = cell.vip !== void 0 || cell.pulsar !== void 0 || hasRedis(cell) && [
4964
5622
  "rst",
4965
5623
  "blackhole",
4966
5624
  "freeze-process",
@@ -4968,7 +5626,7 @@ const runOnce = async (ctx, env, index, warmup, ours) => {
4968
5626
  "rollout",
4969
5627
  "latency"
4970
5628
  ].includes(cell.trigger.kind) ? null : replicas[0];
4971
- const paused = cell.template === "T07" && cell.trigger.kind === "freeze-process" ? cluster.nodes[1] ?? cluster.nodes[0] : zoneLoss ? lostNodeOf(cluster) : null;
5629
+ const paused = cell.template === "T07" && cell.trigger.kind === "freeze-process" && cell.vip === void 0 ? cluster.nodes[1] ?? cluster.nodes[0] : zoneLoss ? lostNodeOf(cluster) : null;
4972
5630
  const cleared = await lab.forgetHandshakes(ctx.clusterIp, 80);
4973
5631
  lab.take();
4974
5632
  for (const key of Object.keys(lab.dropped)) delete lab.dropped[key];
@@ -5061,6 +5719,7 @@ const runOnce = async (ctx, env, index, warmup, ours) => {
5061
5719
  const end = await lab.now();
5062
5720
  const timedOut = Date.now() >= limit;
5063
5721
  if (cell.vip !== void 0) await recordVip(ctx).catch((error) => notes.push(`the record of the VIP failed: ${String(error)}`));
5722
+ if (cell.pulsar !== void 0) await recordPulsar(ctx).catch((error) => notes.push(`the record of Pulsar failed: ${String(error)}`));
5064
5723
  for (const step of plan.restore) try {
5065
5724
  await doStep(ctx, step, replicas, background);
5066
5725
  } catch (error) {
@@ -5103,6 +5762,7 @@ const runOnce = async (ctx, env, index, warmup, ours) => {
5103
5762
  },
5104
5763
  target: stopped,
5105
5764
  paused,
5765
+ ...ctx.broker === null ? {} : { broker: ctx.broker.host },
5106
5766
  replicas,
5107
5767
  void: null,
5108
5768
  lines,
@@ -5147,7 +5807,8 @@ const writeResult = (out, result) => {
5147
5807
  voided: verdict.voided,
5148
5808
  failed: verdict.failed,
5149
5809
  median: verdict.median,
5150
- why: verdict.why
5810
+ why: verdict.why,
5811
+ substrate: substrateKey(substrateOf(result.manifests[0] ?? {}, result.at, "E2"))
5151
5812
  })}\n`, { flag: "a" });
5152
5813
  };
5153
5814
  /**
@@ -5185,19 +5846,21 @@ const runKindCells = async (cells, options) => {
5185
5846
  for (const [name, ref] of Object.entries(announcer.images)) pinned[name] = await localPin(ref);
5186
5847
  announcerImages.set(announcer.name, pinned);
5187
5848
  }
5849
+ const pulsarImage = cells.some((cell) => cell.pulsar !== void 0) ? await localPin(PULSAR_IMAGE) : void 0;
5188
5850
  const images = {
5189
5851
  lab: image,
5190
5852
  redis: redisImage,
5191
5853
  grpc: grpcImage,
5192
- mesh: meshImages
5854
+ mesh: meshImages,
5855
+ pulsar: pulsarImage
5193
5856
  };
5194
5857
  const groups = /* @__PURE__ */ new Map();
5195
5858
  for (const cell of cells) groups.set(clusterKey(cell), [...groups.get(clusterKey(cell)) ?? [], cell]);
5196
5859
  const results = [];
5197
- for (const [key, group] of groups) {
5860
+ for (const group of groups.values()) {
5198
5861
  const partial = /* @__PURE__ */ new Map();
5199
5862
  for (let env = 0; env < environments; env += 1) {
5200
- const s = scenario(`lab-${key}-${env}`, group[0].mode, group[0].workers, void 0, zoned(group[0]));
5863
+ const s = scenario(clusterNameOf(group[0], env), group[0].mode, group[0].workers, void 0, zoned(group[0]));
5201
5864
  const dir = join(options.work ?? join(tmpdir(), "systems-kind-work"), s.name);
5202
5865
  const mesh = group[0].mesh;
5203
5866
  options.log(`cluster ${s.name}: ${s.dataplane}, ${s.workers} workers${mesh === void 0 ? "" : `, Istio ${mesh.flavor}`}`);
@@ -5238,9 +5901,16 @@ const runKindCells = async (cells, options) => {
5238
5901
  for (const each of Object.values(pinned)) await cluster.load(each);
5239
5902
  const began = Date.now();
5240
5903
  vip = announcer.vipOf((await vipNetworkOf(cluster.network)).subnet, lostNodeOf(cluster), workersOf(cluster));
5241
- await announcer.install(cluster, pinned, lostNodeOf(cluster), workersOf(cluster), vip);
5904
+ await announcer.install(cluster, pinned, lostNodeOf(cluster), workersOf(cluster), vip, group[0].vip);
5242
5905
  options.log(` ${announcer.name} installed in ${Math.round((Date.now() - began) / 100) / 10} s, the VIP ${vip}`);
5243
5906
  }
5907
+ if (group[0].pulsar !== void 0) {
5908
+ if (pulsarImage === void 0) throw new Error(`${group[0].name}: the run has no image of Pulsar`);
5909
+ await cluster.load(pulsarImage);
5910
+ const began = Date.now();
5911
+ await installPulsar(cluster, pulsarImage);
5912
+ options.log(` Pulsar ${PULSAR_VERSION} installed in ${Math.round((Date.now() - began) / 100) / 10} s`);
5913
+ }
5244
5914
  lab = await openKindLab(cluster, image);
5245
5915
  const { GenericContainer } = await import("testcontainers");
5246
5916
  const Shared = class extends GenericContainer {
@@ -5279,7 +5949,8 @@ const runKindCells = async (cells, options) => {
5279
5949
  probed: "",
5280
5950
  listened: "",
5281
5951
  expected: cell.pod.replicas,
5282
- pids: /* @__PURE__ */ new Map()
5952
+ pids: /* @__PURE__ */ new Map(),
5953
+ broker: null
5283
5954
  };
5284
5955
  const share = Math.ceil(cell.runs / environments) - (env < cell.runs % environments || cell.runs % environments === 0 ? 0 : 1);
5285
5956
  const runs = [await runOnce(ctx, env, -1, true, ours)];
@@ -5294,13 +5965,15 @@ const runKindCells = async (cells, options) => {
5294
5965
  env,
5295
5966
  scenario: s,
5296
5967
  cluster: manifest,
5968
+ substrate: substrateOn("E2"),
5297
5969
  images: {
5298
5970
  lab: image,
5299
5971
  redis: redisImage,
5300
5972
  ...stackOf(cell) === "grpc" ? { grpc: grpcImage } : {},
5301
5973
  ...cell.mesh === void 0 ? {} : Object.fromEntries(MESH_IMAGES[cell.mesh.flavor].map((name) => [`istio-${name}`, ISTIO_IMAGES[name]])),
5302
5974
  ...s.cni === "cilium" ? Object.fromEntries(Object.entries(CILIUM_IMAGES).map(([name, ref]) => [`cilium-${name}`, ref])) : {},
5303
- ...cell.vip === void 0 ? {} : announcerOf(cell).images
5975
+ ...cell.vip === void 0 ? {} : announcerOf(cell).images,
5976
+ ...cell.pulsar === void 0 ? {} : { pulsar: PULSAR_IMAGE }
5304
5977
  }
5305
5978
  });
5306
5979
  entry.loads.push(...loads);
@@ -5368,14 +6041,17 @@ const estimate = (cell, environments = 2) => {
5368
6041
  "ztunnel-restart": 40,
5369
6042
  "certificate-expiry": hold + 20,
5370
6043
  "cannot-serve": hold + 20,
5371
- "zone-loss": hold + 45
6044
+ "zone-loss": hold + 45,
6045
+ ...cell.vip === void 0 ? {} : { "freeze-process": hold + 20 }
5372
6046
  };
5373
6047
  const after = observationAfter(cell);
5374
6048
  const observed = after === null ? 0 : hold + valueIn(after, "s");
5375
6049
  const closes = cell.prediction.kind === "ends" && cell.prediction.band !== null ? valueIn(cell.prediction.band.hi, "s") + 3 : 0;
5376
6050
  const quiet = cell.trigger.quiet === true ? hold + 10 : 0;
5377
- const each = 25 + (cell.vip === void 0 ? 0 : 30) + Math.max(observed, closes, quiet, trigger[cell.trigger.kind] ?? (cell.template === "T18" ? hold + 35 : 15 + valueIn(cell.pod.preStop, "s")));
5378
- return ((cell.mesh === void 0 ? 60 : 180) + (cell.vip === void 0 ? 0 : 60)) * environments + (cell.runs + environments) * each;
6051
+ const move = cell.vip === void 0 ? 0 : 30;
6052
+ const broker = cell.pulsar === void 0 ? 0 : 150;
6053
+ const each = 25 + move + broker + Math.max(observed, closes, quiet, trigger[cell.trigger.kind] ?? (cell.template === "T18" ? hold + 35 : 15 + valueIn(cell.pod.preStop, "s")));
6054
+ return ((cell.mesh === void 0 ? 60 : 180) + (cell.vip === void 0 ? 0 : 60) + (cell.pulsar === void 0 ? 0 : 240)) * environments + (cell.runs + environments) * each;
5379
6055
  };
5380
6056
  /**
5381
6057
  * This function gives an estimate of the time of a cell: its setup, then each run and each
@@ -5406,6 +6082,6 @@ const shardOf = (cells, index, count) => {
5406
6082
  return out;
5407
6083
  };
5408
6084
  //#endregion
5409
- export { ACCESS_LOG_FORMAT, ANNOUNCE_ANNOTATION, CHANNEL_USE, CILIUM_CHART, CILIUM_CONFIG_KEYS, CILIUM_IMAGES, CILIUM_VERSION, CILIUM_WORKLOADS, CLIENT, CLIENT_ACCOUNT, CLOSE_LEAD, CONTROLLER, DATAPLANES, DNS_PORT, DOCUMENTED, DRAIN_RULES, EDS_EVERY, FLAG_CHECK, FORMAT, FORMAT_V1, HBONE_PORT, HEALTH_PORT, HELM, ISTIOCTL, ISTIO_IMAGES, ISTIO_VERSION, KEPT_KERNEL, KIND, KIND_NOISE, KUBECTL, KUBE_PROXY_MODES, KUBE_VIP, KUBE_VIP_ANNOUNCER, KUBE_VIP_ARP_RATE, KUBE_VIP_ELECTION, KUBE_VIP_IMAGE, KUBE_VIP_LEASE, KUBE_VIP_VERSION, LAST_SLACK, LB_CLASS, LDS_EVERY, LDS_FILTER, LOG_LABEL, MEMBERLIST, MESH_IMAGES, MESH_WORKLOADS, METALLB_ANNOTATION, METALLB_ANNOUNCER, METALLB_CHART, METALLB_IMAGES, METALLB_NAMESPACE, METALLB_POOL, METALLB_SPAM, METALLB_VERSION, MIDDLEBOX, NETEM_DRAIN, NETNS_DEFAULTS, NODE_IMAGES, NODE_LIFECYCLE, O04, O09, O10, O16, OUTSIDE, POD_LINK, POLICY, PORT, QUIET_LEAD, REDIS, REDIS_IMAGE, RELATIVE, ROUTE, RULES_EVERY, SERVER, SPREAD_LEAD, STAGES, SYSCTL, TCP_STATE, VIP_ANNOTATION, VIP_LABEL, WATCHDOG, ZONE_KEPT, ZONE_LABEL, ZONE_LOST, ZTUNNEL_PODS, announcementsOf, announcerOf, arpMacOf, backendsAt, backendsOf, backlogEnd, bandOf, byClients, byNode, cacheFolder, calibrationOf, capOf, cellsTable, certificateArgs, checkRun, cidrOverlap, cidrRange, ciliumBackendsOf, ciliumMode, ciliumValues, clientObjects, clusterFilter, compose, createCluster, cyclesOf, deletes, deletionOf, deletionWindow, deletionsOf, describeCell, dnsCommands, drainBand, dropOf, emptyOf, ensureChart, ensurePinnedChart, ensureTool, estimatedTime, factsOf, failuresOf, faultSpan, followsZtunnel, goDuration, helmInstall, holdMetallb, holdVip, hostsOf, installCilium, installKubeVip, installMesh, installMetallb, intakeAfter, intakeOf, isKubeProxyMode, isTunnelState, istioOperator, judge, judgeKind, keptKernel, kernelCounts, kindCellRecordSchema, kindCellRecordV1Schema, kindConfig, kubeProxyPatch, kubeVipObjects, kubeVipPods, kubeVipReady, kubeletBand, labImage, labSource, latchBand, leafCertificates, learnWindow, lintCell, listenerCountsOf, localPin, macsAt, medianInterval, medianText, meshKey, metallbObjects, metallbRecord, metallbValues, metallbVipOf, middle, middleboxCommands, namesService, namespaceObject, namespaceOf, nodeLossBand, nodeNames, nodeReturnBand, observationAfter, observationEnd, openKindLab, outboundCluster, outcomesOf, overshootBand, ownerOrder, pausedWindow, periodOf, placementOf, planOf, platformKey, podsAt, podsOf, policyObject, pollsListeners, predictionText, probeOf, propagationOf, propagationSource, quantileOf, readRecord, recordsOf, redisObjects, referenceOf, removalBand, requestBand, requestsOf, restartBand, restartOf, restartsAt, reusesReset, rulesOf, runKindCells, scenario, scenarioOf, scenarioProblems, seconds, secondsOf, serverCluster, serverObjects, sessionsAt, setZtunnelEnv, sha256, shardOf, sharedGate, showBand, signalOf, signalsOf, sliceDropOf, sliceEmptyOf, sliceReadyAt, sliceReadyOf, slicesOf, socketsOf, speakersReady, stackOf, stageLabImage, stallRemovalBand, stopOf, stoppingOf, suspicionOf, sysctlCommand, tarEntry, timeSpread, toolPath, transitionOf, tunnelsOf, unitOf, updatesOf, upgrade, upgradeCell, verdictsOf, vipLeaseOf, vipNetworkOf, vipOf, vipServed, vipService, writeResult, zoneOf, ztunnelCertificates, ztunnelEnvArgs, ztunnelPodsOf, ztunnelReplaced };
6085
+ export { ACCESS_LOG_FORMAT, ANNOUNCE_ANNOTATION, CHANNEL_USE, CILIUM_CHART, CILIUM_CONFIG_KEYS, CILIUM_IMAGES, CILIUM_VERSION, CILIUM_WORKLOADS, CLIENT, CLIENT_ACCOUNT, CLOSE_LEAD, CONTROLLER, DATAPLANES, DNS_PORT, DOCUMENTED, DRAIN_RULES, EDS_EVERY, FLAG_CHECK, FORMAT, FORMAT_V1, HBONE_PORT, HEALTH_PORT, HELM, ISTIOCTL, ISTIO_IMAGES, ISTIO_VERSION, KEPT_KERNEL, KIND, KIND_NOISE, KUBECTL, KUBE_PROXY_MODES, KUBE_VIP, KUBE_VIP_ANNOUNCER, KUBE_VIP_ARP_RATE, KUBE_VIP_ELECTION, KUBE_VIP_IMAGE, KUBE_VIP_LEASE, KUBE_VIP_VERSION, LAST_SLACK, LB_CLASS, LDS_EVERY, LDS_FILTER, LOG_LABEL, MEMBERLIST, MEMBERLIST_FILE, MESH_IMAGES, MESH_WORKLOADS, METALLB_ANNOTATION, METALLB_ANNOUNCER, METALLB_CHART, METALLB_IMAGES, METALLB_NAMESPACE, METALLB_POOL, METALLB_SPAM, METALLB_VERSION, MIDDLEBOX, NETEM_DRAIN, NETNS_DEFAULTS, NODE_IMAGES, NODE_LIFECYCLE, O04, O09, O10, O16, OUTSIDE, PING, POD_LINK, POLICY, PORT, PRODUCER, PRODUCER_BACKOFF, PRODUCER_SCRIPT, PULSAR_CHART, PULSAR_IMAGE, PULSAR_LOOKUP, PULSAR_NAMESPACE, PULSAR_SERVICE, PULSAR_TOPIC, PULSAR_VERSION, QUIET_LEAD, REDIS, REDIS_IMAGE, RELATIVE, ROUTE, RULES_EVERY, SERVER, SESSION, SPREAD_LEAD, STAGES, SYSCTL, TCP_STATE, TICK, VIP_ANNOTATION, VIP_LABEL, WATCHDOG, ZONE_KEPT, ZONE_LABEL, ZONE_LOST, ZTUNNEL_PODS, announcementsOf, announcerOf, arpMacOf, backendsAt, backendsOf, backlogEnd, bandOf, brokerOfHost, brokersOf, byClients, byNode, cacheFolder, calibrationOf, capOf, cellsTable, certificateArgs, checkRun, cidrOverlap, cidrRange, ciliumBackendsOf, ciliumMode, ciliumValues, clientObjects, clusterFilter, clusterNameOf, compose, createCluster, cyclesOf, deletes, deletionOf, deletionWindow, deletionsOf, describeCell, dnsCommands, drainBand, dropOf, emptyOf, ensureChart, ensurePinnedChart, ensureTool, estimatedTime, factsOf, failuresOf, faultSpan, followsZtunnel, goDuration, helmInstall, holdMetallb, holdPulsar, holdVip, hostsOf, installCilium, installKubeVip, installMesh, installMetallb, installPulsar, intakeAfter, intakeOf, isKubeProxyMode, isTunnelState, istioOperator, judge, judgeKind, judgeReconnects, keptKernel, kernelCounts, kindCellRecordSchema, kindCellRecordV1Schema, kindConfig, kubeProxyPatch, kubeVipObjects, kubeVipPods, kubeVipReady, kubeletBand, labImage, labSource, latchBand, latestReturn, leafCertificates, learnWindow, lintCell, listenerCountsOf, localPin, macsAt, medianInterval, medianText, memberlistFileOf, memberlistTermsOf, meshKey, metallbObjects, metallbRecord, metallbValues, metallbVipOf, middle, middleboxCommands, namesService, namespaceObject, namespaceOf, nodeLossBand, nodeNames, nodeReturnBand, observationAfter, observationEnd, openKindLab, outboundCluster, outcomesOf, overshootBand, ownerAtRest, ownerOrder, pausedWindow, periodOf, placementOf, planOf, platformKey, podsAt, podsOf, policyObject, pollsListeners, predictionText, probeOf, propagationOf, propagationSource, pulsarObjects, pulsarRecord, pulsarValues, quantileOf, readRecord, recordsOf, redisObjects, referenceOf, removalBand, requestBand, requestsOf, restartBand, restartOf, restartsAt, reusesReset, rulesOf, runKindCells, scenario, scenarioOf, scenarioProblems, seconds, secondsOf, serverCluster, serverObjects, sessionsAt, setZtunnelEnv, sha256, shardOf, sharedGate, showBand, signalOf, signalsOf, sliceDropOf, sliceEmptyOf, sliceReadyAt, sliceReadyOf, slicesOf, socketsOf, speakersReady, stackOf, stageLabImage, stallRemovalBand, stopOf, stoppingOf, suspicionOf, sysctlCommand, tarEntry, timeSpread, toolPath, transitionOf, triesOf, tunnelsOf, unitOf, updatesOf, upgrade, upgradeCell, verdictsOf, vipLeaseOf, vipNetworkOf, vipOf, vipServed, vipService, writeResult, zoneOf, ztunnelCertificates, ztunnelEnvArgs, ztunnelPodsOf, ztunnelReplaced };
5410
6086
 
5411
6087
  //# sourceMappingURL=index.js.map