Skip to content

helm,ci,k8s-smoke: the chart stops encoding the leader — replicated renders candidates (#284) - #344

Open
allamiro wants to merge 4 commits into
mainfrom
feat/284-chart-candidates
Open

helm,ci,k8s-smoke: the chart stops encoding the leader — replicated renders candidates (#284)#344
allamiro wants to merge 4 commits into
mainfrom
feat/284-chart-candidates

Conversation

@allamiro

@allamiro allamiro commented Aug 11, 2026

Copy link
Copy Markdown
Owner

The chart slice of #284, and the last piece of it: #343 put the role inside the binary; this makes the chart stop encoding it, which values.yaml itself promised ('the chart stops encoding it at all').

What changes

  • data.leaderOrdinal is retired, loudly. Setting it fails the render with the migration story. A value that used to steer roles must not be silently ignored.
  • replicated renders every pod as a candidate with one shared peer list (self included; the binary skips its own entry, and identical lists keep the rendered configs diffable). Failover is the binary's job now: whichever pod acquires the lease leads, and when the holder dies a survivor takes the range in place — no re-render, no upgrade, no restart.
  • Two more renders-then-cannot-work shapes fail at render time: replicated without lease.enabled (no pod would ever lead) and a nonzero fencingEpoch (grants are minted from 1; a floor at or above the first grant refuses the very grant that makes a candidate lead).
  • CI render check asserts the symmetric shape — still parsing rather than pattern-matching, for the recorded reason — and the refusal checks cover all three guards by message.
  • The k8s smoke test stops assuming pod 0 leads (nothing renders that anymore): it asks metadata for the holder, produces against it, then deletes the holder's pod and proves the failover — graceful delete so the node: vtop-node ignores SIGTERM, so every orderly stop is a crash stop #280 drain releases rather than lapses, wait past the corpse for a live holder, produce 30 more records against whoever leads now, and assert every replica including the recreated pod converges on all 90. A recreated pod winning its own range back is a legitimate race outcome and is logged as such; live-chaos scenario 14 already proves takeover-by-a-survivor under a kill no orchestrator softens.
  • Chart version 0.2.0 (values-contract change), NOTES.txt gains the replicated post-install walkthrough, ROADMAP records the shipped state.

Validation

  • helm lint + full render parse-verified locally (three identical leased candidates, epoch 0, distinct per-pod addrs); all three refusals fired with their messages.
  • The kind deploy job carries the live proof of the failover phase.

Closes #284.


Summary by cubic

Switch the replicated Helm topology to symmetric candidates that follow the metadata lease at runtime (#284). Retires data.leaderOrdinal, adds a non-blocking candidate metrics collector with a new vtop_broker_candidate_leading gauge, hardens metrics semantics during role transitions, and tightens CI/smoke to verify election, in-place failover, and observability. Chart version 0.2.0.

  • New Features

    • Helm: render every pod as candidate with one shared peers list (self included); require data.lease.enabled=true and data.fencingEpoch=0; refuse any data.leaderOrdinal in all topologies; NOTES updated.
    • Node: add a process-lifetime CandidateCollector that exports existing broker metrics across role changes; add vtop_broker_candidate_leading; make broker_lease_active reflect the authorized holder (role + epoch) and fail closed between roles; keep progress gauges monotonic (no mid-transition rewind of broker_held_fencing_epoch); all reads non-blocking.
    • CI/Smoke: render check enforces identical peer lists, unique addrs, and peer/pod UUIDs matching the configured set; smoke asks metadata for the holder, produces with quorum, deletes the holder, waits for a higher epoch, then retries produce with bounded attempts and per-attempt port-forwards until the new holder’s streams are up; logs member count at write time; distinguishes API failure vs pod unreadiness; verifies all replicas (including the restarted pod) converge; nodes register at their own FQDNs.
  • Migration

    • Remove data.leaderOrdinal from your values.
    • For replicated, set data.lease.enabled=true and data.fencingEpoch=0.
    • Register each node at its own headless FQDN, and use metadata to find the current lease holder before producing.

Written for commit 001bc32. Summary will update on new commits.

Review in cubic

…enders candidates (#284)

The binary's role has followed the metadata lease since #343; this is
the chart catching up, which values.yaml itself promised: 'the durable
fix is for the role to follow the lease inside the binary, so the
chart stops encoding it at all.'

data.leaderOrdinal is RETIRED, not ignored: setting it fails the
render with the migration story, because a value that used to steer
roles deserves an explanation, not silence. The replicated topology
now renders every pod as a CANDIDATE with one shared peer list (self
included — the binary skips its own entry, and identical lists keep
the configs diffable), and two more renders-then-cannot-work shapes
fail at render time: replicated without the lease (no pod would ever
lead) and a nonzero fencing floor (grants are minted from 1, and a
floor at or above the first grant refuses the very grant that makes a
candidate lead).

The CI render check now asserts the symmetric shape — parsing, not
pattern-matching, for the reason recorded beside it — and the refusal
checks cover all three guards by their messages.

The k8s smoke test stops assuming pod 0 leads, because nothing renders
that anymore: it asks metadata for the holder, aims the produce there,
and then does the thing the topology exists for — deletes the holder's
pod gracefully (the #280 drain releases the lease rather than lapsing
it), waits out the corpse for a live holder, produces thirty more
records against whoever that is, and asserts every replica including
the recreated pod converges on all ninety. A recreated pod that wins
its own range back is a legitimate outcome of that race and is logged
as such; scenario 14 already proves the takeover-by-a-survivor path
with a kill no orchestrator softens.

Chart version 0.2.0: a values-contract change earns the minor bump.

Rendered and refusal-checked locally; the kind deploy job carries the
live proof.
@cursor

cursor Bot commented Aug 11, 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: 15ebe407f8

ℹ️ 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 helm/vtop/templates/_helpers.tpl Outdated
Comment on lines +193 to +195
{{- if eq $v.data.topology "replicated" }}
{{- /* Both of these render perfectly well and then cannot work, which is the
worst kind of configuration error: a leaderOrdinal outside the replica
set leaves EVERY pod a follower and the range with no leader at all,
and a single-replica replicated install gives a leader with no
followers, which is standalone wearing the wrong name. */ -}}
{{- if ge (int $v.data.leaderOrdinal) (int $v.replicaCount) }}
{{- fail (printf "\n\ndata.leaderOrdinal is %d but replicaCount is %d: the leader must be one of the pods, or the range has no leader at all." (int $v.data.leaderOrdinal) (int $v.replicaCount)) }}
{{- if hasKey $v.data "leaderOrdinal" }}
{{- fail "\n\ndata.leaderOrdinal is retired (#284): \"replicated\" now renders every pod as a CANDIDATE and the role follows the metadata lease, so failover no longer needs a re-render. Remove the value." }}

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 Reject retired leaderOrdinal regardless of topology

Move this retirement check outside the replicated branch. With data.topology=standalone, a values file can still set data.leaderOrdinal and render successfully, contrary to the new contract that any retained value must fail loudly; this leaves stale configuration unnoticed until a later switch to replicated unexpectedly fails.

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.

Verified real, fixed in c9a1bb3: the guard moves BEFORE the topology branch. Retired is retired in every shape — a standalone values file carrying the key rendered happily and would have ambushed its author the day they switched to replicated, which is the moment they can least afford a surprise. Both cases are now exercised in CI (standalone and replicated) by message.

Comment thread scripts/k8s-smoke.sh Outdated
Comment on lines +813 to +814
read -r NEW_HOLDER NEW_EPOCH <<< "$(await_holder)" \
|| fail "the range had no holder 180s after the pod delete"

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 Wait for a lease generation newer than the deleted holder's

When the best-effort graceful lease release times out or is refused, metadata legitimately retains the deleted holder and its old epoch until expiry, but this call accepts that stale pair immediately because it passes no exclusion. The recreated pod can then become Ready as a follower after another candidate wins, while r_produce still targets the old holder with the stale epoch and reports a false failover failure; accept the same UUID only after its epoch advances, or a different UUID.

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.

Verified real, and the sharper form of it: my own helper took an exclusion parameter and the call site did not pass it. Fixed in c9a1bb3 with a stronger condition than exclusion — wait for a fencing epoch ABOVE the one observed before the delete. Metadata mints epochs monotonically, so 'epoch > old epoch' is exactly 'somebody has been granted the range since', and it holds whether a survivor took over or the recreated pod won its own range back (an exclusion would wrongly reject that legitimate outcome and turn it into a flake). The failure message now names the epoch it waited above.

@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.

All reported issues were addressed across 9 files

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

Re-trigger cubic

Comment thread .github/workflows/ci.yml
Comment thread helm/vtop/templates/_helpers.tpl
Comment thread scripts/k8s-smoke.sh Outdated
Comment thread helm/vtop/values.yaml
…collector the code promised, and four review fixes

The kind deploy failed on the new failover phase, and the failure was
real: quorum produce succeeded on the holder, and every pod then
reported nothing. Candidate mode registered NO role collector — the
first slice said so in a comment and named the follow-up — so
vtop_broker_local_committed_offset simply did not exist on a candidate
pod. That is the metric this repo's own chaos harness reads to choose a
promotion target, the metric the k8s smoke asserts, and the metric
every dashboard draws. A replica nobody can measure is one nobody can
operate, and the gap survived a ten-round review because nothing ever
asked the question.

The follow-up is now the fix. ONE collector, registered for the life of
the process, reading through the switching view: registering a
role-specific collector per transition never could have worked — the
leader's and follower's collectors export the same descriptors, the
registry refuses duplicates, and there is no unregister — so the first
transition would either fail or leave the process exporting a role it
no longer had. Reading through the view instead keeps the NAMES stable
across every transition, which is what lets a panel built for a
statically-rendered range keep working against a candidate deployment.
It is deliberately not the promotion probe's trait: that one blocks on
purpose because a probe must have the true offset, while a scrape must
never block and can afford a stale gauge. A new
broker_candidate_leading answers the one question candidate mode adds —
who serves the range — from metrics alone. Scenario 14 now asks all of
it of BOTH roles, because this suite is the composition proof and this
walked straight through it.

The four review findings, every one verified:

data.leaderOrdinal was only refused under the replicated topology, so a
standalone values file carrying it rendered happily and would ambush
its author the day they switched. Retired is retired: the guard moves
before the branch.

The smoke's failover wait passed no exclusion, so it could accept the
DELETED holder's still-unexpired lease and then produce at a stale
epoch — my own helper offered the parameter and the call site did not
use it. The fix is sharper than an exclusion: wait for an epoch above
the one we saw. Metadata mints epochs monotonically, so that is exactly
"somebody has been granted the range since", and it stays true whether
a survivor took over or the recreated pod won its own range back.

The CI render check proved three distinct ADDRESSES but never checked
identities — three addresses under two UUIDs would render, deploy, and
count a two-node quorum as three. Peer and pod identities are now
checked against the configured set.

And values.yaml said in one paragraph that automatic failover was not
backed while the next promised failover without a re-render. The
preamble is corrected, the lease block's documentation moved back onto
the key it describes forty lines below it, and the fencing floor's
render-time requirement stated where somebody would hit it.

Scenario 14 PASS with the new pins; 68 vtop-node tests, clippy, fmt;
every chart refusal exercised locally.
@allamiro
allamiro force-pushed the feat/284-chart-candidates branch from cac2345 to c9a1bb3 Compare August 12, 2026 07:41
@allamiro

Copy link
Copy Markdown
Owner Author

Round one: five findings, all real, all fixed in c9a1bb3

Four came from review; the fifth came from CI, and it was the most valuable thing on this PR.

The kind deploy caught a real observability gap. The new failover phase failed with pod 0 has durably applied '0' of 60 records — quorum produce had just succeeded, so replication was fine. The truth: candidate mode registered no role collector at all. vtop_broker_local_committed_offset did not exist on a candidate pod. That is the metric this repo's own chaos harness reads to choose a promotion target, the one the smoke asserts, and the one every dashboard draws. The first slice knew — it left a comment saying so and named the follow-up — and ten rounds of review never asked, because nothing in the suite asked.

So the follow-up is now built: one collector for the life of the process, reading through the switching view. Per-transition role collectors could never have worked (same descriptors, registry refuses duplicates, no unregister), and reading through the view keeps the metric names stable across every transition — which is exactly what lets a dashboard built for a statically-rendered range keep working against a candidate deployment. It deliberately does not reuse the promotion probe's trait: that one blocks on purpose because a probe needs the true offset; a scrape must never block and can afford a stale gauge. New broker_candidate_leading answers the question candidate mode adds — who serves the range — from metrics alone.

Scenario 14 now pins all of it against both roles, since the suite is the composition proof and this walked straight through it.

The four review findings

Finding Verdict Fix
leaderOrdinal only refused under replicated real guard moved before the topology branch — retired is retired; both shapes tested by message
failover wait could accept the deleted holder's lease real wait for an epoch above the pre-delete one: a new grant mints a higher epoch, which covers survivor-takeover and same-pod recovery in one condition
render check proved addresses, never identities real peer and pod node_uuids asserted against the configured set — three addresses under two UUIDs would count a two-node quorum as three
values.yaml contradicted itself on failover real preamble corrected to say failover is backed and to name what still is not (#287); lease docs moved back onto their key; fencingEpoch: 0 requirement stated where it bites

Validation

Scenario 14 PASS with the new observability pins; 68 vtop-node tests; clippy and fmt clean; every chart refusal (five of them) exercised locally; the CI render assertions transliterated and checked against the real render before pushing.

@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: c9a1bb34f5

ℹ️ 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".

# that is following it. One collector answers for whichever role holds the
# range, and the names must not change when the role does — that is what
# lets a panel built for a statically-rendered range keep working here.
FOLLOWING=$(( NEW_LEADER == 2 ? 3 : 2 ))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Choose a surviving follower for the metrics assertion

When the original WINNER is candidate 2 and failover elects candidate 1 or 3, this expression selects candidate 2 even though it was just SIGKILLed; similarly, it selects the dead candidate 3 when candidate 3 was the winner and candidate 2 takes over. metric_value then curls a dead endpoint under set -euo pipefail, causing scenario 14 and the live-chaos CI job to fail despite a successful failover. Select the candidate distinct from both WINNER and NEW_LEADER.

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.

Verified real, fixed in 5c0e6c8 — and the joke writes itself: this landed in the suite whose entire purpose is refusing to assume which node won. A fixed ordinal selects the SIGKILLed candidate whenever the election comes out other than it did on my machine. The survivor is now derived: candidates are 1, 2 and 3, and await_lease_holder_changed guarantees NEW_LEADER != WINNER, so 6 - WINNER - NEW_LEADER is always the third, live one.

@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.

All reported issues were addressed across 7 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread scripts/live-chaos/scenarios/14-candidate-failover.sh Outdated
Comment thread scripts/live-chaos/scenarios/14-candidate-failover.sh Outdated
Comment thread scripts/live-chaos/scenarios/14-candidate-failover.sh Outdated
Comment thread crates/vtop-node/src/observe.rs Outdated
… a corpse, and a one-shot produce

Round two, and the fixes I shipped for round one brought their own
findings. Every one was real.

THE GAUGE THAT LIED. The candidate collector set broker_lease_active
from the metadata snapshot alone, so it answered "is there a live lease
for this range" when the name has always meant "is THIS replica the
authorized leaseholder" — the leader's collector says so in its own
comment, and computes `active && epoch == held` for exactly that
reason. Copying that formula would not have been enough here: a
FOLLOWING candidate adopts the holder's granted epoch and activates its
view at it, because that is how it accepts replication, so every term
is true on all three replicas at once. The gauge would have reported
three leaseholders for one range, telling an operator the opposite of
what two of them will do with a produce. The role is the missing term.
The unit test now pins the following case, and scenario 14 pins both
ends of it live.

THE PROBE THAT COULD PICK A CORPSE. The new observability assertions
chose the follower to scrape with a fixed ordinal, which selects the
SIGKILLed candidate whenever the election comes out other than it did
on my machine — curling a dead endpoint and failing the scenario for
the crime of electing a different pod. The candidates are 1, 2 and 3,
so the survivor is 6 minus the winner minus the new leader: derived,
never guessed. That this landed in THIS suite, whose whole purpose is
refusing to assume which node won, is the joke writing itself.

And those assertions were one-shot and unguarded. metric_value is a
pipeline, so under `set -o pipefail` a transient scrape failure aborted
the scenario before the diagnostic it was carrying could ever print;
and a follower that legitimately trailed the quorum by a record would
have been reported as broken replication. Both are now deadline-polled
through the shared helpers, which distinguish `absent` from "says
something else" — the first means candidate mode exports no role
collector at all, the second means replication is behind, and those
send an operator to opposite places. await_metric_equals is new
because some gauges are states rather than progress: "this replica
leads" is 1 or 0, and a floor of 0 would accept the 1 that means the
opposite.

CI ALSO CAUGHT TWO OF MINE. The ruby render check died on a bash syntax
error because a comment I added said "pod's" — inside a program that is
one single-quoted shell argument, so the apostrophe closed the string.
The block already warns that it cannot name a dollar-sigil for a
related reason; it now warns about apostrophes too, and the step is
extracted and parsed locally before pushing.

And the kind deploy got further than ever — every pod reported its
records, the holder was deleted, a survivor took the range at epoch 2
in place — then failed on a single-shot produce. A freshly granted
holder has to establish its replication streams before any quorum write
can land, which is why live-chaos retries every post-promotion produce
under a deadline and says so. The smoke does now too, and it produces
while the deleted pod is still returning: with three members a quorum
is the holder plus one, so proving the range serves THROUGH the outage
is the stronger claim and the one operators care about. The wait for
the returning pod is polled rather than `kubectl wait`, which matched
the pod still terminating under the same name and returned in four
seconds while the replacement had not started.

Scenario 14 PASS with the new gauge pins; 68 vtop-node tests, clippy,
fmt; the CI step extracted and syntax-checked.

@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: 5c0e6c888a

ℹ️ 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".

self.held_fencing_epoch
.with_label_values(&range)
.set(held as i64);
if let Some((epoch, active)) = snapshot {

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 Clear lease-active when no role is installed

When a scrape occurs after SwitchingLocalView::clear() during demotion, try_meta_fencing_epoch() returns None, so this branch leaves the previous broker_lease_active value unchanged. If the pod was just leading, it therefore continues exporting lease_active=1 while broker_candidate_leading is already reset to 0; because the subsequent quiesce/reopen can stall on disk, that false leaseholder signal can persist indefinitely. Set lease-active to 0 when the view has no role rather than retaining the prior leader's authorization state.

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.

Verified real, and it exposed a distinction the collector was missing: a contended read is IGNORANCE (keep the last value — the module doctrine, and right for progress gauges) while an empty switching view is KNOWLEDGE (this replica owns nothing right now). Fixed in 001bc32: the trait gains has_role(), defaulted true for concrete roles and false for a switching view between them, and broker_lease_active is written to 0 the moment no role owns the range — so it can no longer contradict broker_candidate_leading through a transition whose quiesce and reopen may stall on disk. Your finding also surfaced a neighbour: broker_held_fencing_epoch was written unconditionally and so REWOUND to 0 mid-transition, a monotonic gauge going backwards reading as a replica that lost its history; it now holds its last value too. New unit test pins both.

@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.

All reported issues were addressed across 5 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread scripts/k8s-smoke.sh
Comment thread scripts/k8s-smoke.sh Outdated
Comment thread scripts/k8s-smoke.sh Outdated
Comment thread scripts/k8s-smoke.sh
Round three. Five findings, all real, all in code from the two rounds
before it.

A REPLICA BETWEEN ROLES WAS STILL CLAIMING THE LEASE. The collector
leaves a gauge standing when a read comes back contended — the module's
own doctrine, and right for progress: a stalled disk should show an
offset that stops advancing, not one that rewinds. But mid-transition
the switching view holds no role at all, and that is a different thing
entirely. A contended lock is ignorance; an empty view is knowledge.
So broker_lease_active stayed 1 from the role that had just ended,
beside a broker_candidate_leading of 0 that already said the opposite,
and a transition that quiesces and reopens the range can stall on disk
long enough for that contradiction to be what an operator sees.
Authorization now fails closed the moment no role owns the range, while
the progress gauges keep their last reading — including the held epoch,
which used to be written unconditionally and so rewound to zero
mid-transition, a monotonic gauge going backwards reading as a replica
that lost its history.

The rest are in the smoke test, and three of them are the retry loop I
added yesterday to fix a one-shot produce.

Every failed attempt left its port-forward alive, so sixty retries
meant sixty kubectl processes and sixty API streams held open for the
length of the wait. Each attempt now tears its own forward down —
still a FRESH forward per attempt, because a pod that restarted takes
its forward with it and a reused one only repeats "connection
refused", which is the same reasoning the init retry loop above it
records.

The 180-second bound was also advisory: the deadline was checked
between attempts, and the client waits on a socket, so a forward that
accepted and then stopped answering would hang the job forever while
the loop never got another chance to look at the clock. Attempts are
bounded now.

The readiness wait converted an API failure into "not ready", so four
minutes of an unanswering API server would have been reported as a pod
that never came back — accusing a component nobody observed doing
anything. That rule is stated a hundred lines above in
await_pods_exist, and this loop broke it on its first draft. Query
failure and unreadiness now get the two different messages they
deserve.

And the phase claimed something it never checked. "Produce while the
deleted pod is still returning" is only true if the replacement was
actually still absent, which is a race this test does not control — a
fast StatefulSet restart is not a failure. The member count is now
captured and NARRATED: a run that says "2 of 3" proves the degraded
path, one that says "3 of 3" says plainly that it did not, and a
reading of zero is reported as the failed query it must be, since no
cluster accepts a quorum write with nothing Ready. A log line nobody
verified is worse than no log line.

Scenario 14 PASS; 68 vtop-node tests including the new between-roles
pin; clippy, fmt.

@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: 001bc3275a

ℹ️ 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 helm/vtop/values.yaml
Comment on lines +173 to +180
# replicated ONE range across the pods, every pod a CANDIDATE (#284): the
# role follows the metadata lease inside the binary. Whichever
# pod acquires the range leads and the rest follow it; when the
# holder dies, a surviving candidate takes the range in place —
# no re-render, no helm upgrade, no restart. Requires
# `lease.enabled` (candidates acquire the range through the
# lease) and `fencingEpoch: 0` (grants are minted from 1);
# both are enforced at render time.

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 Update the chart README for replicated topology

Update the chart's primary README alongside this new topology. helm/vtop/README.md:46-49 still explicitly says every pod is standalone and that the chart does not offer replication, its values table omits data.topology, and its verification section at lines 116-119 still claims every pod is an independent range. Users consulting the documented chart contract will therefore conclude this newly supported mode is unavailable and will not learn its required lease and fencing settings.

Useful? React with 👍 / 👎.

Comment thread scripts/k8s-smoke.sh
Comment on lines +867 to +869
members_at_write="$(kubectl -n "$REPLICATED_NS" get pods -l "app.kubernetes.io/instance=${REL}" \
-o jsonpath='{range .items[*]}{.metadata.deletionTimestamp}{"|"}{.status.conditions[?(@.type=="Ready")].status}{"\n"}{end}' \
2>/dev/null | grep -c '^|True$' || true)"

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 Sample readiness after the successful write

Move this measurement to the attempt that actually succeeds. It is currently captured before a retry loop that can run for 180 seconds, so if two pods are Ready here, early produces fail, and the replacement becomes Ready before a later produce succeeds, lines 880-883 still report that the range served on a bare quorum. That creates false evidence that the degraded path was exercised even though the write may have landed only after all three members recovered.

Useful? React with 👍 / 👎.

@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.

1 issue found across 3 files (changes from recent commits).

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/k8s-smoke.sh">

<violation number="1" location="scripts/k8s-smoke.sh:867">
P2: The failover log can report the wrong quorum state because `members_at_write` is captured before potentially many produce retries, not when the write succeeds. Capturing readiness for each attempt, immediately before its produce call, would keep the degraded-quorum narration tied to the successful attempt.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread scripts/k8s-smoke.sh
# count is captured and NARRATED: a run that says "2 of 3" proves the
# degraded path, and one that says "3 of 3" says plainly that it did not,
# instead of printing a claim nobody checked (review).
members_at_write="$(kubectl -n "$REPLICATED_NS" get pods -l "app.kubernetes.io/instance=${REL}" \

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 failover log can report the wrong quorum state because members_at_write is captured before potentially many produce retries, not when the write succeeds. Capturing readiness for each attempt, immediately before its produce call, would keep the degraded-quorum narration tied to the successful attempt.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/k8s-smoke.sh, line 867:

<comment>The failover log can report the wrong quorum state because `members_at_write` is captured before potentially many produce retries, not when the write succeeds. Capturing readiness for each attempt, immediately before its produce call, would keep the degraded-quorum narration tied to the successful attempt.</comment>

<file context>
@@ -843,6 +857,16 @@ R_TOTAL=$((R_EXPECTED + 30))
+# count is captured and NARRATED: a run that says "2 of 3" proves the
+# degraded path, and one that says "3 of 3" says plainly that it did not,
+# instead of printing a claim nobody checked (review).
+members_at_write="$(kubectl -n "$REPLICATED_NS" get pods -l "app.kubernetes.io/instance=${REL}" \
+  -o jsonpath='{range .items[*]}{.metadata.deletionTimestamp}{"|"}{.status.conditions[?(@.type=="Ready")].status}{"\n"}{end}' \
+  2>/dev/null | grep -c '^|True$' || true)"
</file context>

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.

helm: the chart cannot deploy a replicated range, so nothing in Kubernetes exercises replication

1 participant