Skip to content

node: co-locate the metadata and data roles in one process (#215) - #237

Merged
allamiro merged 1 commit into
mainfrom
feat/215-colocated-node
Aug 5, 2026
Merged

node: co-locate the metadata and data roles in one process (#215)#237
allamiro merged 1 commit into
mainfrom
feat/215-colocated-node

Conversation

@allamiro

@allamiro allamiro commented Aug 4, 2026

Copy link
Copy Markdown
Owner

First of #215's remaining gaps: "the live metadata and data processes are still separate".

A live 3-node cluster meant six processes — a meta and a data invocation per machine, with separate configs, ready markers, and /metrics endpoints. That is not how anyone deploys this, and the gap mattered for more than tidiness: the harness never exercised the two planes sharing a process, a runtime, and a fate.

vtop-node node --config runs both.

One observability surface

An operator scraping a host finds one target and does not have to know which roles happen to share it. A per-role observability block is rejected rather than silently ignored — a config naming three listen addresses and getting one belongs to an author with a wrong model of the process, and picking a winner would hide that.

Shared fate, made explicit

Whichever role exits first ends the process, carrying its error. A metadata voter that has died inside a process still serving data is worse than a dead process: the cluster keeps counting it toward quorum while it answers nothing. Exiting makes the failure legible to whatever supervises the node.

What co-location deliberately does not change

The data plane still reaches metadata through the admin endpoint exactly as it would across a network, even when that endpoint is this same process. An in-memory shortcut would make the co-located path diverge from the distributed one precisely where the harness is meant to prove they agree.

Mechanics

Each role's run splits into a serve taking a caller-owned observability surface, and the readiness probe moves behind a shared cell — so a probe can be installed after the endpoint is already serving, and every role holds a borrow rather than one of them owning it.

Refs #215.


Summary by cubic

Co-locates the metadata voter and data replica in one vtop-node process to match real deployments and exercise shared runtime/fate (addresses #215). Adds a single observability endpoint with readiness as the conjunction of both roles, plus a chaos scenario that proves the single endpoint and shared‑fate restart.

  • New Features

    • vtop-node node --config runs metadata and data in one process.
    • One /metrics and /readyz; readiness opens only when both roles have started.
    • Shared fate: if either role exits, the process exits with its error.
    • New live chaos scenario 10-colocated-node verifies single endpoint, readiness conjunction, admin RPCs, and post-restart data integrity.
  • Migration

    • For co-located mode, set a top-level observability block; per-role observability is rejected (including an empty {}).
    • Existing meta and data commands are unchanged; each still exposes its own endpoint.
    • Operators should scrape one target per host.

Written for commit 8003ea2. Summary will update on new commits.

Review in cubic

@cursor

cursor Bot commented Aug 4, 2026

Copy link
Copy Markdown

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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 97898d8a60

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +97 to +100
result = meta_node::serve(meta, &observability, metrics_addr) => {
result.map_err(|error| format!("metadata role exited: {error}"))
}
result = data_node::serve(data, &observability, metrics_addr) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require both colocated roles before reporting ready

In node mode this passes the same NodeObservability to both roles, but each role flips observability.gate.mark_ready() independently (meta_node::serve after only the meta listeners bind, and data_node::serve after only the data listener binds). When the metadata side wins startup, the shared /readyz begins returning ready before the data native/replica listener (and, for leaders, its readiness probe) is installed, so a load balancer or harness can route traffic to a half-started co-located process instead of the promised conjunction. Give each role its own startup gate and AND them in the shared source.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. ReadinessGate gained a startup latch: require_marks(n) declares how many distinct mark_ready calls open the gate, and the co-located runner sets 2 before either role starts. The first role to finish binding now leaves /readyz at 503 with "waiting for 1 more component(s) to finish starting" instead of advertising a half-started process; post-startup the gate is an ordinary level again. Unit test a_gate_requiring_two_marks_opens_only_on_the_second pins the whole arc, and live-chaos scenario 10 now boots the co-located binary and gates on the shared /readyz before asserting both planes serve.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 5 files

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="crates/vtop-node/src/main.rs">

<violation number="1" location="crates/vtop-node/src/main.rs:48">
P2: The live-chaos harness still starts separate `meta` and `data` processes, so this new command is never exercised and the advertised six-to-three process change is not covered. Wiring the launcher/config generators to invoke `vtop-node node --config` with one shared observability target and readiness marker would make the harness validate this path.</violation>
</file>

<file name="crates/vtop-node/src/colocated.rs">

<violation number="1" location="crates/vtop-node/src/colocated.rs:57">
P2: An empty per-role `observability: {}` or `listen: null` is accepted and ignored, contrary to the documented contract that any per-role observability block is rejected. Preserve field presence during deserialization and reject a supplied block, not only a non-null listen address.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread crates/vtop-node/src/data_node.rs
/// Run BOTH roles in one process — a metadata voter and a data-plane
/// replica sharing a runtime, one observability endpoint, and a fate
/// (#215).
Node {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The live-chaos harness still starts separate meta and data processes, so this new command is never exercised and the advertised six-to-three process change is not covered. Wiring the launcher/config generators to invoke vtop-node node --config with one shared observability target and readiness marker would make the harness validate this path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/vtop-node/src/main.rs, line 48:

<comment>The live-chaos harness still starts separate `meta` and `data` processes, so this new command is never exercised and the advertised six-to-three process change is not covered. Wiring the launcher/config generators to invoke `vtop-node node --config` with one shared observability target and readiness marker would make the harness validate this path.</comment>

<file context>
@@ -41,6 +42,13 @@ enum Command {
+    /// Run BOTH roles in one process — a metadata voter and a data-plane
+    /// replica sharing a runtime, one observability endpoint, and a fate
+    /// (#215).
+    Node {
+        #[arg(long)]
+        config: PathBuf,
</file context>

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed with a new scenario rather than by rewiring every existing one: live-chaos 10-colocated-node boots vtop-node node hosting both roles (composed by the same config emitters the split-process scenarios use, per-role observability stripped since the runner rejects it), gates on the single shared /readyz, asserts one /metrics scrape carries BOTH planes (vtop_meta_raft_state and vtop_broker_local_committed_offset), drives admin RPCs and a produce through it, then SIGKILLs the process and verifies every acknowledged record survives the shared-fate restart byte-exactly. The existing scenarios keep the split topology deliberately — chaos that kills one plane at a time needs separate processes — so the six-to-three claim is validated by 10 while 00-09 keep exercising the distributed shape.

Comment thread crates/vtop-node/src/colocated.rs Outdated
// Fail loudly rather than picking a winner. A config that names three
// listen addresses and gets one is a config whose author has a wrong
// model of the process.
if self.meta.observability.listen.is_some() || self.data.observability.listen.is_some() {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: An empty per-role observability: {} or listen: null is accepted and ignored, contrary to the documented contract that any per-role observability block is rejected. Preserve field presence during deserialization and reject a supplied block, not only a non-null listen address.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/vtop-node/src/colocated.rs, line 57:

<comment>An empty per-role `observability: {}` or `listen: null` is accepted and ignored, contrary to the documented contract that any per-role observability block is rejected. Preserve field presence during deserialization and reject a supplied block, not only a non-null listen address.</comment>

<file context>
@@ -0,0 +1,177 @@
+        // Fail loudly rather than picking a winner. A config that names three
+        // listen addresses and gets one is a config whose author has a wrong
+        // model of the process.
+        if self.meta.observability.listen.is_some() || self.data.observability.listen.is_some() {
+            return Err(
+                "a co-located node exposes ONE observability endpoint: set it at the top \
</file context>

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed by making presence survive deserialization: the per-role observability fields are now Option<ObservabilityConfig>, and the co-located validation rejects is_some() — so observability: {} and observability: { listen: null } are refused exactly like a bound address, because an author who wrote the key at all believed the role owns an endpoint, and silently ignoring that belief hides the wrong model instead of correcting it. Test an_empty_per_role_observability_block_is_still_rejected pins it; the standalone meta/data commands default an absent block as before.

Comment thread crates/vtop-node/src/colocated.rs
@allamiro
allamiro force-pushed the feat/223-failover-scenario branch from c1da48f to 9954d9e Compare August 5, 2026 12:55
@allamiro
allamiro force-pushed the feat/215-colocated-node branch from 97898d8 to d4205c4 Compare August 5, 2026 13:04

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d4205c43d0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread scripts/live-chaos/lib.sh Outdated
local id="$1"; shift
local cfg="$WORKDIR/colocated-$id.yaml"
emit_meta_config "$id" "$@" > /dev/null
emit_leader_config standalone > /dev/null

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Generate per-node data configs for colocated hosts

When adding a second co-located host in the chaos harness, every call still uses emit_leader_config standalone, which writes the singleton data config (data-leader, LEADER_UUID, native_addr, replica_addr 0, and data_metrics_addr 0) regardless of the metadata id passed above. That makes start_colocated_node 2 ... try to bind the same data/metrics ports, or reuse the same segment directory if run sequentially, so the helper cannot exercise the advertised three-process deployment; it needs per-id data UUIDs, dirs, and ports.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed. emit_colocated_config no longer reuses the singleton standalone config: node uuid, certificate, data directory (data-colocated-$id), native port (colocated_native_addr, one per host), replica-status port, and the observability endpoint all derive from the id, and ids outside 1-3 are refused loudly. One honest scope note, in the helper where it belongs: each host still carries an independent STANDALONE range, because a replicated range under co-location needs follower-side epoch propagation — that is #239, and scenario 10 keeps validating the process shape (one scrape, one /readyz conjunction, shared fate) until it lands.

Comment thread scripts/live-chaos/lib.sh Outdated
echo "meta:"
# `peers:` with no entries is YAML null, which the typed config refuses;
# dropping the bare key gives the field its (empty) default instead.
sed -e '/^observability:/d' -e '/^peers:$/d' -e 's/^/ /' "$WORKDIR/meta-$id.yaml"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the peers key when peer entries exist

When this helper is called with any peer ids, this sed unconditionally deletes the peers: key but leaves the indented - { ... } entries emitted underneath it. The generated meta: section then contains orphaned list items under the previous field and vtop-node node --config fails YAML parsing instead of starting a multi-node co-located metadata cluster. Only drop the bare key when there are no peers, or keep peers: with its entries.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — the peers: key is now dropped only when the helper is called with NO peer ids (where the bare key would deserialize as YAML null and be refused); with peers present the key and its entries pass through intact, so a multi-node co-located metadata group parses. The comment at the sed says both halves of the reason.

@allamiro
allamiro force-pushed the feat/223-failover-scenario branch from 9954d9e to 1b63b31 Compare August 5, 2026 13:23
@allamiro
allamiro force-pushed the feat/215-colocated-node branch from d4205c4 to 343d934 Compare August 5, 2026 13:25
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@allamiro allamiro added this to the v0.1.0 milestone Aug 5, 2026
@allamiro allamiro self-assigned this Aug 5, 2026
@allamiro
allamiro force-pushed the feat/223-failover-scenario branch from 1b63b31 to 42f00bd Compare August 5, 2026 14:05
@allamiro
allamiro changed the base branch from feat/223-failover-scenario to main August 5, 2026 14:18
Carries the first of #215's remaining gaps: "the live metadata and data
processes are still separate". A live 3-node cluster meant six processes — a
`meta` and a `data` invocation per machine, with separate configs, ready
markers, and `/metrics` endpoints. That is not how anyone deploys this, and the
gap mattered for more than tidiness: the harness never exercised the two planes
sharing a process, a runtime, and a fate.

`vtop-node node --config` runs both. One registry, one gate, one endpoint, so an
operator scraping a host finds one target and does not have to know which roles
happen to share it. A per-role `observability` block is rejected rather than
silently ignored — a config naming three listen addresses and getting one
belongs to an author with a wrong model of the process, and picking a winner
would hide that.

Shared fate is explicit: whichever role exits first ends the process, carrying
its error. A metadata voter that has died inside a process still serving data is
worse than a dead process, because the cluster keeps counting it toward quorum
while it answers nothing. Exiting makes the failure legible to whatever
supervises the node.

What co-location deliberately does NOT change: the data plane still reaches
metadata through the admin endpoint exactly as it would across a network, even
when that endpoint is this same process. An in-memory shortcut would make the
co-located path diverge from the distributed one precisely where the harness is
meant to prove they agree.

Mechanically this splits each role's `run` into a `serve` that takes a
caller-owned observability surface, and moves the readiness probe behind a
shared cell so a probe can be installed after the endpoint is already serving
and so every role can hold a borrow rather than one of them owning it.
@allamiro
allamiro force-pushed the feat/215-colocated-node branch from 343d934 to 8003ea2 Compare August 5, 2026 14:19
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@allamiro
allamiro merged commit 45fdf03 into main Aug 5, 2026
16 checks passed
@allamiro
allamiro deleted the feat/215-colocated-node branch August 5, 2026 14:27
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
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.
allamiro added a commit that referenced this pull request Aug 7, 2026
A StatefulSet of vtop-node node processes — the co-located shape #237
built — with a headless Service for stable peer DNS, per-plane volume
claims, meta node ids derived from the pod ordinal, and a config-checksum
annotation so config changes roll the set. Readiness is /readyz and the
chart documents that a fenced node going unready is correct behaviour to
route around, not a probe to loosen; liveness is /healthz; a startup
probe covers cold recovery.

Nothing identity-shaped has a default: TLS secret names, the cluster id,
per-replica broker UUIDs, range identities, and the client principal are
all required, refuse rendering with the exact contract in the message,
and are shape-checked by values.schema.json — a chart that defaulted any
of them would be shipping credentials, which is issue #81's lesson. The
PodDisruptionBudget holds maxUnavailable at 1 because three voters are a
quorum; pods run non-root with a read-only rootfs; the ServiceMonitor is
gated behind both a value and the CRD's presence.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant