meta: an uninitialized membership is readable, not a storage error (scenario 10) - #252
Merged
Merged
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. |
allamiro
force-pushed
the
fix/09-post-failover-produce
branch
from
August 5, 2026 19:27
62e9d9f to
b3b66eb
Compare
allamiro
force-pushed
the
fix/10-uninitialized-membership
branch
from
August 5, 2026 19:27
b47cfb2 to
de308d3
Compare
allamiro
added a commit
that referenced
this pull request
Aug 5, 2026
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.
…cenario 10) `vtopctl meta status` against a freshly started metadata node failed with "when Write Logs: joint membership with 0 configs cannot be stored in MetaMembership" — a storage write error, reported by a read, on the first command an operator runs. Bootstrap hit the same fault: the co-located scenario (#215) has been unable to `init` since it landed. Zero joint configs is not corruption. It is exactly how openraft represents a node that has never been initialized; a fresh node's startup state carries `Membership { configs: [], nodes: {} }` until `init` lands. `membership_to_meta` rejected it, and that converter runs on the metrics and status paths as well as the log-append path, so every read of an uninitialized node became an error. It now maps zero configs to an empty MetaMembership, which describes the state truthfully. Whether an empty membership may be PERSISTED is a different question with a different answer, so it is now asked at the write boundary: `entry_to_meta` refuses a membership log entry with no voters, because a committed entry naming no voters describes a cluster in which no quorum can ever form, and replaying it would strand the group. Openraft does not produce one; if that changes, this fails loudly instead of writing it. The remaining failures keep the strict error but report it as a READ fault. `sto_err_logs` labelled every failure from this converter "when Write Logs", including failures from paths that never write a log — which is why the original symptom pointed at the storage layer, where nothing was wrong. Bootstrap had a second defect the first one masked. `init` read the membership back from the metrics watch channel, which openraft publishes to asynchronously, so a read taken the instant `initialize` returns can still observe the pre-init state. A successful bootstrap could therefore answer "voters: []". The race is wide open on a single-member group, which needs no peer round trip to initialize — which is why the co-located scenario, the only one that bootstraps one member, is the only one that hit it. `init` now waits (bounded, 2s) for a membership with voters, and on expiry returns what it last saw rather than inventing an error for an operation that already succeeded. Scenario 10 asserts on the reported voter rather than the exit code, so the wait is covered: reverting it fails the scenario. Three unit tests cover the converter — uninitialized reads as empty, empty is refused at the write boundary, and an impossible config count is reported as a read fault.
allamiro
force-pushed
the
fix/10-uninitialized-membership
branch
from
August 5, 2026 19:28
de308d3 to
f6ba1ba
Compare
allamiro
added a commit
that referenced
this pull request
Aug 5, 2026
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.
allamiro
added a commit
that referenced
this pull request
Aug 5, 2026
* 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #251 (which is stacked on #250). Bases retarget automatically as each merges.
vtopctl meta statusagainst a freshly started metadata node fails with:A storage write error, reported by a read, on the first command an operator runs. Bootstrap hits the same fault — the co-located scenario (#215) has been unable to
initsince it landed.Zero configs is not corruption
It is exactly how openraft represents a node that has never been initialized. A fresh node's startup state carries
Membership { configs: [], nodes: {} }:membership_to_metarejected that, and the converter runs on the metrics and status paths as well as the log-append path — so every read of an uninitialized node became an error. It now maps zero configs to an emptyMetaMembership, which describes the state truthfully.Whether an empty membership may be persisted is a different question with a different answer, so it is now asked at the write boundary:
entry_to_metarefuses a membership log entry with no voters. A committed entry naming no voters describes a cluster in which no quorum can ever form, and replaying it would strand the group. Openraft does not produce one — if that changes, this fails loudly rather than writing it.The misleading label is part of the bug
sto_err_logslabelled every failure from this converterwhen Write Logs, including failures from paths that never write a log. That is why the symptom pointed at the storage layer, where nothing was wrong. Genuinely impossible config counts still error, now reported as a read fault.A second defect the first one masked
initread the membership back from the metrics watch channel, which openraft publishes to asynchronously. A read taken the instantinitializereturns can still observe the pre-init state, so a successful bootstrap could answervoters: [].The race is wide open on a single-member group, which needs no peer round trip to initialize — which is why the co-located scenario, the only one that bootstraps one member, is the only one that hit it.
initnow waits (bounded, 2s) for a membership with voters, and on expiry returns what it last saw rather than inventing an error for an operation that already succeeded.Verification
Scenario 10 passes. It now asserts on the reported voter rather than the exit code, which is what makes the second fix testable — an exit code alone passes on
voters: []. Mutation-checked: revertinginitto the immediate read fails the scenario.Three unit tests cover the converter:
Why this was invisible
The live-chaos harness never runs in CI, so scenario 10 has been red on
mainsince #237 without signal. Wiring it in is the next change; this PR and #251 are the two failures that had to be fixed before that suite could be enabled without turningmainred.Summary by cubic
Fixes false storage errors on fresh metadata nodes by treating zero-config memberships as readable and only rejecting them when writing to the log. Also makes
initreturn the actual voter by waiting briefly for metrics, fixing scenario 10.membership_to_metamaps zero configs to an emptyMetaMembership; impossible config counts are reported as READ faults.entry_to_metarefuses to persist membership entries with no voters.init: waits up to 2s for a published membership with voters; on timeout, returns the last seen state.Written for commit f6ba1ba. Summary will update on new commits.