chaos: install harness configs atomically, and make the harness shellcheck-clean - #253
Conversation
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
There was a problem hiding this comment.
4 issues found across 1 file
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="scripts/live-chaos/lib.sh">
<violation number="1" location="scripts/live-chaos/lib.sh:318">
P2: Using `mktemp` here adds an unconditional dependency that the documented exact `CHAOS_WORKDIR` mode does not preflight. On a minimal supported host without `mktemp`, the run reaches config emission and fails only then; preflight `mktemp` for both modes or provide a supported fallback.</violation>
<violation number="2" location="scripts/live-chaos/lib.sh:318">
P3: Every emitted config file now silently changes permissions from the shell's default umask (typically 0644) to 0600, because `mktemp` creates the temp file with mode 0600 and `mv` preserves that mode. Today that is harmless — the harness launches vtop-node as the same user — but it is an unannounced regression across all 12 emitters, and it will break any future consumer that reads these configs as a different user (e.g. a container mount, sudo-run node, or a shared workdir). Consider pinning the final mode so the change in behavior is explicit and stable.</violation>
<violation number="3" location="scripts/live-chaos/lib.sh:319">
P1: A failed config write can still replace the live config with a partial file because `cat`'s status is ignored and `mv` always runs. On ENOSPC or another I/O error, readers get an atomically renamed but truncated YAML instead of the previous complete config; remove the temp file and return before renaming when `cat` fails.</violation>
<violation number="4" location="scripts/live-chaos/lib.sh:354">
P2: An installation failure is hidden from callers because every emitter prints `$cfg` after the pipeline regardless of its status, so command-substitution callers can start nodes or CLIs with a missing or stale config. Return the pipeline failure before printing the path (for example, `} | install_config "$cfg" || return 1`) in each emitter.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| cat > "$tmp" | ||
| mv -f "$tmp" "$path" |
There was a problem hiding this comment.
P1: A failed config write can still replace the live config with a partial file because cat's status is ignored and mv always runs. On ENOSPC or another I/O error, readers get an atomically renamed but truncated YAML instead of the previous complete config; remove the temp file and return before renaming when cat fails.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/live-chaos/lib.sh, line 319:
<comment>A failed config write can still replace the live config with a partial file because `cat`'s status is ignored and `mv` always runs. On ENOSPC or another I/O error, readers get an atomically renamed but truncated YAML instead of the previous complete config; remove the temp file and return before renaming when `cat` fails.</comment>
<file context>
@@ -296,6 +301,25 @@ require_binaries() {
+install_config() {
+ local path="$1" tmp
+ tmp="$(mktemp "$path.XXXXXX")" || return 1
+ cat > "$tmp"
+ mv -f "$tmp" "$path"
+}
</file context>
| cat > "$tmp" | |
| mv -f "$tmp" "$path" | |
| if ! cat > "$tmp"; then | |
| rm -f "$tmp" | |
| return 1 | |
| fi | |
| mv -f "$tmp" "$path" |
| # either the previous complete config or the new one, never a fragment. | ||
| install_config() { | ||
| local path="$1" tmp | ||
| tmp="$(mktemp "$path.XXXXXX")" || return 1 |
There was a problem hiding this comment.
P2: Using mktemp here adds an unconditional dependency that the documented exact CHAOS_WORKDIR mode does not preflight. On a minimal supported host without mktemp, the run reaches config emission and fails only then; preflight mktemp for both modes or provide a supported fallback.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/live-chaos/lib.sh, line 318:
<comment>Using `mktemp` here adds an unconditional dependency that the documented exact `CHAOS_WORKDIR` mode does not preflight. On a minimal supported host without `mktemp`, the run reaches config emission and fails only then; preflight `mktemp` for both modes or provide a supported fallback.</comment>
<file context>
@@ -296,6 +301,25 @@ require_binaries() {
+# either the previous complete config or the new one, never a fragment.
+install_config() {
+ local path="$1" tmp
+ tmp="$(mktemp "$path.XXXXXX")" || return 1
+ cat > "$tmp"
+ mv -f "$tmp" "$path"
</file context>
| fi | ||
| echo "observability: { listen: \"$(meta_metrics_addr "$id")\" }" | ||
| } > "$cfg" | ||
| } | install_config "$cfg" |
There was a problem hiding this comment.
P2: An installation failure is hidden from callers because every emitter prints $cfg after the pipeline regardless of its status, so command-substitution callers can start nodes or CLIs with a missing or stale config. Return the pipeline failure before printing the path (for example, } | install_config "$cfg" || return 1) in each emitter.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/live-chaos/lib.sh, line 354:
<comment>An installation failure is hidden from callers because every emitter prints `$cfg` after the pipeline regardless of its status, so command-substitution callers can start nodes or CLIs with a missing or stale config. Return the pipeline failure before printing the path (for example, `} | install_config "$cfg" || return 1`) in each emitter.</comment>
<file context>
@@ -327,7 +351,7 @@ emit_meta_config() {
fi
echo "observability: { listen: \"$(meta_metrics_addr "$id")\" }"
- } > "$cfg"
+ } | install_config "$cfg"
echo "$cfg"
}
</file context>
| # either the previous complete config or the new one, never a fragment. | ||
| install_config() { | ||
| local path="$1" tmp | ||
| tmp="$(mktemp "$path.XXXXXX")" || return 1 |
There was a problem hiding this comment.
P3: Every emitted config file now silently changes permissions from the shell's default umask (typically 0644) to 0600, because mktemp creates the temp file with mode 0600 and mv preserves that mode. Today that is harmless — the harness launches vtop-node as the same user — but it is an unannounced regression across all 12 emitters, and it will break any future consumer that reads these configs as a different user (e.g. a container mount, sudo-run node, or a shared workdir). Consider pinning the final mode so the change in behavior is explicit and stable.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/live-chaos/lib.sh, line 318:
<comment>Every emitted config file now silently changes permissions from the shell's default umask (typically 0644) to 0600, because `mktemp` creates the temp file with mode 0600 and `mv` preserves that mode. Today that is harmless — the harness launches vtop-node as the same user — but it is an unannounced regression across all 12 emitters, and it will break any future consumer that reads these configs as a different user (e.g. a container mount, sudo-run node, or a shared workdir). Consider pinning the final mode so the change in behavior is explicit and stable.</comment>
<file context>
@@ -296,6 +301,25 @@ require_binaries() {
+# either the previous complete config or the new one, never a fragment.
+install_config() {
+ local path="$1" tmp
+ tmp="$(mktemp "$path.XXXXXX")" || return 1
+ cat > "$tmp"
+ mv -f "$tmp" "$path"
</file context>
b47cfb2 to
de308d3
Compare
f5fbbff to
217f974
Compare
The live-chaos harness drives real processes over real TLS — metadata Raft, replication, fencing, promotion, failover — and it has never run in CI. No workflow referenced it. It executed only when someone ran it by hand. That gap is not theoretical. Scenario 09 sat red on main from #236, scenario 10 from #237, a data node presented a metadata node's certificate to the admin endpoint across three issues, and scenario 01 failed on a scheduling race that appeared and vanished between runs. Every one of those was found by running the suite manually this week, not by CI, and each had been on main for weeks. The harness was not linted either: the `shell` filter listed `docker/*.sh` and `docker/tests/**`, so editing `lib.sh` — which all thirteen scenarios source — triggered nothing at all. Both are now covered: * `shell` gains `scripts/live-chaos/**`, and a second shellcheck step lints the harness with `-x -P scripts/live-chaos/scenarios` so the scenarios' dynamic `source` resolves. Without `-P`, every scenario reports SC1091 plus spurious "may not be assigned" warnings for variables lib.sh does define — noise that trains people to ignore the job. The tree is clean under it. * A new `chaos` filter and `live-chaos` job run the suite. Scoped, not blanket: only the harness itself and the four crates whose binaries it executes can change a scenario's outcome, so a docs or dashboard edit does not pay for a cluster. Debug binaries, because these scenarios assert correctness rather than throughput. Unprivileged user and mount namespaces are enabled and then PROBED. Ubuntu 24.04 restricts unprivileged userns by default, and scenarios 05, 05b, and 06 need it. The probe does not skip on failure: these scenarios fail with a remediation message rather than skipping, so a runner without namespaces turns the job red honestly instead of reporting success for three scenarios that never ran — which is exactly the silent gap this job exists to close. Per-scenario logs upload on failure. A failing scenario names a log file, and without the artifact that diagnosis dies with the runner. Enabled only now because the suite had to be green first: #251, #252, and #253 fixed the three real failures. Local state is 9 of 13 passing, with the four remaining failures all requiring Linux facilities absent on macOS — `unshare` for 05, 05b, 06 and a clock shim for 07 — which is what this job exists to exercise. Scenario 01 was run five times after the race fix: 5 of 5.
de308d3 to
f6ba1ba
Compare
…check-clean Scenario 01 failed intermittently with error: parse .../admin-1.yaml: missing field `endpoint` an error about the harness's own scratch file that reads like a product fault and points nowhere near its cause. Config emitters are called per invocation, not once — `meta_admin` re-emits its client config on every call. Scenario 01 runs a proposal loop in the background while the foreground drives membership changes, so two writers re-emit the same path concurrently. A plain `> "$cfg"` redirect truncates in place, leaving a window in which a reader parses a fragment. Every emitter now writes to a temp file and renames, which is atomic within a directory: a reader sees either the previous complete config or the new one. This was always latent — the same hazard existed under the old filename — and it surfaced as a scenario that passed or failed depending on scheduling. That is the worst failure mode for a suite about to gate CI, because it teaches people to re-run rather than read. Also silences the harness's one remaining shellcheck finding, with the reason rather than a blanket exclusion: the single quotes around the `unshare` probe are deliberate, since `$1` must be expanded by the inner `bash -c`. The tree is now clean under `shellcheck -x -P scripts/live-chaos/scenarios`, which is what lets the next change lint it in CI.
217f974 to
e19ecef
Compare
The live-chaos harness drives real processes over real TLS — metadata Raft, replication, fencing, promotion, failover — and it has never run in CI. No workflow referenced it. It executed only when someone ran it by hand. That gap is not theoretical. Scenario 09 sat red on main from #236, scenario 10 from #237, a data node presented a metadata node's certificate to the admin endpoint across three issues, and scenario 01 failed on a scheduling race that appeared and vanished between runs. Every one of those was found by running the suite manually this week, not by CI, and each had been on main for weeks. The harness was not linted either: the `shell` filter listed `docker/*.sh` and `docker/tests/**`, so editing `lib.sh` — which all thirteen scenarios source — triggered nothing at all. Both are now covered: * `shell` gains `scripts/live-chaos/**`, and a second shellcheck step lints the harness with `-x -P scripts/live-chaos/scenarios` so the scenarios' dynamic `source` resolves. Without `-P`, every scenario reports SC1091 plus spurious "may not be assigned" warnings for variables lib.sh does define — noise that trains people to ignore the job. The tree is clean under it. * A new `chaos` filter and `live-chaos` job run the suite. Scoped, not blanket: only the harness itself and the four crates whose binaries it executes can change a scenario's outcome, so a docs or dashboard edit does not pay for a cluster. Debug binaries, because these scenarios assert correctness rather than throughput. Unprivileged user and mount namespaces are enabled and then PROBED. Ubuntu 24.04 restricts unprivileged userns by default, and scenarios 05, 05b, and 06 need it. The probe does not skip on failure: these scenarios fail with a remediation message rather than skipping, so a runner without namespaces turns the job red honestly instead of reporting success for three scenarios that never ran — which is exactly the silent gap this job exists to close. Per-scenario logs upload on failure. A failing scenario names a log file, and without the artifact that diagnosis dies with the runner. Enabled only now because the suite had to be green first: #251, #252, and #253 fixed the three real failures. Local state is 9 of 13 passing, with the four remaining failures all requiring Linux facilities absent on macOS — `unshare` for 05, 05b, 06 and a clock shim for 07 — which is what this job exists to exercise. Scenario 01 was run five times after the race fix: 5 of 5.
* ci: run and lint the live-chaos harness, path-gated The live-chaos harness drives real processes over real TLS — metadata Raft, replication, fencing, promotion, failover — and it has never run in CI. No workflow referenced it. It executed only when someone ran it by hand. That gap is not theoretical. Scenario 09 sat red on main from #236, scenario 10 from #237, a data node presented a metadata node's certificate to the admin endpoint across three issues, and scenario 01 failed on a scheduling race that appeared and vanished between runs. Every one of those was found by running the suite manually this week, not by CI, and each had been on main for weeks. The harness was not linted either: the `shell` filter listed `docker/*.sh` and `docker/tests/**`, so editing `lib.sh` — which all thirteen scenarios source — triggered nothing at all. Both are now covered: * `shell` gains `scripts/live-chaos/**`, and a second shellcheck step lints the harness with `-x -P scripts/live-chaos/scenarios` so the scenarios' dynamic `source` resolves. Without `-P`, every scenario reports SC1091 plus spurious "may not be assigned" warnings for variables lib.sh does define — noise that trains people to ignore the job. The tree is clean under it. * A new `chaos` filter and `live-chaos` job run the suite. Scoped, not blanket: only the harness itself and the four crates whose binaries it executes can change a scenario's outcome, so a docs or dashboard edit does not pay for a cluster. Debug binaries, because these scenarios assert correctness rather than throughput. Unprivileged user and mount namespaces are enabled and then PROBED. Ubuntu 24.04 restricts unprivileged userns by default, and scenarios 05, 05b, and 06 need it. The probe does not skip on failure: these scenarios fail with a remediation message rather than skipping, so a runner without namespaces turns the job red honestly instead of reporting success for three scenarios that never ran — which is exactly the silent gap this job exists to close. Per-scenario logs upload on failure. A failing scenario names a log file, and without the artifact that diagnosis dies with the runner. Enabled only now because the suite had to be green first: #251, #252, and #253 fixed the three real failures. Local state is 9 of 13 passing, with the four remaining failures all requiring Linux facilities absent on macOS — `unshare` for 05, 05b, 06 and a clock shim for 07 — which is what this job exists to exercise. Scenario 01 was run five times after the race fix: 5 of 5. * ci: run CI on every pull request, not only those targeting main `pull_request: branches: [main]` meant a stacked PR ran no CI whatsoever. This project stacks slices — each bases on the one below — so those PRs carried a single check, an AI reviewer's comment, and nothing else. They could be reviewed, approved, and merged with no build, no tests, and no lint. Nor did merging the parent repair it. Retargeting a PR's base fires the `edited` activity type, which is not in the default `pull_request` event set, so the retargeted PR did not reliably re-trigger CI either. The first real signal arrived on push to main — after the merge, on the branch that is supposed to stay green. The path filters already scope each run, so the added cost is only the jobs a stacked slice actually needs. This is the same shape as the gap the previous commit closes: a check that exists and is trusted, but never runs on the change it is meant to guard. * ci: make the live-chaos log artifact actually contain logs The upload step collected nothing. Scenarios delete workdirs they generated themselves, so by the time the step ran the `/tmp/vtop-chaos.*` tree was gone — and `if-no-files-found: ignore` turned that into a silent success. The first run confirmed it: the job passed and produced no artifact at all. A diagnostic that appears to exist and is absent exactly when it is needed is worse than none, because it stops anyone from looking for a real one. Supplying CHAOS_WORKDIR fixes both halves: the logs land in a known path, and because the harness only cleans up directories it generated itself, providing one suppresses the cleanup. `if-no-files-found` becomes `error`, so a future break in this wiring fails the job instead of quietly returning to collecting nothing. Upload is now `if: failure()` — the logs are for diagnosing a failed run, and uploading them on every green run is storage for nobody. Verified by running a scenario with CHAOS_WORKDIR set and confirming the log tree survives. * ci: widen the chaos filter to the real dependency closure, and gate the upload correctly Two review findings, both correct. The chaos filter named four crates and asserted that nothing else could change a scenario's outcome. That assertion was false. The two binaries this job runs pull in every workspace crate: cargo tree -p vtop-node --edges normal → broker, log, meta, observe, protocol cargo tree -p vtop-cli --no-default-features --edges normal → adapters, broker, core, log, meta, observe, protocol, state, upload The omissions were not marginal. vtop-log owns the segment format the durability and recovery scenarios assert on, and vtop-protocol owns the produce/fetch wire every scenario speaks — either could have broken the suite with this job never running, which is precisely the hole this job exists to close. The filter is now `crates/**`, derived from the closure rather than from a guess about it. That means the suite runs on most Rust changes. That is the honest cost of it being the only thing validating replication and fencing against real processes; a cheaper gate with a hole in it is what was just removed. The log upload was gated on `failure()`, which is also true when the namespace probe or the build failed. In those cases the suite never reached a scenario, so no logs exist, and `if-no-files-found: error` would stack a spurious "no files found" on top of the actual cause. It now keys on the run step's own outcome, so the empty-artifact signal only fires once a run was actually attempted — which is the only situation in which it means anything.
Stacked on #252 → #251 → #250. Bases retarget automatically as each merges.
Scenario 01 failed intermittently with:
An error about the harness's own scratch file, which reads like a product fault and points nowhere near its cause.
The race
Config emitters are called per invocation, not once —
meta_adminre-emits its client config on every call. Scenario 01 runs a proposal loop in the background while the foreground drives membership changes:Two writers re-emit the same path concurrently, and
> "$cfg"truncates in place — leaving a window where a reader parses a fragment. All 12 emitters now write to a temp file and rename, which is atomic within a directory: a reader sees either the previous complete config or the new one, never a partial.This was always latent; the same hazard existed under the old filename. It surfaced as a scenario that passed or failed depending on scheduling — the worst failure mode for a suite about to gate CI, because it teaches people to re-run instead of read.
Shellcheck-clean
Also silences the harness's one remaining finding with its reason rather than a blanket exclusion: the single quotes around the
unshareprobe are deliberate, because$1must be expanded by the innerbash -c, not the outer shell.The tree is now clean under:
-Pmatters — without it,source "$(dirname …)/../lib.sh"can't be resolved and every scenario reports SC1091 plus spurious SC2153 "may not be assigned" warnings for variables lib.sh does define. That is what lets the next change lint this tree in CI without drowning it in false positives.Verification
Scenario 01 passes. Full-suite re-run in progress; the three scenarios fixed across this stack (09, 10, 11) all pass, and the remaining local failures are the four that need Linux (
unsharefor 05/05b/06, a clock shim for 07) and are expected to run on CI's ubuntu runners.Summary by cubic
Install atomic config writes in the chaos harness to prevent partial YAML reads during concurrent emits, and clean up the last shellcheck warning. This removes the flaky Scenario 01 error and makes the harness ready for CI linting.
install_configto write via temp file + rename, avoiding truncation races (no more intermittent “missing fieldendpoint”).require_mount_namespacewith a targetedSC2016disable around theunshareprobe so$1expands in the innerbash -c.Written for commit e19ecef. Summary will update on new commits.