Skip to content

meta: linearizable range-lease read on the admin transport (#223) - #233

Open
allamiro wants to merge 1 commit into
feat/223-lease-expiryfrom
feat/223-lease-read
Open

meta: linearizable range-lease read on the admin transport (#223)#233
allamiro wants to merge 1 commit into
feat/223-lease-expiryfrom
feat/223-lease-read

Conversation

@allamiro

@allamiro allamiro commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Second slice of #223, stacked on #232.

Why a candidate cannot decide without this

The admin transport offered propose, status, and membership — no read of applied state at all. So an election loop could only guess: propose an acquisition and learn from the rejection.

That conflates two very different situations. A GenerationMismatch tells a candidate its CAS token was stale; it does not tell it whether the incumbent is healthy and renewing, or dead and expired. Acting on that ambiguity is exactly how a candidate fences a leader that was doing nothing wrong.

AdminReadRangeLease returns the lease view together with the range_generation acquisition must CAS against — reading the lease without the token it has to be paired with would just move the guessing one step along.

The ordering is the point

ensure_linearizable() runs before the state is read, not after. A deposed node serving its own lagging copy could report an expired lease that the real leader has already renewed, and a candidate acting on that would fence a healthy leader. Fencing first makes that impossible: a node that has lost leadership fails the read instead of answering it.

Three distinctions the response keeps

  • a range that does not exist vs. one that exists with nobody leading it
  • an administrative lease with no deadline vs. an election lease with one — the codec mirrors the durable presence-byte encoding from meta: range-lease expiry and renewal for leader election (#223) #232 so the two representations cannot drift apart in meaning
  • the applied index the read was fenced at, so a caller can tell an answer from a newer state machine apart from a replayed older one

Notes

OpenraftConsensus gains an optional store handle — optional because the existing harnesses build the façade from a bare Raft handle and have no applied state to offer; a read against one of those reports the store as unavailable rather than inventing an answer. start_meta_node attaches it.

openraft containment holds: the new wire types and trait are consensus-library-free, and the only openraft call sits inside raft/. The policy test passes.

Refs #223.


Summary by cubic

Adds a linearizable range-lease read to the admin transport so candidates can see the current lease and CAS token before acquiring, preventing fences of a healthy leader. Supports #223.

  • New Features
    • Admin endpoint read_range_lease that fences with Raft ensure_linearizable() before reading applied state.
    • Response includes range_generation, fencing_epoch, optional lease view (admin vs election with expiry), and read_at_applied_index; distinguishes missing range from existing-without-lease.
    • OpenraftConsensus can be built with a store via .with_store(...); start_meta_node attaches it. Admin server and client gain new wire types and a client method to call the read; nodes without a store return an error instead of fabricating data.

Written for commit 8bdb93c. Summary will update on new commits.

Review in cubic

Second slice of #223. A candidate deciding whether to take a range has to be
able to see whether the current lease is still live, and until now nothing could
ask. The admin transport offered propose, status, and membership — no read of
applied state at all.

Without it the election loop could only guess: propose an acquisition, and learn
from the rejection. That conflates two very different situations. A
`GenerationMismatch` tells a candidate its CAS token was stale; it does not tell
it whether the incumbent is healthy and renewing or dead and expired. Acting on
that ambiguity is how a candidate fences a leader that was doing nothing wrong.

`AdminReadRangeLease` returns the range's lease view together with the
`range_generation` that acquisition must CAS against — reading the lease without
the token it has to be paired with would leave the caller guessing again, one
step further along.

The read is linearizable, and the ordering is the point: `ensure_linearizable()`
runs BEFORE the state is read. A deposed node serving its own lagging copy could
report an expired lease that the real leader has already renewed, and a
candidate acting on that answer would fence a perfectly healthy leader. Fencing
first makes that impossible; a node that has lost leadership fails the read
instead of answering it.

Three distinctions the response keeps that a looser type would collapse:

* a range that does not exist versus one that exists with nobody leading it;
* an administrative lease with no deadline versus an election lease with one —
  the codec mirrors the durable presence-byte encoding from the previous slice
  so the two representations cannot drift apart in meaning;
* the applied index the read was fenced at, so a caller can tell an answer from
  a newer state machine apart from a replayed older one.

`OpenraftConsensus` gains an optional store handle. Optional because the
existing harnesses build the façade from a bare Raft handle and have no applied
state to offer; a read against one of those reports the store as unavailable
rather than inventing an answer. `start_meta_node` attaches it, so every live
node can serve reads.

The openraft containment policy still holds: the new wire types and the trait
are consensus-library-free, and the only openraft call sits inside `raft/`.

Next slice: the node-side agent that uses this read to acquire and renew, and
publishes grants into `MetaFencingEpoch`.
@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.

@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 4 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-meta/src/transport/wire.rs">

<violation number="1" location="crates/vtop-meta/src/transport/wire.rs:687">
P2: The new lease RPC types and kind IDs are not available through the transport API's established flat exports, making downstream implementations of the public admin surface inconsistent with the existing admin payloads. Re-export the new types/constants from `transport` (and the response types from the crate root where appropriate).</violation>
</file>

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

Re-trigger cubic


/// Which range to read the lease for.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct AdminReadRangeLeaseRequest {

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 new lease RPC types and kind IDs are not available through the transport API's established flat exports, making downstream implementations of the public admin surface inconsistent with the existing admin payloads. Re-export the new types/constants from transport (and the response types from the crate root where appropriate).

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

<comment>The new lease RPC types and kind IDs are not available through the transport API's established flat exports, making downstream implementations of the public admin surface inconsistent with the existing admin payloads. Re-export the new types/constants from `transport` (and the response types from the crate root where appropriate).</comment>

<file context>
@@ -673,6 +682,123 @@ impl AdminStatusResponse {
 
+/// Which range to read the lease for.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub struct AdminReadRangeLeaseRequest {
+    pub topic_uuid: Uuid,
+    pub range_uuid: Uuid,
</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.

1 participant