@mark1russell7/kind 0.13.4 → 0.14.1
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 +6 -0
- package/build/index.d.ts +233 -11
- package/build/index.js +873 -64
- package/build/index.js.map +1 -1
- package/build/{judge-BZFTT0RL.d.ts → judge-DLn2PLxb.d.ts} +193 -4
- package/build/{judge-DKrgfhAt.js → judge-DjtNaAVO.js} +375 -11
- package/build/judge-DjtNaAVO.js.map +1 -0
- package/build/judge.d.ts +2 -2
- package/build/judge.js +1 -1
- package/image/agent.mjs +15 -0
- package/image/npm-lock.json +22 -0
- package/image/package.json +1 -0
- package/package.json +3 -3
- package/build/judge-DKrgfhAt.js.map +0 -1
package/build/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { $ as
|
|
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-DjtNaAVO.js";
|
|
2
2
|
import { valueIn } from "@mark1russell7/quantity";
|
|
3
|
+
import { median } from "@mark1russell7/rigor-statistics";
|
|
3
4
|
import { chmodSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
4
5
|
import { dirname, join } from "node:path";
|
|
5
6
|
import { execFile, execFileSync } from "node:child_process";
|
|
@@ -643,16 +644,18 @@ const createCluster = async (scenario, dir) => {
|
|
|
643
644
|
};
|
|
644
645
|
//#endregion
|
|
645
646
|
//#region src/helm.ts
|
|
646
|
-
/**
|
|
647
|
-
|
|
647
|
+
/**
|
|
648
|
+
* This function gives the path of a chart in the cache, after a download when the cache does not have it.
|
|
649
|
+
* It refuses a download whose sha256 is not the pinned value. A file in the cache with a different
|
|
650
|
+
* sha256 (a torn write) is downloaded again, and each write replaces the file in one step.
|
|
651
|
+
*/
|
|
652
|
+
const ensurePinnedChart = async (chart, cache = cacheFolder(), get = download) => {
|
|
648
653
|
const path = join(cache, "charts", `${chart.name}-${chart.version}.tgz`);
|
|
649
|
-
|
|
654
|
+
if (existsSync(path) && sha256(readFileSync(path)) === chart.sha256) return path;
|
|
655
|
+
const bytes = await get(chart.url);
|
|
650
656
|
const found = sha256(bytes);
|
|
651
657
|
if (found !== chart.sha256) throw new Error(`the chart ${chart.name} ${chart.version} has the sha256 ${found}, not the pinned ${chart.sha256}`);
|
|
652
|
-
|
|
653
|
-
mkdirSync(join(cache, "charts"), { recursive: true });
|
|
654
|
-
writeFileSync(path, bytes);
|
|
655
|
-
}
|
|
658
|
+
writeWhole(path, bytes, false);
|
|
656
659
|
return path;
|
|
657
660
|
};
|
|
658
661
|
/**
|
|
@@ -1069,7 +1072,8 @@ const dnsCommands = (on) => {
|
|
|
1069
1072
|
};
|
|
1070
1073
|
/**
|
|
1071
1074
|
* This function gives the commands of the injector that put a stateful middlebox in its network
|
|
1072
|
-
* namespace, with each peer, on the local port `port`. With `idle` null, they remove
|
|
1075
|
+
* namespace, with each peer, on the local port `port`. With `idle` null, they remove the parts of these
|
|
1076
|
+
* peers. A run removes what the table of the namespace holds (`middleboxRemoval`).
|
|
1073
1077
|
*
|
|
1074
1078
|
* The middlebox forgets a flow of the port after `idle` with no segment. Then it drops each later
|
|
1075
1079
|
* segment of it: the silent-drop form of the router of the harness. Conntrack stays as it is. The
|
|
@@ -1201,6 +1205,38 @@ const middleboxCommands = (peers, idle, port) => {
|
|
|
1201
1205
|
];
|
|
1202
1206
|
};
|
|
1203
1207
|
/**
|
|
1208
|
+
* This function gives the commands that remove a middlebox from a network namespace, from the filter
|
|
1209
|
+
* table that `iptables-save -c` gives for it. An empty list means that the namespace has no part of it.
|
|
1210
|
+
*
|
|
1211
|
+
* The commands remove each part that the table holds, and only those. The lab stops at the first
|
|
1212
|
+
* command that fails. Thus a removal from a list of the peers stops at a jump that is not there, and
|
|
1213
|
+
* its chains stay. For example, a set that failed half way added no jumps, or a client pod has a new
|
|
1214
|
+
* address. Then the next set of the namespace fails at its `-N`.
|
|
1215
|
+
*/
|
|
1216
|
+
const middleboxRemoval = (saved) => {
|
|
1217
|
+
const chains = [MIDDLEBOX.input, MIDDLEBOX.output];
|
|
1218
|
+
const lines = saved.split("\n").map((line) => line.trim().replace(/^\[\d+:\d+\]\s+/, ""));
|
|
1219
|
+
const present = chains.filter((chain) => lines.some((line) => line.startsWith(`:${chain} `)));
|
|
1220
|
+
const jumps = lines.map((line) => line.split(/\s+/)).filter((words) => words[0] === "-A" && (words[1] === "INPUT" || words[1] === "OUTPUT") && chains.some((chain) => words.at(-1) === chain && words.at(-2) === "-j")).map((words) => [
|
|
1221
|
+
"iptables",
|
|
1222
|
+
"-D",
|
|
1223
|
+
...words.slice(1)
|
|
1224
|
+
]);
|
|
1225
|
+
return [
|
|
1226
|
+
...present.map((chain) => [
|
|
1227
|
+
"iptables",
|
|
1228
|
+
"-F",
|
|
1229
|
+
chain
|
|
1230
|
+
]),
|
|
1231
|
+
...jumps,
|
|
1232
|
+
...present.map((chain) => [
|
|
1233
|
+
"iptables",
|
|
1234
|
+
"-X",
|
|
1235
|
+
chain
|
|
1236
|
+
])
|
|
1237
|
+
];
|
|
1238
|
+
};
|
|
1239
|
+
/**
|
|
1204
1240
|
* This function gives the target of a reset or a black hole of a cell. In T07 and T18 it is Redis,
|
|
1205
1241
|
* with the replicas as peers. In T15 the fault is on the mesh leg between the client pods and the
|
|
1206
1242
|
* replicas. It is a rule in each replica, with the client pods as peers. A partition (`partition`)
|
|
@@ -1375,7 +1411,7 @@ const stepsOf = (cell) => {
|
|
|
1375
1411
|
case "crash": return {
|
|
1376
1412
|
inject: [{
|
|
1377
1413
|
do: "signal",
|
|
1378
|
-
target: "server",
|
|
1414
|
+
target: cell.pulsar === void 0 ? "server" : "broker",
|
|
1379
1415
|
signal: "SIGKILL"
|
|
1380
1416
|
}],
|
|
1381
1417
|
hold: null,
|
|
@@ -1393,6 +1429,24 @@ const stepsOf = (cell) => {
|
|
|
1393
1429
|
restore: []
|
|
1394
1430
|
};
|
|
1395
1431
|
case "freeze-process":
|
|
1432
|
+
if (cell.vip !== void 0) return {
|
|
1433
|
+
inject: [{
|
|
1434
|
+
do: "signal",
|
|
1435
|
+
target: "speaker",
|
|
1436
|
+
signal: "SIGSTOP"
|
|
1437
|
+
}],
|
|
1438
|
+
hold: needs(trigger.hold, "a hold, the time that the speaker is frozen"),
|
|
1439
|
+
heal: [{
|
|
1440
|
+
do: "signal",
|
|
1441
|
+
target: "speaker",
|
|
1442
|
+
signal: "SIGCONT"
|
|
1443
|
+
}],
|
|
1444
|
+
restore: [{
|
|
1445
|
+
do: "signal",
|
|
1446
|
+
target: "speaker",
|
|
1447
|
+
signal: "SIGCONT"
|
|
1448
|
+
}]
|
|
1449
|
+
};
|
|
1396
1450
|
if (cell.template === "T07") return {
|
|
1397
1451
|
inject: [
|
|
1398
1452
|
{
|
|
@@ -1814,7 +1868,7 @@ const deletes = (cell) => [
|
|
|
1814
1868
|
"rolling-update",
|
|
1815
1869
|
"node-drain",
|
|
1816
1870
|
"scale-in"
|
|
1817
|
-
].includes(cell.trigger.kind) || cell.template === "T07" && cell.trigger.kind === "freeze-process";
|
|
1871
|
+
].includes(cell.trigger.kind) || cell.template === "T07" && cell.trigger.kind === "freeze-process" && cell.vip === void 0;
|
|
1818
1872
|
//#endregion
|
|
1819
1873
|
//#region src/image.ts
|
|
1820
1874
|
/** This folder is the root of the package, and its `image/` folder has the files of the image. */
|
|
@@ -2511,7 +2565,7 @@ const vipOf = (subnet) => {
|
|
|
2511
2565
|
0
|
|
2512
2566
|
].map((shift) => Math.floor(address / 2 ** shift) % 256).join(".");
|
|
2513
2567
|
};
|
|
2514
|
-
const sleep$
|
|
2568
|
+
const sleep$3 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
2515
2569
|
/** This function gives the pods of kube-vip that are not deleted. */
|
|
2516
2570
|
const kubeVipPods = async (cluster) => {
|
|
2517
2571
|
return JSON.parse(await cluster.kubectl([
|
|
@@ -2540,7 +2594,7 @@ const until$1 = async (within, check) => {
|
|
|
2540
2594
|
const end = Date.now() + within;
|
|
2541
2595
|
let why = await check();
|
|
2542
2596
|
while (why !== null && Date.now() < end) {
|
|
2543
|
-
await sleep$
|
|
2597
|
+
await sleep$3(500);
|
|
2544
2598
|
why = await check();
|
|
2545
2599
|
}
|
|
2546
2600
|
return why;
|
|
@@ -2657,8 +2711,8 @@ const vipServed = (cluster, namespace, service, vip, within = 12e4) => until$1(w
|
|
|
2657
2711
|
* a usable speaker (speaker/layer2_controller.go, lines 83–129). A usable speaker is a member of
|
|
2658
2712
|
* memberlist that is not dead (internal/speakerlist/speakerlist.go, lines 303–320).
|
|
2659
2713
|
* - The speakers start memberlist from `DefaultLANConfig` (speakerlist.go, line 72). A file
|
|
2660
|
-
* /etc/metallb/memberlist/config.json replaces that config (lines 77–84).
|
|
2661
|
-
* such file
|
|
2714
|
+
* /etc/metallb/memberlist/config.json replaces that config (lines 77–84). Without settings of
|
|
2715
|
+
* memberlist, the install makes no such file. Then each value is the default of memberlist v0.5.1.
|
|
2662
2716
|
* - Each event of memberlist (a join, a leave or an update) calls `ForceSync` (speakerlist.go, lines
|
|
2663
2717
|
* 339–349). `ForceSync` reconciles each Service at once, with no delay (internal/k8s/k8s.go, lines
|
|
2664
2718
|
* 181–184, and internal/k8s/controllers/service_controller_reload.go, lines 62–118).
|
|
@@ -2719,6 +2773,60 @@ const MEMBERLIST = {
|
|
|
2719
2773
|
unit: "ms"
|
|
2720
2774
|
}
|
|
2721
2775
|
};
|
|
2776
|
+
/**
|
|
2777
|
+
* These are the values of the config file of memberlist, as `DefaultLANConfig` of v0.5.1 gives them
|
|
2778
|
+
* (config.go, lines 302–341). The speaker needs each field of the file, and it reads a time as a
|
|
2779
|
+
* duration of Go (speakerlist.go, lines 355–432). The chart mounts the ConfigMap
|
|
2780
|
+
* `metallb-memberlist-config` at /etc/metallb/memberlist, if the ConfigMap exists
|
|
2781
|
+
* (charts/metallb/templates/speaker.yaml, lines 195–198 and 391–392). The key is `config.json`.
|
|
2782
|
+
*/
|
|
2783
|
+
const MEMBERLIST_FILE = {
|
|
2784
|
+
TCPTimeout: "10s",
|
|
2785
|
+
IndirectChecks: 3,
|
|
2786
|
+
RetransmitMult: 4,
|
|
2787
|
+
SuspicionMult: 4,
|
|
2788
|
+
SuspicionMaxTimeoutMult: 6,
|
|
2789
|
+
PushPullInterval: "30s",
|
|
2790
|
+
ProbeTimeout: "500ms",
|
|
2791
|
+
ProbeInterval: "1s",
|
|
2792
|
+
DisableTcpPings: false,
|
|
2793
|
+
AwarenessMaxMultiplier: 8,
|
|
2794
|
+
GossipNodes: 3,
|
|
2795
|
+
GossipInterval: "200ms",
|
|
2796
|
+
GossipToTheDeadTime: "30s",
|
|
2797
|
+
GossipVerifyIncoming: true,
|
|
2798
|
+
GossipVerifyOutgoing: true,
|
|
2799
|
+
EnableCompression: true,
|
|
2800
|
+
HandoffQueueDepth: 1024,
|
|
2801
|
+
UDPBufferSize: 1400,
|
|
2802
|
+
QueueCheckInterval: "30s"
|
|
2803
|
+
};
|
|
2804
|
+
/** This name is the ConfigMap of the config file of memberlist. */
|
|
2805
|
+
const MEMBERLIST_CONFIG = "metallb-memberlist-config";
|
|
2806
|
+
/** This function gives a time as a duration of Go in whole milliseconds, for example "500ms". */
|
|
2807
|
+
const goDuration$1 = (time) => {
|
|
2808
|
+
const ms = valueIn(time, "ms");
|
|
2809
|
+
if (!Number.isInteger(ms) || ms <= 0) throw new RangeError(`a time of memberlist is a whole count of milliseconds above zero, not ${ms} ms`);
|
|
2810
|
+
return `${ms}ms`;
|
|
2811
|
+
};
|
|
2812
|
+
/** This function gives the config file of memberlist with `settings`. Each other value is the default of `MEMBERLIST_FILE`. */
|
|
2813
|
+
const memberlistFileOf = (settings) => {
|
|
2814
|
+
if (!Number.isInteger(settings.suspicionMult) || settings.suspicionMult < 1) throw new RangeError(`SuspicionMult is a whole number of 1 or more, not ${settings.suspicionMult}`);
|
|
2815
|
+
if (valueIn(settings.probeTimeout, "ms") >= valueIn(settings.probeInterval, "ms")) throw new RangeError("the probe timeout of memberlist is less than its probe interval");
|
|
2816
|
+
return {
|
|
2817
|
+
...MEMBERLIST_FILE,
|
|
2818
|
+
ProbeInterval: goDuration$1(settings.probeInterval),
|
|
2819
|
+
ProbeTimeout: goDuration$1(settings.probeTimeout),
|
|
2820
|
+
SuspicionMult: settings.suspicionMult
|
|
2821
|
+
};
|
|
2822
|
+
};
|
|
2823
|
+
/** This function gives the terms of memberlist with `settings`, or the defaults (`MEMBERLIST`) without them. */
|
|
2824
|
+
const memberlistTermsOf = (settings) => settings === void 0 ? MEMBERLIST : {
|
|
2825
|
+
...MEMBERLIST,
|
|
2826
|
+
probeInterval: settings.probeInterval,
|
|
2827
|
+
probeTimeout: settings.probeTimeout,
|
|
2828
|
+
suspicionMult: settings.suspicionMult
|
|
2829
|
+
};
|
|
2722
2830
|
/** 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
2831
|
const METALLB_SPAM = {
|
|
2724
2832
|
every: {
|
|
@@ -2829,13 +2937,13 @@ const metallbVipOf = (subnet, owner, nodes) => {
|
|
|
2829
2937
|
}
|
|
2830
2938
|
throw new Error(`the subnet ${subnet} has no VIP that ${owner} owns`);
|
|
2831
2939
|
};
|
|
2832
|
-
const sleep$
|
|
2940
|
+
const sleep$2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
2833
2941
|
/** This function waits until a condition gives null, and gives null, or the last reason after `within` ms. */
|
|
2834
2942
|
const until = async (within, check) => {
|
|
2835
2943
|
const end = Date.now() + within;
|
|
2836
2944
|
let why = await check();
|
|
2837
2945
|
while (why !== null && Date.now() < end) {
|
|
2838
|
-
await sleep$
|
|
2946
|
+
await sleep$2(500);
|
|
2839
2947
|
why = await check();
|
|
2840
2948
|
}
|
|
2841
2949
|
return why;
|
|
@@ -2902,8 +3010,12 @@ const holdMetallb = async (cluster, owner, nodes, namespace, service, within = 1
|
|
|
2902
3010
|
* and a pool of the VIP. The webhook of the controller checks the pool, thus the function tries the
|
|
2903
3011
|
* pool again until the webhook answers. A failed install gives the pods of MetalLB and the tail of
|
|
2904
3012
|
* their logs.
|
|
3013
|
+
*
|
|
3014
|
+
* With `memberlist`, the ConfigMap of the config file comes before the speakers. Then the function
|
|
3015
|
+
* checks that each speaker logged the load of the file (speakerlist.go, line 393). Without it, the
|
|
3016
|
+
* function checks that the ConfigMap does not exist.
|
|
2905
3017
|
*/
|
|
2906
|
-
const installMetallb = async (cluster, images, nodes, vip) => {
|
|
3018
|
+
const installMetallb = async (cluster, images, nodes, vip, memberlist) => {
|
|
2907
3019
|
try {
|
|
2908
3020
|
for (const node of nodes) await cluster.kubectl([
|
|
2909
3021
|
"label",
|
|
@@ -2912,24 +3024,66 @@ const installMetallb = async (cluster, images, nodes, vip) => {
|
|
|
2912
3024
|
`${VIP_LABEL}=1`,
|
|
2913
3025
|
"--overwrite"
|
|
2914
3026
|
]);
|
|
3027
|
+
if (memberlist !== void 0) {
|
|
3028
|
+
await cluster.apply([{
|
|
3029
|
+
apiVersion: "v1",
|
|
3030
|
+
kind: "Namespace",
|
|
3031
|
+
metadata: { name: METALLB_NAMESPACE }
|
|
3032
|
+
}]);
|
|
3033
|
+
await cluster.apply([{
|
|
3034
|
+
apiVersion: "v1",
|
|
3035
|
+
kind: "ConfigMap",
|
|
3036
|
+
metadata: {
|
|
3037
|
+
name: MEMBERLIST_CONFIG,
|
|
3038
|
+
namespace: METALLB_NAMESPACE
|
|
3039
|
+
},
|
|
3040
|
+
data: { "config.json": JSON.stringify(memberlistFileOf(memberlist)) }
|
|
3041
|
+
}]);
|
|
3042
|
+
}
|
|
2915
3043
|
await helmInstall(cluster, "metallb", METALLB_CHART, METALLB_NAMESPACE, metallbValues(images), { createNamespace: true });
|
|
2916
3044
|
let failure = null;
|
|
2917
3045
|
const end = Date.now() + 18e4;
|
|
2918
3046
|
do {
|
|
2919
3047
|
failure = await cluster.apply(metallbObjects(vip)).then(() => null, (error) => error);
|
|
2920
|
-
if (failure !== null) await sleep$
|
|
3048
|
+
if (failure !== null) await sleep$2(2e3);
|
|
2921
3049
|
} while (failure !== null && Date.now() < end);
|
|
2922
3050
|
if (failure !== null) throw failure;
|
|
2923
|
-
if (
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
3051
|
+
if (memberlist === void 0) {
|
|
3052
|
+
if ((await cluster.kubectl([
|
|
3053
|
+
"-n",
|
|
3054
|
+
"metallb-system",
|
|
3055
|
+
"get",
|
|
3056
|
+
"configmap",
|
|
3057
|
+
MEMBERLIST_CONFIG,
|
|
3058
|
+
"--ignore-not-found",
|
|
3059
|
+
"-o",
|
|
3060
|
+
"name"
|
|
3061
|
+
])).trim() !== "") throw new Error(`the ConfigMap ${MEMBERLIST_CONFIG} is there: memberlist would not use its defaults`);
|
|
3062
|
+
} else {
|
|
3063
|
+
const loaded = await until(12e4, async () => {
|
|
3064
|
+
const pods = JSON.parse(await cluster.kubectl([
|
|
3065
|
+
"-n",
|
|
3066
|
+
METALLB_NAMESPACE,
|
|
3067
|
+
"get",
|
|
3068
|
+
"pods",
|
|
3069
|
+
"-l",
|
|
3070
|
+
"app.kubernetes.io/component=speaker",
|
|
3071
|
+
"-o",
|
|
3072
|
+
"json"
|
|
3073
|
+
]));
|
|
3074
|
+
if (pods.items.length !== nodes.length) return `${pods.items.length} speakers, not ${nodes.length}`;
|
|
3075
|
+
for (const pod of pods.items) if (!(await cluster.kubectl([
|
|
3076
|
+
"-n",
|
|
3077
|
+
"metallb-system",
|
|
3078
|
+
"logs",
|
|
3079
|
+
pod.metadata.name,
|
|
3080
|
+
"-c",
|
|
3081
|
+
"speaker"
|
|
3082
|
+
]).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`;
|
|
3083
|
+
return null;
|
|
3084
|
+
});
|
|
3085
|
+
if (loaded !== null) throw new Error(loaded);
|
|
3086
|
+
}
|
|
2933
3087
|
} catch (error) {
|
|
2934
3088
|
const state = async (args) => (await cluster.kubectl([
|
|
2935
3089
|
"-n",
|
|
@@ -2954,7 +3108,7 @@ const installMetallb = async (cluster, images, nodes, vip) => {
|
|
|
2954
3108
|
}
|
|
2955
3109
|
};
|
|
2956
3110
|
/** 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/;
|
|
3111
|
+
const KEPT$1 = /node event|Marking|Suspect|Refuting|serviceAnnounced|serviceWithdrawn|memberlist/;
|
|
2958
3112
|
/**
|
|
2959
3113
|
* This function gives the record of MetalLB after a run: the nodes that announce the Service, and
|
|
2960
3114
|
* some lines of each speaker. The lines are about memberlist and the announces, with the times of the kubelet.
|
|
@@ -2974,7 +3128,7 @@ const metallbRecord = async (cluster, namespace, service) => {
|
|
|
2974
3128
|
"--tail=4000",
|
|
2975
3129
|
"--prefix",
|
|
2976
3130
|
"--timestamps"
|
|
2977
|
-
]).catch((error) => String(error))).split("\n").filter((line) => KEPT.test(line)).slice(-
|
|
3131
|
+
]).catch((error) => String(error))).split("\n").filter((line) => KEPT$1.test(line)).slice(-600).join("\n").slice(-9e4)
|
|
2978
3132
|
};
|
|
2979
3133
|
};
|
|
2980
3134
|
//#endregion
|
|
@@ -3025,10 +3179,10 @@ const METALLB_ANNOUNCER = {
|
|
|
3025
3179
|
},
|
|
3026
3180
|
settle: METALLB_SPAM.for,
|
|
3027
3181
|
vipOf: (subnet, owner, nodes) => metallbVipOf(subnet, owner, nodes),
|
|
3028
|
-
install: (cluster, images, _owner, nodes, vip) => installMetallb(cluster, {
|
|
3182
|
+
install: (cluster, images, _owner, nodes, vip, settings) => installMetallb(cluster, {
|
|
3029
3183
|
speaker: local(images, "metallb-speaker"),
|
|
3030
3184
|
controller: local(images, "metallb-controller")
|
|
3031
|
-
}, nodes, vip),
|
|
3185
|
+
}, nodes, vip, settings?.memberlist),
|
|
3032
3186
|
hold: (cluster, owner, nodes, namespace, service) => holdMetallb(cluster, owner, nodes, namespace, service),
|
|
3033
3187
|
record: (cluster, namespace, service) => metallbRecord(cluster, namespace, service)
|
|
3034
3188
|
};
|
|
@@ -3041,6 +3195,455 @@ const macsAt = (found, mac, count) => {
|
|
|
3041
3195
|
return wrong.length === 0 ? null : wrong.map((each) => `${each.client} has ${each.mac ?? "no MAC"}`).join(", ");
|
|
3042
3196
|
};
|
|
3043
3197
|
//#endregion
|
|
3198
|
+
//#region src/pulsar.ts
|
|
3199
|
+
/**
|
|
3200
|
+
* This module installs Apache Pulsar 5.0.0 in a kind cluster (SYS-56): one ZooKeeper, one bookie and
|
|
3201
|
+
* two brokers, from the Helm chart 4.8.0. It also gives the producers of the cell: a Python script
|
|
3202
|
+
* in the image of Pulsar, with its client 3.13.0 over the C++ client 4.2.0. Each producer sends to
|
|
3203
|
+
* one topic, and a thread of it looks up the owner of that topic at each probe.
|
|
3204
|
+
*
|
|
3205
|
+
* The fault is a SIGKILL of the JVM of the broker that owns the topic. Each term of the return of a
|
|
3206
|
+
* producer comes from a pinned source:
|
|
3207
|
+
* - The owner of a bundle is an ephemeral znode of the session of its broker. A lookup gives the
|
|
3208
|
+
* owner from the cache of the ownership, with no check of its life (pulsar-broker
|
|
3209
|
+
* NamespaceService.java at v5.0.0, lines 489–508). Thus each lookup names the stopped broker
|
|
3210
|
+
* until its session expires.
|
|
3211
|
+
* - The session timeout is `metadataStoreSessionTimeoutMillis`, 30000 (conf/broker.conf at v5.0.0,
|
|
3212
|
+
* line 187, and the chart, values.yaml line 1474). ZKMetadataStore.java gives it to ZooKeeper (line 105).
|
|
3213
|
+
* - The client of ZooKeeper 3.9.6 pings after 9 s with no send (ClientCnxn.java, lines 422, 1184
|
|
3214
|
+
* and 1263–1268). That is half of the read timeout of 2/3 of the session, less 1 s.
|
|
3215
|
+
* The server rounds each expiry up to the next tick of 2000 ms (ExpiryQueue.java, lines 53–54
|
|
3216
|
+
* and 87, and conf/zookeeper.conf of Pulsar, line 21). Thus the session expires 21–32 s after the stop.
|
|
3217
|
+
* - The broker that stays then takes the bundle at the next lookup.
|
|
3218
|
+
* - The chart starts a broker again only after its old znode is gone (templates/broker-statefulset.yaml,
|
|
3219
|
+
* lines 296–302). Thus the stopped broker cannot take the bundle back first.
|
|
3220
|
+
*/
|
|
3221
|
+
/** This version is the release of Pulsar of the lab. */
|
|
3222
|
+
const PULSAR_VERSION = "5.0.0";
|
|
3223
|
+
/**
|
|
3224
|
+
* This chart is the Helm chart 4.8.0 of Pulsar. The sha256 is of the file of the release. The URL is
|
|
3225
|
+
* on the archive of Apache, which keeps each release. The mirror downloads.apache.org keeps only the
|
|
3226
|
+
* latest releases.
|
|
3227
|
+
*/
|
|
3228
|
+
const PULSAR_CHART = {
|
|
3229
|
+
name: "pulsar",
|
|
3230
|
+
version: "4.8.0",
|
|
3231
|
+
url: "https://archive.apache.org/dist/pulsar/helm-chart/4.8.0/pulsar-4.8.0.tgz",
|
|
3232
|
+
sha256: "464f4a268463d187c9bc83b365b31de0b4a460533d948a04f846ba9b026068f5"
|
|
3233
|
+
};
|
|
3234
|
+
/**
|
|
3235
|
+
* This image is Pulsar 5.0.0 for linux/amd64, pinned by the digest of the manifest of the platform.
|
|
3236
|
+
* It has ZooKeeper 3.9.6, BookKeeper, the broker, Python 3 and the Python client 3.13.0. The lab
|
|
3237
|
+
* pulls it by its digest, tags it (`localPin`), and loads it into each node.
|
|
3238
|
+
*/
|
|
3239
|
+
const PULSAR_IMAGE = "apachepulsar/pulsar:5.0.0@sha256:11e9a885121e16be91da77fdd3cfc3d4cbbece4700271d75e949585dbd3a5a5b";
|
|
3240
|
+
/** This namespace holds Pulsar. The release of the chart is `pulsar` too. */
|
|
3241
|
+
const PULSAR_NAMESPACE = "pulsar";
|
|
3242
|
+
/** This name is the app of the producers of a cell. */
|
|
3243
|
+
const PRODUCER = "producer";
|
|
3244
|
+
/** This topic is the topic of the producers. */
|
|
3245
|
+
const PULSAR_TOPIC = "persistent://public/default/lab";
|
|
3246
|
+
/** This URL is the Service of the brokers for the binary protocol. */
|
|
3247
|
+
const PULSAR_SERVICE = `pulsar://pulsar-broker.${PULSAR_NAMESPACE}.svc.cluster.local:6650`;
|
|
3248
|
+
/** This URL is the lookup of the owner of the topic, on the Service of the brokers. */
|
|
3249
|
+
const PULSAR_LOOKUP = `http://pulsar-broker.${PULSAR_NAMESPACE}.svc.cluster.local:8080/lookup/v2/topic/persistent/public/default/lab`;
|
|
3250
|
+
/** This time is the session timeout of each broker in ZooKeeper (`metadataStoreSessionTimeoutMillis`). */
|
|
3251
|
+
const SESSION = {
|
|
3252
|
+
value: 30,
|
|
3253
|
+
unit: "s"
|
|
3254
|
+
};
|
|
3255
|
+
/** This time is the tick of ZooKeeper (`tickTime`). The server rounds each expiry up to a tick. */
|
|
3256
|
+
const TICK = {
|
|
3257
|
+
value: 2,
|
|
3258
|
+
unit: "s"
|
|
3259
|
+
};
|
|
3260
|
+
/** 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. */
|
|
3261
|
+
const PING = {
|
|
3262
|
+
value: 9,
|
|
3263
|
+
unit: "s"
|
|
3264
|
+
};
|
|
3265
|
+
/**
|
|
3266
|
+
* This backoff is the backoff of a producer of the C++ client 4.2.0, with the defaults of the Python
|
|
3267
|
+
* client 3.13.0:
|
|
3268
|
+
* - The initial delay is 100 ms and the most is 60 s (lib/ClientConfigurationImpl.h, lines 39–40).
|
|
3269
|
+
* - The mandatory stop is the send timeout less 100 ms, and at least 100 ms (lib/ProducerImpl.cc,
|
|
3270
|
+
* lines 55–57). The send timeout of `create_producer` is 30000 ms (pulsar/__init__.py, line 1015).
|
|
3271
|
+
* - The jitter takes 0–9 % of each delay (lib/Backoff.cc, line 54).
|
|
3272
|
+
* - A try that fails takes the lookup and the connect, which a reset ends at once. The lab gives it 100 ms.
|
|
3273
|
+
*/
|
|
3274
|
+
const PRODUCER_BACKOFF = {
|
|
3275
|
+
initial: {
|
|
3276
|
+
value: 100,
|
|
3277
|
+
unit: "ms"
|
|
3278
|
+
},
|
|
3279
|
+
max: {
|
|
3280
|
+
value: 60,
|
|
3281
|
+
unit: "s"
|
|
3282
|
+
},
|
|
3283
|
+
mandatoryStop: {
|
|
3284
|
+
value: 29.9,
|
|
3285
|
+
unit: "s"
|
|
3286
|
+
},
|
|
3287
|
+
jitter: .09,
|
|
3288
|
+
attempt: {
|
|
3289
|
+
value: 100,
|
|
3290
|
+
unit: "ms"
|
|
3291
|
+
}
|
|
3292
|
+
};
|
|
3293
|
+
/** This function gives the repository and the tag of a local image, for the values of a chart. */
|
|
3294
|
+
const repositoryOf = (local) => {
|
|
3295
|
+
const at = local.lastIndexOf(":");
|
|
3296
|
+
if (at <= 0) throw new Error(`the image ${local} has no tag`);
|
|
3297
|
+
return {
|
|
3298
|
+
repository: local.slice(0, at),
|
|
3299
|
+
tag: local.slice(at + 1)
|
|
3300
|
+
};
|
|
3301
|
+
};
|
|
3302
|
+
/**
|
|
3303
|
+
* This function gives the values of the chart: ZooKeeper, one bookie and two brokers, with no
|
|
3304
|
+
* proxy, no autorecovery, no toolset and no monitor. The bookie keeps one copy of each entry, and
|
|
3305
|
+
* the brokers do not move a bundle by load. Each image is the local image, with no pull.
|
|
3306
|
+
*/
|
|
3307
|
+
const pulsarValues = (image) => {
|
|
3308
|
+
const { repository, tag } = repositoryOf(image);
|
|
3309
|
+
const noMonitor = { podMonitor: { enabled: false } };
|
|
3310
|
+
return {
|
|
3311
|
+
components: {
|
|
3312
|
+
zookeeper: true,
|
|
3313
|
+
bookkeeper: true,
|
|
3314
|
+
broker: true,
|
|
3315
|
+
autorecovery: false,
|
|
3316
|
+
proxy: false,
|
|
3317
|
+
toolset: false,
|
|
3318
|
+
functions: false,
|
|
3319
|
+
function_worker: false
|
|
3320
|
+
},
|
|
3321
|
+
"victoria-metrics-k8s-stack": { enabled: false },
|
|
3322
|
+
defaultPulsarImageRepository: repository,
|
|
3323
|
+
defaultPulsarImageTag: tag,
|
|
3324
|
+
defaultPullPolicy: "Never",
|
|
3325
|
+
affinity: { anti_affinity: false },
|
|
3326
|
+
volumes: { persistence: false },
|
|
3327
|
+
zookeeper: {
|
|
3328
|
+
replicaCount: 1,
|
|
3329
|
+
statefulsetUpgrade: { enabled: false },
|
|
3330
|
+
...noMonitor
|
|
3331
|
+
},
|
|
3332
|
+
bookkeeper: {
|
|
3333
|
+
replicaCount: 1,
|
|
3334
|
+
...noMonitor
|
|
3335
|
+
},
|
|
3336
|
+
autorecovery: noMonitor,
|
|
3337
|
+
proxy: noMonitor,
|
|
3338
|
+
broker: {
|
|
3339
|
+
replicaCount: 2,
|
|
3340
|
+
statefulsetUpgrade: { enabled: false },
|
|
3341
|
+
...noMonitor,
|
|
3342
|
+
configData: {
|
|
3343
|
+
managedLedgerDefaultEnsembleSize: "1",
|
|
3344
|
+
managedLedgerDefaultWriteQuorum: "1",
|
|
3345
|
+
managedLedgerDefaultAckQuorum: "1",
|
|
3346
|
+
loadBalancerSheddingEnabled: "false",
|
|
3347
|
+
loadBalancerAutoBundleSplitEnabled: "false"
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3350
|
+
};
|
|
3351
|
+
};
|
|
3352
|
+
/**
|
|
3353
|
+
* This script is a producer of the cell. Each line that it writes is JSON, with the time of
|
|
3354
|
+
* CLOCK_MONOTONIC in milliseconds, the pod and the node:
|
|
3355
|
+
* - `send`: one send, at its start, with its time to the answer and its outcome. A send is `ok` within
|
|
3356
|
+
* the timeout, `late` after it, or the name of the result of the client (for example `timeout`).
|
|
3357
|
+
* - `lookup`: one lookup of the owner of the topic, with the host of the owner.
|
|
3358
|
+
* - `pulsar`: one line of the C++ client about the connection of the producer.
|
|
3359
|
+
*/
|
|
3360
|
+
const PRODUCER_SCRIPT = `import json, logging, os, sys, threading, time, urllib.request
|
|
3361
|
+
import pulsar
|
|
3362
|
+
|
|
3363
|
+
POD = os.environ["POD_NAME"]
|
|
3364
|
+
NODE = os.environ["NODE_NAME"]
|
|
3365
|
+
EVERY = int(os.environ["EVERY_MS"]) / 1000
|
|
3366
|
+
TIMEOUT = int(os.environ["TIMEOUT_MS"])
|
|
3367
|
+
PROBE = int(os.environ["PROBE_MS"]) / 1000
|
|
3368
|
+
LOCK = threading.Lock()
|
|
3369
|
+
KEPT = ("Schedule reconnection", "Getting connection from pool", "Created producer", "Connection closed", "Disconnected", "Lookup response", "Failed to", "Finished connecting")
|
|
3370
|
+
|
|
3371
|
+
|
|
3372
|
+
def now():
|
|
3373
|
+
return time.monotonic() * 1000
|
|
3374
|
+
|
|
3375
|
+
|
|
3376
|
+
def say(**line):
|
|
3377
|
+
text = json.dumps({**line, "p": POD, "n": NODE}, separators=(",", ":"))
|
|
3378
|
+
with LOCK:
|
|
3379
|
+
sys.stdout.write(text + "\\n")
|
|
3380
|
+
sys.stdout.flush()
|
|
3381
|
+
|
|
3382
|
+
|
|
3383
|
+
class Relay(logging.Handler):
|
|
3384
|
+
def emit(self, record):
|
|
3385
|
+
message = record.getMessage()
|
|
3386
|
+
if any(word in message for word in KEPT):
|
|
3387
|
+
say(ev="pulsar", t=now(), m=message[:400])
|
|
3388
|
+
|
|
3389
|
+
|
|
3390
|
+
def probe():
|
|
3391
|
+
while True:
|
|
3392
|
+
start = now()
|
|
3393
|
+
owner = None
|
|
3394
|
+
outcome = "ok"
|
|
3395
|
+
try:
|
|
3396
|
+
with urllib.request.urlopen(os.environ["LOOKUP"], timeout=1) as answer:
|
|
3397
|
+
data = json.loads(answer.read())
|
|
3398
|
+
url = data.get("brokerUrl") or ""
|
|
3399
|
+
owner = url.split("://", 1)[-1].rsplit(":", 1)[0] or None
|
|
3400
|
+
except Exception as error:
|
|
3401
|
+
outcome = type(error).__name__.lower()
|
|
3402
|
+
say(ev="lookup", t=start, d=round(now() - start, 3), o=outcome, owner=owner)
|
|
3403
|
+
time.sleep(max(0.0, PROBE - (now() - start) / 1000))
|
|
3404
|
+
|
|
3405
|
+
|
|
3406
|
+
def answered(start):
|
|
3407
|
+
def done(result, message_id):
|
|
3408
|
+
took = now() - start
|
|
3409
|
+
if result == pulsar.Result.Ok:
|
|
3410
|
+
outcome = "ok" if took <= TIMEOUT else "late"
|
|
3411
|
+
else:
|
|
3412
|
+
outcome = getattr(result, "name", str(result).split(".")[-1]).lower()
|
|
3413
|
+
say(ev="send", t=start, d=round(took, 3), o=outcome)
|
|
3414
|
+
return done
|
|
3415
|
+
|
|
3416
|
+
|
|
3417
|
+
logger = logging.getLogger("lab")
|
|
3418
|
+
logger.setLevel(logging.INFO)
|
|
3419
|
+
logger.propagate = False
|
|
3420
|
+
logger.addHandler(Relay())
|
|
3421
|
+
threading.Thread(target=probe, daemon=True).start()
|
|
3422
|
+
client = pulsar.Client(os.environ["SERVICE"], logger=logger)
|
|
3423
|
+
while True:
|
|
3424
|
+
try:
|
|
3425
|
+
producer = client.create_producer(os.environ["TOPIC"])
|
|
3426
|
+
break
|
|
3427
|
+
except Exception as error:
|
|
3428
|
+
say(ev="text", t=now(), text="create_producer: " + str(error)[:200])
|
|
3429
|
+
time.sleep(2)
|
|
3430
|
+
at = time.monotonic()
|
|
3431
|
+
while True:
|
|
3432
|
+
start = now()
|
|
3433
|
+
try:
|
|
3434
|
+
producer.send_async(b"lab", answered(start))
|
|
3435
|
+
except Exception as error:
|
|
3436
|
+
say(ev="send", t=start, d=0, o="refused-" + type(error).__name__.lower())
|
|
3437
|
+
at += EVERY
|
|
3438
|
+
time.sleep(max(0.0, at - time.monotonic()))
|
|
3439
|
+
`;
|
|
3440
|
+
/**
|
|
3441
|
+
* This function gives the producers of a cell in its namespace. They are the script in a ConfigMap,
|
|
3442
|
+
* and a Deployment of `clients` pods in the local image of Pulsar, one on each worker.
|
|
3443
|
+
*/
|
|
3444
|
+
const pulsarObjects = (cell, namespace, image) => {
|
|
3445
|
+
if (cell.pulsar === void 0) throw new Error(`${cell.name} has no broker`);
|
|
3446
|
+
const env = {
|
|
3447
|
+
SERVICE: PULSAR_SERVICE,
|
|
3448
|
+
TOPIC: PULSAR_TOPIC,
|
|
3449
|
+
LOOKUP: PULSAR_LOOKUP,
|
|
3450
|
+
EVERY_MS: String(valueIn(cell.workload.every, "ms")),
|
|
3451
|
+
TIMEOUT_MS: String(valueIn(cell.workload.timeout, "ms")),
|
|
3452
|
+
PROBE_MS: String(valueIn(cell.pulsar.probe, "ms")),
|
|
3453
|
+
PYTHONUNBUFFERED: "1"
|
|
3454
|
+
};
|
|
3455
|
+
return [{
|
|
3456
|
+
apiVersion: "v1",
|
|
3457
|
+
kind: "ConfigMap",
|
|
3458
|
+
metadata: {
|
|
3459
|
+
name: PRODUCER,
|
|
3460
|
+
namespace
|
|
3461
|
+
},
|
|
3462
|
+
data: { "producer.py": PRODUCER_SCRIPT }
|
|
3463
|
+
}, {
|
|
3464
|
+
apiVersion: "apps/v1",
|
|
3465
|
+
kind: "Deployment",
|
|
3466
|
+
metadata: {
|
|
3467
|
+
name: PRODUCER,
|
|
3468
|
+
namespace,
|
|
3469
|
+
labels: { app: PRODUCER }
|
|
3470
|
+
},
|
|
3471
|
+
spec: {
|
|
3472
|
+
replicas: cell.pulsar.clients,
|
|
3473
|
+
selector: { matchLabels: { app: PRODUCER } },
|
|
3474
|
+
template: {
|
|
3475
|
+
metadata: { labels: {
|
|
3476
|
+
app: PRODUCER,
|
|
3477
|
+
[LOG_LABEL]: "1"
|
|
3478
|
+
} },
|
|
3479
|
+
spec: {
|
|
3480
|
+
terminationGracePeriodSeconds: 0,
|
|
3481
|
+
topologySpreadConstraints: [{
|
|
3482
|
+
maxSkew: 1,
|
|
3483
|
+
topologyKey: "kubernetes.io/hostname",
|
|
3484
|
+
whenUnsatisfiable: "DoNotSchedule",
|
|
3485
|
+
labelSelector: { matchLabels: { app: PRODUCER } }
|
|
3486
|
+
}],
|
|
3487
|
+
volumes: [{
|
|
3488
|
+
name: "script",
|
|
3489
|
+
configMap: { name: PRODUCER }
|
|
3490
|
+
}],
|
|
3491
|
+
containers: [{
|
|
3492
|
+
name: PRODUCER,
|
|
3493
|
+
image,
|
|
3494
|
+
imagePullPolicy: "Never",
|
|
3495
|
+
command: ["python3", "/lab/producer.py"],
|
|
3496
|
+
volumeMounts: [{
|
|
3497
|
+
name: "script",
|
|
3498
|
+
mountPath: "/lab"
|
|
3499
|
+
}],
|
|
3500
|
+
env: [
|
|
3501
|
+
...Object.entries(env).map(([name, value]) => ({
|
|
3502
|
+
name,
|
|
3503
|
+
value
|
|
3504
|
+
})),
|
|
3505
|
+
{
|
|
3506
|
+
name: "POD_NAME",
|
|
3507
|
+
valueFrom: { fieldRef: { fieldPath: "metadata.name" } }
|
|
3508
|
+
},
|
|
3509
|
+
{
|
|
3510
|
+
name: "NODE_NAME",
|
|
3511
|
+
valueFrom: { fieldRef: { fieldPath: "spec.nodeName" } }
|
|
3512
|
+
}
|
|
3513
|
+
]
|
|
3514
|
+
}]
|
|
3515
|
+
}
|
|
3516
|
+
}
|
|
3517
|
+
}
|
|
3518
|
+
}];
|
|
3519
|
+
};
|
|
3520
|
+
/** This function gives the pods of the brokers, in the sequence of their names. */
|
|
3521
|
+
const brokersOf = async (cluster) => {
|
|
3522
|
+
return JSON.parse(await cluster.kubectl([
|
|
3523
|
+
"-n",
|
|
3524
|
+
PULSAR_NAMESPACE,
|
|
3525
|
+
"get",
|
|
3526
|
+
"pods",
|
|
3527
|
+
"-l",
|
|
3528
|
+
"component=broker",
|
|
3529
|
+
"-o",
|
|
3530
|
+
"json"
|
|
3531
|
+
])).items.filter((pod) => pod.metadata.deletionTimestamp === void 0).map((pod) => ({
|
|
3532
|
+
pod: pod.metadata.name,
|
|
3533
|
+
node: pod.spec.nodeName ?? "",
|
|
3534
|
+
ip: pod.status?.podIP ?? "",
|
|
3535
|
+
ready: (pod.status?.containerStatuses ?? []).length > 0 && pod.status.containerStatuses.every((status) => status.ready)
|
|
3536
|
+
})).sort((a, b) => a.pod.localeCompare(b.pod));
|
|
3537
|
+
};
|
|
3538
|
+
/** 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. */
|
|
3539
|
+
const brokerOfHost = (brokers, host) => brokers.find((broker) => host.split(".")[0] === broker.pod || host === broker.ip);
|
|
3540
|
+
/**
|
|
3541
|
+
* This function tells why the producers are not at rest, or gives the host of the owner of the
|
|
3542
|
+
* topic. At rest, each of `clients` producers had an answer in the last second and no failure in
|
|
3543
|
+
* the last 2 s. The last lookup of each one is in the last 2 s, and each names the same owner.
|
|
3544
|
+
*/
|
|
3545
|
+
const ownerAtRest = (state, clients) => {
|
|
3546
|
+
if (state.sends.size !== clients) return `${state.sends.size} producers send, not ${clients}`;
|
|
3547
|
+
for (const [pod, send] of state.sends) {
|
|
3548
|
+
if (state.last - send.ok > 1e3) return `the producer ${pod} has no answer in the last second`;
|
|
3549
|
+
if (state.last - send.failed < 2e3) return `the producer ${pod} failed in the last 2 s`;
|
|
3550
|
+
}
|
|
3551
|
+
const owners = /* @__PURE__ */ new Set();
|
|
3552
|
+
for (const pod of state.sends.keys()) {
|
|
3553
|
+
const seen = state.owners.get(pod);
|
|
3554
|
+
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`;
|
|
3555
|
+
owners.add(seen.owner);
|
|
3556
|
+
}
|
|
3557
|
+
if (owners.size !== 1) return `the lookups name ${[...owners].join(" and ")}`;
|
|
3558
|
+
return { owner: [...owners][0] };
|
|
3559
|
+
};
|
|
3560
|
+
const sleep$1 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
3561
|
+
/**
|
|
3562
|
+
* This function waits until each broker is ready and the producers are at rest (`ownerAtRest`). It
|
|
3563
|
+
* gives the broker that owns the topic and the host that the lookups name, or why it could not in `within` ms.
|
|
3564
|
+
*/
|
|
3565
|
+
const holdPulsar = async (cluster, state, clients, within = 3e5) => {
|
|
3566
|
+
const end = Date.now() + within;
|
|
3567
|
+
let why = "the hold did not start";
|
|
3568
|
+
do {
|
|
3569
|
+
const brokers = await brokersOf(cluster);
|
|
3570
|
+
const busy = brokers.find((broker) => !broker.ready);
|
|
3571
|
+
const rest = ownerAtRest(state(), clients);
|
|
3572
|
+
if (brokers.length !== 2) why = `${brokers.length} brokers, not 2`;
|
|
3573
|
+
else if (busy !== void 0) why = `the broker ${busy.pod} is not ready`;
|
|
3574
|
+
else if (typeof rest === "string") why = rest;
|
|
3575
|
+
else {
|
|
3576
|
+
const broker = brokerOfHost(brokers, rest.owner);
|
|
3577
|
+
if (broker !== void 0) return {
|
|
3578
|
+
broker,
|
|
3579
|
+
host: rest.owner
|
|
3580
|
+
};
|
|
3581
|
+
why = `the owner ${rest.owner} is no broker`;
|
|
3582
|
+
}
|
|
3583
|
+
await sleep$1(500);
|
|
3584
|
+
} while (Date.now() < end);
|
|
3585
|
+
return why;
|
|
3586
|
+
};
|
|
3587
|
+
/**
|
|
3588
|
+
* This function installs Pulsar in a cluster, with its image on each node. A failed install gives
|
|
3589
|
+
* the pods of Pulsar and the tail of their logs.
|
|
3590
|
+
*/
|
|
3591
|
+
const installPulsar = async (cluster, image) => {
|
|
3592
|
+
try {
|
|
3593
|
+
await helmInstall(cluster, "pulsar", PULSAR_CHART, PULSAR_NAMESPACE, pulsarValues(image), { createNamespace: true });
|
|
3594
|
+
} catch (error) {
|
|
3595
|
+
const state = async (args) => (await cluster.kubectl([
|
|
3596
|
+
"-n",
|
|
3597
|
+
PULSAR_NAMESPACE,
|
|
3598
|
+
...args
|
|
3599
|
+
]).catch((why) => String(why))).slice(-4e3);
|
|
3600
|
+
const pods = await state([
|
|
3601
|
+
"get",
|
|
3602
|
+
"pods",
|
|
3603
|
+
"-o",
|
|
3604
|
+
"wide"
|
|
3605
|
+
]);
|
|
3606
|
+
const logs = await state([
|
|
3607
|
+
"logs",
|
|
3608
|
+
"-l",
|
|
3609
|
+
"app=pulsar",
|
|
3610
|
+
"--tail=30",
|
|
3611
|
+
"--prefix",
|
|
3612
|
+
"--all-containers"
|
|
3613
|
+
]);
|
|
3614
|
+
throw new Error(`the install of Pulsar failed: ${String(error)}\nthe pods of Pulsar:\n${pods}\nthe logs:\n${logs}`);
|
|
3615
|
+
}
|
|
3616
|
+
};
|
|
3617
|
+
/** These words mark the lines of a broker that the record keeps: the sessions, the leader, the ownership and the topics. */
|
|
3618
|
+
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)/;
|
|
3619
|
+
/**
|
|
3620
|
+
* This function gives the record of Pulsar after a run. It has the brokers, and the kept lines of
|
|
3621
|
+
* each broker and of its last container, with the times of the kubelet.
|
|
3622
|
+
*/
|
|
3623
|
+
const pulsarRecord = async (cluster) => {
|
|
3624
|
+
const brokers = await brokersOf(cluster);
|
|
3625
|
+
const logs = {};
|
|
3626
|
+
for (const broker of brokers) for (const previous of [false, true]) {
|
|
3627
|
+
const text = await cluster.kubectl([
|
|
3628
|
+
"-n",
|
|
3629
|
+
PULSAR_NAMESPACE,
|
|
3630
|
+
"logs",
|
|
3631
|
+
broker.pod,
|
|
3632
|
+
"-c",
|
|
3633
|
+
"pulsar-broker",
|
|
3634
|
+
"--since=300s",
|
|
3635
|
+
"--tail=6000",
|
|
3636
|
+
"--timestamps",
|
|
3637
|
+
...previous ? ["--previous"] : []
|
|
3638
|
+
]).catch((error) => String(error));
|
|
3639
|
+
logs[`${broker.pod}${previous ? ", the last container" : ""}`] = text.split("\n").filter((line) => KEPT.test(line)).slice(-80).join("\n").slice(-8e3);
|
|
3640
|
+
}
|
|
3641
|
+
return {
|
|
3642
|
+
brokers,
|
|
3643
|
+
logs
|
|
3644
|
+
};
|
|
3645
|
+
};
|
|
3646
|
+
//#endregion
|
|
3044
3647
|
//#region src/lds.ts
|
|
3045
3648
|
/**
|
|
3046
3649
|
* This filter of RE2 selects the two counters of the listener manager of Envoy that an update of LDS
|
|
@@ -4092,6 +4695,48 @@ const learnWindow = (declaration, readiness, propagation, workload) => {
|
|
|
4092
4695
|
};
|
|
4093
4696
|
};
|
|
4094
4697
|
//#endregion
|
|
4698
|
+
//#region src/quiet.ts
|
|
4699
|
+
/**
|
|
4700
|
+
* These are the terms of the lab. The flat share is the share of SYS-42. The other terms come from the
|
|
4701
|
+
* calibration of SYS-95, over the 9 records of SYS-57, with no foreign container in them:
|
|
4702
|
+
*
|
|
4703
|
+
* - The margin is the 99th percentile of the fall from the settled median of an environment: 4.88
|
|
4704
|
+
* points of 93 samples, rounded up.
|
|
4705
|
+
* - The floor is the lowest settled median of an environment, 48 %, minus the margin.
|
|
4706
|
+
* - The time to settle holds the slowest environment. Its conntrack table was at its plateau at 105 s.
|
|
4707
|
+
*/
|
|
4708
|
+
const QUIET_GATE = {
|
|
4709
|
+
flat: .6,
|
|
4710
|
+
margin: .05,
|
|
4711
|
+
floor: .43,
|
|
4712
|
+
settle: 12e4,
|
|
4713
|
+
samples: 3
|
|
4714
|
+
};
|
|
4715
|
+
/**
|
|
4716
|
+
* This function gives the idle share that a run needs. With no baseline, it is the floor: a warm-up
|
|
4717
|
+
* counts for nothing, and its table is not full yet. A cell that is at rest at the flat share or more
|
|
4718
|
+
* keeps the flat share.
|
|
4719
|
+
*/
|
|
4720
|
+
const neededIdle = (gate, baseline) => baseline === null ? gate.floor : Math.min(gate.flat, Math.max(gate.floor, baseline - gate.margin));
|
|
4721
|
+
/** This function gives the reason of a sample that the gate refuses. The site reads it back with `NOT_QUIET`. */
|
|
4722
|
+
const quietWhy = (idle, needed, foreign) => `${Math.round(idle * 100)}% idle, the gate needs ${Math.round(needed * 100)}%, other lab containers: ${foreign.join("; ") || "none"}`;
|
|
4723
|
+
/**
|
|
4724
|
+
* This form reads the void of a run that the gate refuses. Its groups are the wait in seconds, the idle share, the share
|
|
4725
|
+
* of the gate and the other lab containers. A record from before SYS-95 has no share of the gate.
|
|
4726
|
+
*/
|
|
4727
|
+
const NOT_QUIET = /^the VM was not quiet in (\d+) s: (\d+)% idle(?:, the gate needs (\d+)%)?, other lab containers: (.*)$/;
|
|
4728
|
+
/**
|
|
4729
|
+
* This function gives the next baseline of a cell from the samples of one environment. Samples with
|
|
4730
|
+
* a foreign container do not count. The highest median holds, because a foreign load only lowers
|
|
4731
|
+
* the idle share. Thus a load in one environment does not move the gate of the other one.
|
|
4732
|
+
*/
|
|
4733
|
+
const baselineOf = (previous, samples) => {
|
|
4734
|
+
const clean = samples.filter((sample) => sample.foreign.length === 0).map((sample) => sample.idle);
|
|
4735
|
+
if (clean.length === 0) return previous;
|
|
4736
|
+
const middle = median(clean);
|
|
4737
|
+
return previous === null ? middle : Math.max(previous, middle);
|
|
4738
|
+
};
|
|
4739
|
+
//#endregion
|
|
4095
4740
|
//#region src/rules.ts
|
|
4096
4741
|
const seconds$1 = (amount) => valueIn(amount, "s");
|
|
4097
4742
|
const s = (value) => ({
|
|
@@ -4187,9 +4832,11 @@ const hasRedis = (cell) => cell.declaration !== null && cell.declaration.redis !
|
|
|
4187
4832
|
* This function gives the key of the cluster of a cell: its kube-proxy mode, its node count, and the
|
|
4188
4833
|
* flavor of its mesh. A zone loss has clusters of its own, with a zone on each node.
|
|
4189
4834
|
*/
|
|
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
|
|
4191
|
-
/** This function
|
|
4192
|
-
const
|
|
4835
|
+
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");
|
|
4836
|
+
/** 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. */
|
|
4837
|
+
const clusterNameOf = (cell, env) => `lab-${clusterKey(cell)}-${env}`;
|
|
4838
|
+
/** 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. */
|
|
4839
|
+
const zoned = (cell) => cell.trigger.kind === "zone-loss" || cell.vip?.memberlist !== void 0;
|
|
4193
4840
|
/**
|
|
4194
4841
|
* This tracker keeps the latest state of the cell from the lines: each pod, each slice and each
|
|
4195
4842
|
* client. It also has, for each node, the backends of its rules and the hosts of its client sidecar.
|
|
@@ -4202,7 +4849,13 @@ var Tracker = class {
|
|
|
4202
4849
|
clients = /* @__PURE__ */ new Map();
|
|
4203
4850
|
/** This map is the backend of the last answer of each client, by client. */
|
|
4204
4851
|
answers = /* @__PURE__ */ new Map();
|
|
4852
|
+
/** This map is the last send of each producer of a broker that got an answer, and the last that failed. */
|
|
4853
|
+
sends = /* @__PURE__ */ new Map();
|
|
4854
|
+
/** This map is the last lookup of the owner of the topic of each producer, and the host that it names. */
|
|
4855
|
+
owners = /* @__PURE__ */ new Map();
|
|
4205
4856
|
last = 0;
|
|
4857
|
+
/** This time is the last line of a producer. */
|
|
4858
|
+
lastSend = 0;
|
|
4206
4859
|
see = (line) => {
|
|
4207
4860
|
if (typeof line.t === "number" && line.t > this.last && line.ev === "req") this.last = line.t;
|
|
4208
4861
|
if (line.ev === "pod") {
|
|
@@ -4225,6 +4878,23 @@ var Tracker = class {
|
|
|
4225
4878
|
else client.failed = Math.max(client.failed, request.t);
|
|
4226
4879
|
this.clients.set(request.p, client);
|
|
4227
4880
|
if (request.o === "ok" && request.b !== void 0) this.answers.set(request.p, request.b);
|
|
4881
|
+
} else if (line.ev === "send") {
|
|
4882
|
+
const pod = String(line["p"]);
|
|
4883
|
+
const send = this.sends.get(pod) ?? {
|
|
4884
|
+
ok: 0,
|
|
4885
|
+
failed: 0
|
|
4886
|
+
};
|
|
4887
|
+
if (line["o"] === "ok") send.ok = Math.max(send.ok, line.t);
|
|
4888
|
+
else send.failed = Math.max(send.failed, line.t);
|
|
4889
|
+
this.sends.set(pod, send);
|
|
4890
|
+
this.lastSend = Math.max(this.lastSend, line.t);
|
|
4891
|
+
} else if (line.ev === "lookup") {
|
|
4892
|
+
const pod = String(line["p"]);
|
|
4893
|
+
const owner = line["o"] === "ok" && typeof line["owner"] === "string" ? line["owner"] : null;
|
|
4894
|
+
if (line.t >= (this.owners.get(pod)?.t ?? 0)) this.owners.set(pod, {
|
|
4895
|
+
t: line.t,
|
|
4896
|
+
owner
|
|
4897
|
+
});
|
|
4228
4898
|
}
|
|
4229
4899
|
};
|
|
4230
4900
|
servers() {
|
|
@@ -4335,21 +5005,57 @@ const sample = async (ctx, ours) => {
|
|
|
4335
5005
|
* This is below the share of 60%. That wait had no bound, and it took approximately 30 min.
|
|
4336
5006
|
*/
|
|
4337
5007
|
const QUIET_WITHIN = 3e5;
|
|
5008
|
+
/** This function gives the names of this lab at a sample: the prober and the watcher start again with new names. */
|
|
5009
|
+
const oursAt = (ctx, ours) => /* @__PURE__ */ new Set([
|
|
5010
|
+
...ours,
|
|
5011
|
+
...ctx.lab.own(),
|
|
5012
|
+
ctx.loader.getName().replace(/^\//, "")
|
|
5013
|
+
]);
|
|
5014
|
+
/**
|
|
5015
|
+
* This function takes the baseline of the cell in this environment, once. The first run after the
|
|
5016
|
+
* warm-up calls it. It waits until the cell has its age. The conntrack table fills for the life of
|
|
5017
|
+
* its entries, and the read of the table costs more until then.
|
|
5018
|
+
*/
|
|
5019
|
+
const takeBaseline = async (ctx, ours) => {
|
|
5020
|
+
if (ctx.settled) return;
|
|
5021
|
+
await sleep(Math.max(0, ctx.deployed + ctx.gate.settle - Date.now()));
|
|
5022
|
+
const samples = [];
|
|
5023
|
+
for (let index = 0; index < ctx.gate.samples; index += 1) samples.push(await sample(ctx, oursAt(ctx, ours)));
|
|
5024
|
+
ctx.loads.push(...samples);
|
|
5025
|
+
const baseline = baselineOf(ctx.baselines.get(ctx.cell.name) ?? null, samples);
|
|
5026
|
+
if (baseline !== null) ctx.baselines.set(ctx.cell.name, baseline);
|
|
5027
|
+
ctx.settled = true;
|
|
5028
|
+
ctx.log(` baseline: ${samples.map((each) => `${Math.round(each.idle * 100)}%`).join(", ")} idle, the gate needs ${Math.round(neededIdle(ctx.gate, baseline) * 100)}%`);
|
|
5029
|
+
};
|
|
5030
|
+
/** This function starts the planned load of a cell for the longest wait of the gate. The load runs in the loader, out of the cluster. */
|
|
5031
|
+
const startPlannedLoad = async (ctx) => {
|
|
5032
|
+
const cores = ctx.cell.plannedLoad;
|
|
5033
|
+
if (cores === void 0) return;
|
|
5034
|
+
const spec = JSON.stringify({
|
|
5035
|
+
cores,
|
|
5036
|
+
ms: 32e4
|
|
5037
|
+
});
|
|
5038
|
+
await ctx.loader.exec([
|
|
5039
|
+
"sh",
|
|
5040
|
+
"-c",
|
|
5041
|
+
`nohup node agent.mjs burn '${spec}' >/dev/null 2>&1 &`
|
|
5042
|
+
]);
|
|
5043
|
+
};
|
|
4338
5044
|
/**
|
|
4339
5045
|
* This function waits until the VM is quiet: no lab container of another agent, and an idle share of
|
|
4340
|
-
* the CPUs. It gives null, or the reason when the VM is not quiet in `within` ms.
|
|
5046
|
+
* the CPUs (`neededIdle`). It gives null, or the reason when the VM is not quiet in `within` ms.
|
|
4341
5047
|
*/
|
|
4342
5048
|
const quiet = async (ctx, ours, within = QUIET_WITHIN) => {
|
|
4343
5049
|
const end = Date.now() + within;
|
|
5050
|
+
const needed = neededIdle(ctx.gate, ctx.baselines.get(ctx.cell.name) ?? null);
|
|
4344
5051
|
for (;;) {
|
|
4345
|
-
const load = await sample(ctx,
|
|
4346
|
-
|
|
4347
|
-
...
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
const why = `${Math.round(load.idle * 100)}% idle, other lab containers: ${load.foreign.join("; ") || "none"}`;
|
|
5052
|
+
const load = await sample(ctx, oursAt(ctx, ours));
|
|
5053
|
+
ctx.loads.push({
|
|
5054
|
+
...load,
|
|
5055
|
+
needed
|
|
5056
|
+
});
|
|
5057
|
+
if (load.foreign.length === 0 && load.idle >= needed) return null;
|
|
5058
|
+
const why = quietWhy(load.idle, needed, load.foreign);
|
|
4353
5059
|
if (Date.now() >= end) return `the VM was not quiet in ${within / 1e3} s: ${why}`;
|
|
4354
5060
|
ctx.log(` waiting: ${why}`);
|
|
4355
5061
|
await sleep(3e4);
|
|
@@ -4405,6 +5111,18 @@ const deploy = async (lab, cluster, cell, namespace, images, vip) => {
|
|
|
4405
5111
|
`daemonset/${CLIENT}`,
|
|
4406
5112
|
"--timeout=300s"
|
|
4407
5113
|
]);
|
|
5114
|
+
if (cell.pulsar !== void 0) {
|
|
5115
|
+
if (images.pulsar === void 0) throw new Error(`${cell.name}: the cluster has no image of Pulsar`);
|
|
5116
|
+
await cluster.apply(pulsarObjects(cell, namespace, images.pulsar));
|
|
5117
|
+
await cluster.kubectl([
|
|
5118
|
+
"-n",
|
|
5119
|
+
namespace,
|
|
5120
|
+
"rollout",
|
|
5121
|
+
"status",
|
|
5122
|
+
`deployment/${PRODUCER}`,
|
|
5123
|
+
"--timeout=300s"
|
|
5124
|
+
]);
|
|
5125
|
+
}
|
|
4408
5126
|
if (cell.vip !== void 0) {
|
|
4409
5127
|
if (vip === null) throw new Error(`${cell.name}: the cluster has no VIP`);
|
|
4410
5128
|
const served = await vipServed(cluster, namespace, "lab", vip);
|
|
@@ -4488,7 +5206,7 @@ const probeThePods = async (ctx) => {
|
|
|
4488
5206
|
if (key === ctx.probed) return;
|
|
4489
5207
|
const netns = [];
|
|
4490
5208
|
for (const pod of pods) {
|
|
4491
|
-
const container = pod.labels["app"] === "lab" ? "app" : pod.labels["app"] === "client" ? CLIENT : REDIS;
|
|
5209
|
+
const container = pod.labels["app"] === "lab" ? "app" : pod.labels["app"] === "client" ? CLIENT : pod.labels["app"] === "producer" ? PRODUCER : REDIS;
|
|
4492
5210
|
const pid = await ctx.lab.pidOf(pod.node, ctx.namespace, pod.name, container);
|
|
4493
5211
|
netns.push(await ctx.lab.netnsOf(pod.node, pid));
|
|
4494
5212
|
}
|
|
@@ -4645,13 +5363,19 @@ const namespacesOf = async (ctx, target, replicas) => {
|
|
|
4645
5363
|
*/
|
|
4646
5364
|
const middleboxServers = async (ctx, idle, port, replicas) => {
|
|
4647
5365
|
const peers = [...ctx.tracker.pods.values()].filter((pod) => pod.labels["app"] === "client" && !pod.del && pod.ip !== null).map((pod) => pod.ip).sort();
|
|
4648
|
-
const commands = middleboxCommands(peers, idle, port);
|
|
5366
|
+
const commands = idle === null ? null : middleboxCommands(peers, idle, port);
|
|
4649
5367
|
const t0 = await ctx.lab.now();
|
|
4650
5368
|
const each = await namespacesOf(ctx, "servers", replicas);
|
|
4651
5369
|
await Promise.all(each.map(async ({ node, pid }) => {
|
|
4652
|
-
if (
|
|
4653
|
-
|
|
4654
|
-
|
|
5370
|
+
if (commands !== null) {
|
|
5371
|
+
await ctx.lab.netns(node, pid, commands);
|
|
5372
|
+
await ctx.lab.tables(node, pid, "filter");
|
|
5373
|
+
return;
|
|
5374
|
+
}
|
|
5375
|
+
const removal = middleboxRemoval(await ctx.lab.tables(node, pid, "filter"));
|
|
5376
|
+
if (removal.length > 0) await ctx.lab.netns(node, pid, removal);
|
|
5377
|
+
const left = middleboxRemoval(await ctx.lab.tables(node, pid, "filter"));
|
|
5378
|
+
if (left.length > 0) throw new Error(`the middlebox on ${node} (PID ${pid}) has ${left.length} parts after its removal`);
|
|
4655
5379
|
}));
|
|
4656
5380
|
return {
|
|
4657
5381
|
t0,
|
|
@@ -4671,6 +5395,35 @@ const doStep = async (ctx, step, replicas, background) => {
|
|
|
4671
5395
|
pid: await lab.pidOf(target.node, namespace, target.pod, "app"),
|
|
4672
5396
|
pod: null
|
|
4673
5397
|
};
|
|
5398
|
+
if (which === "speaker") {
|
|
5399
|
+
const owner = lostNodeOf(cluster);
|
|
5400
|
+
const pod = JSON.parse(await cluster.kubectl([
|
|
5401
|
+
"-n",
|
|
5402
|
+
METALLB_NAMESPACE,
|
|
5403
|
+
"get",
|
|
5404
|
+
"pods",
|
|
5405
|
+
"-l",
|
|
5406
|
+
"app.kubernetes.io/component=speaker",
|
|
5407
|
+
"--field-selector",
|
|
5408
|
+
`spec.nodeName=${owner}`,
|
|
5409
|
+
"-o",
|
|
5410
|
+
"json"
|
|
5411
|
+
])).items[0];
|
|
5412
|
+
if (pod === void 0) throw new Error(`no speaker of MetalLB is on ${owner}`);
|
|
5413
|
+
return {
|
|
5414
|
+
node: owner,
|
|
5415
|
+
pid: await lab.pidOf(owner, METALLB_NAMESPACE, pod.metadata.name, "speaker"),
|
|
5416
|
+
pod: null
|
|
5417
|
+
};
|
|
5418
|
+
}
|
|
5419
|
+
if (which === "broker") {
|
|
5420
|
+
if (ctx.broker === null) throw new Error(`${ctx.cell.name}: the run has no broker that owns the topic`);
|
|
5421
|
+
return {
|
|
5422
|
+
node: ctx.broker.node,
|
|
5423
|
+
pid: await lab.pidOf(ctx.broker.node, PULSAR_NAMESPACE, ctx.broker.pod, "pulsar-broker"),
|
|
5424
|
+
pod: null
|
|
5425
|
+
};
|
|
5426
|
+
}
|
|
4674
5427
|
if (which === "redis") {
|
|
4675
5428
|
const redis = [...ctx.tracker.pods.values()].find((pod) => pod.labels["app"] === "redis" && !pod.del);
|
|
4676
5429
|
return {
|
|
@@ -4719,7 +5472,8 @@ const doStep = async (ctx, step, replicas, background) => {
|
|
|
4719
5472
|
}
|
|
4720
5473
|
case "signal": {
|
|
4721
5474
|
if (step.target === "clients") return signalClients(ctx, step.signal);
|
|
4722
|
-
const at = await where(step.target);
|
|
5475
|
+
const at = (step.target === "speaker" && step.signal === "SIGCONT" ? ctx.speaker : void 0) ?? await where(step.target);
|
|
5476
|
+
if (step.target === "speaker") ctx.speaker = step.signal === "SIGSTOP" ? at : void 0;
|
|
4723
5477
|
return lab.act(at.node, {
|
|
4724
5478
|
op: "signal",
|
|
4725
5479
|
pid: at.pid,
|
|
@@ -4878,6 +5632,19 @@ const recordVip = async (ctx) => {
|
|
|
4878
5632
|
macs
|
|
4879
5633
|
});
|
|
4880
5634
|
};
|
|
5635
|
+
/** This function records the brokers after a run, with the kept lines of each broker. It stamps the wall clock and the monotonic clock. */
|
|
5636
|
+
const recordPulsar = async (ctx) => {
|
|
5637
|
+
const wall = Date.now();
|
|
5638
|
+
const t = Number(process.hrtime.bigint()) / 1e6;
|
|
5639
|
+
ctx.lab.record({
|
|
5640
|
+
ev: "pulsar-record",
|
|
5641
|
+
t,
|
|
5642
|
+
wall,
|
|
5643
|
+
version: PULSAR_VERSION,
|
|
5644
|
+
owner: ctx.broker?.pod ?? null,
|
|
5645
|
+
...await pulsarRecord(ctx.cluster)
|
|
5646
|
+
});
|
|
5647
|
+
};
|
|
4881
5648
|
/** This function puts the replicas of a node first, and keeps the sequence of the others. */
|
|
4882
5649
|
const lostFirst = (replicas, node) => [...replicas.filter((replica) => replica.node === node), ...replicas.filter((replica) => replica.node !== node)];
|
|
4883
5650
|
/** This function tells if a run is over: the trigger is done, and the cell is at rest again. */
|
|
@@ -4931,6 +5698,10 @@ const runOnce = async (ctx, env, index, warmup, ours) => {
|
|
|
4931
5698
|
if (atRest !== null) return empty(`the cell was not at rest: ${atRest}`, await lab.now());
|
|
4932
5699
|
await probeThePods(ctx);
|
|
4933
5700
|
await pollListeners(ctx);
|
|
5701
|
+
if (!warmup) {
|
|
5702
|
+
await takeBaseline(ctx, ours);
|
|
5703
|
+
await startPlannedLoad(ctx);
|
|
5704
|
+
}
|
|
4934
5705
|
const loud = await quiet(ctx, ours);
|
|
4935
5706
|
if (loud !== null) {
|
|
4936
5707
|
ctx.log(` ${loud}`);
|
|
@@ -4957,10 +5728,25 @@ const runOnce = async (ctx, env, index, warmup, ours) => {
|
|
|
4957
5728
|
await sleep(valueIn(announcer.settle, "ms") + valueIn(cell.workload.timeout, "ms") + 1e3);
|
|
4958
5729
|
}
|
|
4959
5730
|
if (await steady(ctx, 3e5) !== null) return empty("the cell was not at rest after the wait for a quiet VM", await lab.now());
|
|
5731
|
+
ctx.broker = null;
|
|
5732
|
+
if (cell.pulsar !== void 0) {
|
|
5733
|
+
const tracker = ctx.tracker;
|
|
5734
|
+
const held = await holdPulsar(cluster, () => ({
|
|
5735
|
+
owners: tracker.owners,
|
|
5736
|
+
sends: tracker.sends,
|
|
5737
|
+
last: tracker.lastSend
|
|
5738
|
+
}), cell.pulsar.clients);
|
|
5739
|
+
if (typeof held === "string") return empty(`the producers were not at rest: ${held}`, await lab.now());
|
|
5740
|
+
ctx.broker = {
|
|
5741
|
+
pod: held.broker.pod,
|
|
5742
|
+
node: held.broker.node,
|
|
5743
|
+
host: held.host
|
|
5744
|
+
};
|
|
5745
|
+
}
|
|
4960
5746
|
const replicas = zoneLoss ? lostFirst(replicasOf(ctx), lostNodeOf(cluster)) : replicasOf(ctx);
|
|
4961
5747
|
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
5748
|
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) && [
|
|
5749
|
+
const target = cell.vip !== void 0 || cell.pulsar !== void 0 || hasRedis(cell) && [
|
|
4964
5750
|
"rst",
|
|
4965
5751
|
"blackhole",
|
|
4966
5752
|
"freeze-process",
|
|
@@ -4968,7 +5754,7 @@ const runOnce = async (ctx, env, index, warmup, ours) => {
|
|
|
4968
5754
|
"rollout",
|
|
4969
5755
|
"latency"
|
|
4970
5756
|
].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;
|
|
5757
|
+
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
5758
|
const cleared = await lab.forgetHandshakes(ctx.clusterIp, 80);
|
|
4973
5759
|
lab.take();
|
|
4974
5760
|
for (const key of Object.keys(lab.dropped)) delete lab.dropped[key];
|
|
@@ -5061,6 +5847,7 @@ const runOnce = async (ctx, env, index, warmup, ours) => {
|
|
|
5061
5847
|
const end = await lab.now();
|
|
5062
5848
|
const timedOut = Date.now() >= limit;
|
|
5063
5849
|
if (cell.vip !== void 0) await recordVip(ctx).catch((error) => notes.push(`the record of the VIP failed: ${String(error)}`));
|
|
5850
|
+
if (cell.pulsar !== void 0) await recordPulsar(ctx).catch((error) => notes.push(`the record of Pulsar failed: ${String(error)}`));
|
|
5064
5851
|
for (const step of plan.restore) try {
|
|
5065
5852
|
await doStep(ctx, step, replicas, background);
|
|
5066
5853
|
} catch (error) {
|
|
@@ -5103,6 +5890,7 @@ const runOnce = async (ctx, env, index, warmup, ours) => {
|
|
|
5103
5890
|
},
|
|
5104
5891
|
target: stopped,
|
|
5105
5892
|
paused,
|
|
5893
|
+
...ctx.broker === null ? {} : { broker: ctx.broker.host },
|
|
5106
5894
|
replicas,
|
|
5107
5895
|
void: null,
|
|
5108
5896
|
lines,
|
|
@@ -5186,19 +5974,22 @@ const runKindCells = async (cells, options) => {
|
|
|
5186
5974
|
for (const [name, ref] of Object.entries(announcer.images)) pinned[name] = await localPin(ref);
|
|
5187
5975
|
announcerImages.set(announcer.name, pinned);
|
|
5188
5976
|
}
|
|
5977
|
+
const pulsarImage = cells.some((cell) => cell.pulsar !== void 0) ? await localPin(PULSAR_IMAGE) : void 0;
|
|
5189
5978
|
const images = {
|
|
5190
5979
|
lab: image,
|
|
5191
5980
|
redis: redisImage,
|
|
5192
5981
|
grpc: grpcImage,
|
|
5193
|
-
mesh: meshImages
|
|
5982
|
+
mesh: meshImages,
|
|
5983
|
+
pulsar: pulsarImage
|
|
5194
5984
|
};
|
|
5195
5985
|
const groups = /* @__PURE__ */ new Map();
|
|
5196
5986
|
for (const cell of cells) groups.set(clusterKey(cell), [...groups.get(clusterKey(cell)) ?? [], cell]);
|
|
5197
5987
|
const results = [];
|
|
5198
|
-
|
|
5988
|
+
const baselines = /* @__PURE__ */ new Map();
|
|
5989
|
+
for (const group of groups.values()) {
|
|
5199
5990
|
const partial = /* @__PURE__ */ new Map();
|
|
5200
5991
|
for (let env = 0; env < environments; env += 1) {
|
|
5201
|
-
const s = scenario(
|
|
5992
|
+
const s = scenario(clusterNameOf(group[0], env), group[0].mode, group[0].workers, void 0, zoned(group[0]));
|
|
5202
5993
|
const dir = join(options.work ?? join(tmpdir(), "systems-kind-work"), s.name);
|
|
5203
5994
|
const mesh = group[0].mesh;
|
|
5204
5995
|
options.log(`cluster ${s.name}: ${s.dataplane}, ${s.workers} workers${mesh === void 0 ? "" : `, Istio ${mesh.flavor}`}`);
|
|
@@ -5239,9 +6030,16 @@ const runKindCells = async (cells, options) => {
|
|
|
5239
6030
|
for (const each of Object.values(pinned)) await cluster.load(each);
|
|
5240
6031
|
const began = Date.now();
|
|
5241
6032
|
vip = announcer.vipOf((await vipNetworkOf(cluster.network)).subnet, lostNodeOf(cluster), workersOf(cluster));
|
|
5242
|
-
await announcer.install(cluster, pinned, lostNodeOf(cluster), workersOf(cluster), vip);
|
|
6033
|
+
await announcer.install(cluster, pinned, lostNodeOf(cluster), workersOf(cluster), vip, group[0].vip);
|
|
5243
6034
|
options.log(` ${announcer.name} installed in ${Math.round((Date.now() - began) / 100) / 10} s, the VIP ${vip}`);
|
|
5244
6035
|
}
|
|
6036
|
+
if (group[0].pulsar !== void 0) {
|
|
6037
|
+
if (pulsarImage === void 0) throw new Error(`${group[0].name}: the run has no image of Pulsar`);
|
|
6038
|
+
await cluster.load(pulsarImage);
|
|
6039
|
+
const began = Date.now();
|
|
6040
|
+
await installPulsar(cluster, pulsarImage);
|
|
6041
|
+
options.log(` Pulsar ${PULSAR_VERSION} installed in ${Math.round((Date.now() - began) / 100) / 10} s`);
|
|
6042
|
+
}
|
|
5245
6043
|
lab = await openKindLab(cluster, image);
|
|
5246
6044
|
const { GenericContainer } = await import("testcontainers");
|
|
5247
6045
|
const Shared = class extends GenericContainer {
|
|
@@ -5274,13 +6072,20 @@ const runKindCells = async (cells, options) => {
|
|
|
5274
6072
|
vip: cell.vip === void 0 ? null : vip,
|
|
5275
6073
|
loads,
|
|
5276
6074
|
loader,
|
|
5277
|
-
|
|
6075
|
+
gate: {
|
|
6076
|
+
...QUIET_GATE,
|
|
6077
|
+
flat: options.idle ?? QUIET_GATE.flat
|
|
6078
|
+
},
|
|
6079
|
+
baselines,
|
|
6080
|
+
deployed: Date.now(),
|
|
6081
|
+
settled: false,
|
|
5278
6082
|
log: options.log,
|
|
5279
6083
|
version: 1,
|
|
5280
6084
|
probed: "",
|
|
5281
6085
|
listened: "",
|
|
5282
6086
|
expected: cell.pod.replicas,
|
|
5283
|
-
pids: /* @__PURE__ */ new Map()
|
|
6087
|
+
pids: /* @__PURE__ */ new Map(),
|
|
6088
|
+
broker: null
|
|
5284
6089
|
};
|
|
5285
6090
|
const share = Math.ceil(cell.runs / environments) - (env < cell.runs % environments || cell.runs % environments === 0 ? 0 : 1);
|
|
5286
6091
|
const runs = [await runOnce(ctx, env, -1, true, ours)];
|
|
@@ -5302,7 +6107,8 @@ const runKindCells = async (cells, options) => {
|
|
|
5302
6107
|
...stackOf(cell) === "grpc" ? { grpc: grpcImage } : {},
|
|
5303
6108
|
...cell.mesh === void 0 ? {} : Object.fromEntries(MESH_IMAGES[cell.mesh.flavor].map((name) => [`istio-${name}`, ISTIO_IMAGES[name]])),
|
|
5304
6109
|
...s.cni === "cilium" ? Object.fromEntries(Object.entries(CILIUM_IMAGES).map(([name, ref]) => [`cilium-${name}`, ref])) : {},
|
|
5305
|
-
...cell.vip === void 0 ? {} : announcerOf(cell).images
|
|
6110
|
+
...cell.vip === void 0 ? {} : announcerOf(cell).images,
|
|
6111
|
+
...cell.pulsar === void 0 ? {} : { pulsar: PULSAR_IMAGE }
|
|
5306
6112
|
}
|
|
5307
6113
|
});
|
|
5308
6114
|
entry.loads.push(...loads);
|
|
@@ -5370,14 +6176,17 @@ const estimate = (cell, environments = 2) => {
|
|
|
5370
6176
|
"ztunnel-restart": 40,
|
|
5371
6177
|
"certificate-expiry": hold + 20,
|
|
5372
6178
|
"cannot-serve": hold + 20,
|
|
5373
|
-
"zone-loss": hold + 45
|
|
6179
|
+
"zone-loss": hold + 45,
|
|
6180
|
+
...cell.vip === void 0 ? {} : { "freeze-process": hold + 20 }
|
|
5374
6181
|
};
|
|
5375
6182
|
const after = observationAfter(cell);
|
|
5376
6183
|
const observed = after === null ? 0 : hold + valueIn(after, "s");
|
|
5377
6184
|
const closes = cell.prediction.kind === "ends" && cell.prediction.band !== null ? valueIn(cell.prediction.band.hi, "s") + 3 : 0;
|
|
5378
6185
|
const quiet = cell.trigger.quiet === true ? hold + 10 : 0;
|
|
5379
|
-
const
|
|
5380
|
-
|
|
6186
|
+
const move = cell.vip === void 0 ? 0 : 30;
|
|
6187
|
+
const broker = cell.pulsar === void 0 ? 0 : 150;
|
|
6188
|
+
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")));
|
|
6189
|
+
return ((cell.mesh === void 0 ? 60 : 180) + (cell.vip === void 0 ? 0 : 60) + (cell.pulsar === void 0 ? 0 : 240)) * environments + (cell.runs + environments) * each;
|
|
5381
6190
|
};
|
|
5382
6191
|
/**
|
|
5383
6192
|
* This function gives an estimate of the time of a cell: its setup, then each run and each
|
|
@@ -5408,6 +6217,6 @@ const shardOf = (cells, index, count) => {
|
|
|
5408
6217
|
return out;
|
|
5409
6218
|
};
|
|
5410
6219
|
//#endregion
|
|
5411
|
-
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 };
|
|
6220
|
+
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, NOT_QUIET, 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_GATE, 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, baselineOf, 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, middleboxRemoval, namesService, namespaceObject, namespaceOf, neededIdle, 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, quietWhy, 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 };
|
|
5412
6221
|
|
|
5413
6222
|
//# sourceMappingURL=index.js.map
|