8000 docs(inkless:switch): describe the last-known-leader election in phase 1 [KC-759] by giuseppelillo · Pull Request #871 · aiven/inkless · GitHub
[go: up one dir, main page]

Skip to content

docs(inkless:switch): describe the last-known-leader election in phase 1 [KC-759] - #871

Merged
jeqo merged 4 commits into
mainfrom
claude/interactive-problem-solving-30a25f
Nov 13, 2026
Merged

jeqo merged 4 commits into
mainfrom
claude/interactive-problem-solving-30a25f

Conversation

@giuseppelillo
@giuseppelillo giuseppelillo commented Nov 12, 2026 •
Copy link
Copy Markdown
Contributor

With eligible leader replicas (ELR) enabled, a partition in phase 1 of the classic-to-diskless switch can still elect a leader from outside its ISR. CLASSIC_TO_DISKLESS_SWITCH.md says that a partition that loses every ISR member has no leader until one of them returns. The last-known-leader election of KIP-966 contradicts that, and the docs don't describe it.

Problem

The phase 1 guards in ReplicationControlManager block unclean leader election by not calling setElection(Election.UNCLEAN). PartitionChangeBuilder.electAnyLeader and electPreferredLeader call canElectLastKnownLeader() before they check the election type. So the last-known-leader election runs for every election type, and unclean.leader.election.enable doesn't control it.

The following example uses replicas [2, 0, 1], min.insync.replicas=2, and a seal at 20:

Step Event ISR ELR lastKnownElr Leader
1 Phase 1 starts [2, 0, 1] [] [] 2
2 Brokers 0 and 1 stop [2] [0] [] 2
3 Broker 2 stops after its log loses offsets 15 to 19 [] [0, 2] [2] none
4 Brokers 0 and 1 return after an unclean shutdown [] [2] [2] none
5 Broker 2 returns after an unclean shutdown [2] [] [] 2, RECOVERING

The partition loses offsets 15 to 19, even though the high watermark covered them. KIP-966 accepts the same loss for a classic topic. In phase 1, the loss shows up differently:

  • The leader's log ends below the seal, so the broker marks the partition offline. The controller isn't told, and keeps broker 2 as the leader with an ISR of one.
  • No other replica can lead. None is in the ISR or the ELR, and phase 1 blocks unclean leader election.
  • The remote log manager can't upload the prefix, so phase 2 doesn't start.

Change

The switch doc gains a "Last-known-leader election" subsection. It describes when the controller runs the election, what happens to a partition in phase 1, and how to recover one that's offline:

  1. Lower the seal to the leader's log end offset with kafka-topic-switch.sh seal.
  2. Restart the leader. The broker doesn't apply metadata changes to a partition that it marked offline.
  3. Run kafka-topic-switch.sh repair.

The recovery loses the records between the leader's log end offset and the old seal. The "Unclean leader election" section and the phase table now point to the new subsection.

This change doesn't modify controller behavior. Blocking the election in phase 1 would leave the partition without a leader in both outcomes: the one where the last known leader holds the whole prefix, and the one where it doesn't.

Testing

ClassicToDisklessSwitchLastKnownLeaderClusterTest runs the sequence in the example on a 3-broker cluster with ELR enabled. A gated remote storage manager fails segment copies until the test releases it. Without the gate, the leader rolls its active segment at the seal and the partition leaves phase 1 within one task interval. The test deletes the clean-shutdown markers, so the controller sees unclean shutdowns.

The test is parametrized on loseTail, so it covers both outcomes in the doc.

With loseTail=true, the test truncates the leader's log to 15 before the leader stops. It then checks the following:

  • The controller elects the last known leader with an ISR of one and the RECOVERING state, and the leader marks the partition offline.
  • 15 falls in the offset range that kafka-topic-switch.sh seal validates against. After the seal is lowered, the partition stays offline until the leader restarts.
  • After the restart and the repair, the ISR recovers and every replica's log ends at 15 while the partition is still in phase 1. After the gate opens, phase 2 starts.
  • A consumer with auto.offset.reset=none reads offsets 0 to 14 and the diskless records at 20 to 24.

With loseTail=false, the test checks the following:

  • The leader keeps the partition online and finishes recovery.
  • The ISR recovers and every replica's log ends at 20 in phase 1. After the gate opens, phase 2 starts.
  • A consumer reads offsets 0 to 24.

The test doesn't run the recovery without the repair step, or the election while the switch is pending. The doc describes the pending case from the code.

KC-759

🤖 Generated with Claude Code

Assisted-by: Claude Opus 5.5

…e 1 [KC-759]

The phase 1 guards block unclean leader election, but they don't block
the last-known-leader election that KIP-966 adds. With ELR enabled, a
partition whose ISR and ELR members all shut down uncleanly elects its
last known leader when that broker returns, resets the ISR to it, and
loses any records that broker lost. The docs said such a partition has
no leader until an ISR member returns.

The switch doc now describes when the controller runs this election and
what it does to a partition in phase 1. If the leader lost records below
the seal, the leader marks the partition offline, the controller keeps
it as leader, and phase 2 doesn't start. The doc gives the recovery: a
downward re-seal to the leader's log end offset, a leader restart, and a
control-plane repair. That recovery loses the records between the
leader's log end offset and the old seal.

Testing

`ClassicToDisklessSwitchLastKnownLeaderClusterTest` holds phase 1 open
with a gated remote storage manager, drives the partition into the
last-known-leader election through unclean shutdowns, and checks that
the leader marks the partition offline. It then runs the re-seal
recovery and checks that phase 2 starts and that a consumer reads the
retained classic records followed by the diskless records.

Assisted-by: Claude Opus 5.5
…ng them

The last-known-leader test logged each stage while it was being written.
It now asserts those states: the leader recovery state after the
election, the seal command's offset range, the partition staying offline
after the re-seal until the leader restarts, and the log end offset on
every replica in phase 2. The offline check waits for the leader to
apply the lower seal instead of sleeping for a fixed time.

Assisted-by: Claude Opus 5.5
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The recovery test can fail nondeterministically because it checks replica log end offsets after opening the upload gate, allowing phase-2 fetchers to advance them before the assertion runs.

1 open issue
What changed in this PR

This PR documents how KIP-966 last-known-leader elections affect phase 1 of Inkless’s classic-to-diskless switch, without changing controller behavior.

Changes:

  • Clarifies that phase 1 guards don't block last-known-leader elections.
  • Documents data-loss consequences and recovery through lowering the seal, restarting the leader, and repairing the control plane.
  • Adds a cluster test covering the election, offline partition, recovery, and consumption across missing offsets.
File Description
docs/​inkless/​CLASSIC_TO_DISKLESS_SWITCH.md Explains election behavior, failure outcomes, and recovery steps.
core/​src/​test/​java/​kafka/​server/​ClassicToDisklessSwitchLastKnownLeaderClusterTest.java Tests last-known-leader data loss and recovery with gated remote uploads.

Review effort: Balanced

Once the upload gate opens, phase 2 consolidates from the seal, and a
replica can append the diskless records and move its log end offset
past the seal before the test checks it. The test now checks the log
end offsets after the ISR recovers and before it opens the gate, and
asserts that phase 2 hasn't started at that point.

Assisted-by: Claude Opus 5.5
Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

No active review issues were identified in the documentation and cluster-test changes covering last-known-leader recovery during classic-to-diskless switching.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

@giuseppelillo
giuseppelillo requested a review from jeqo November 12, 2026 15:02

### Last-known-leader election

If you enable the `eligible.leader.replicas.version` feature, the controller tracks eligible leader replicas (ELR) as described in KIP-966. KIP-966 adds a last-known-leader election, and the phase 1 guards don't block it. The controller runs this election for every election type, not only `UNCLEAN`, and `unclean.leader.election.enable` doesn't control it.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Seems that ELRV_1 is LATEST_PRODUCTION (EligibleLeaderReplicasVersion.java:27,31), so a 4.1 or later cluster has ELR on without anyone enabling it. "If you enable" reads as an opt-in. Can you reword it to say it's the default?

Suggested change
If you enable the `eligible.leader.replicas.version` feature, the controller tracks eligible leader replicas (ELR) as described in KIP-966. KIP-966 adds a last-known-leader election, and the phase 1 guards don't block it. The controller runs this election for every election type, not only `UNCLEAN`, and `unclean.leader.election.enable` doesn't control it.
The `eligible.leader.replicas.version` feature is on by default at 4.1 and later. With it on, the controller tracks eligible leader replicas (ELR) as described in KIP-966. KIP-966 adds a last-known-leader election, and the phase 1 guards don't block it. The controller runs this election for every election type, not only `UNCLEAN`, and `unclean.leader.election.enable` doesn't control it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Applied in bf9a65e. The default comes from Feature.defaultLevel, so a cluster formatted at 4.1 or later has ELR on, and so does one upgraded with kafka-features.sh upgrade --release-version to 4.1 or later. An upgrade that sets only --metadata leaves the feature at its old level.

return registration(0, topic).leader;
}

private void driveIntoLastKnownLeaderBelowSeal(final Admin admin, final String topic, final int leaderHint) throws Exception {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The doc describes two outcomes, and the test runs the one where the leader lost records. I added a second case that skips the truncation. It passes: the leader stays online, the high watermark is 20, the ISR returns to 3, the controller records RECOVERED, phase 2 starts after the gate, and the consumer reads 0 to 24. The high watermark check doesn't prove the reconcile code, because makeLeader advances it with an ISR of one.

This can be parametrized:

private void driveIntoLastKnownLeader(final Admin admin, final String topic, final int leaderHint,
                                      final boolean loseTail) throws Exception {
    ...
    if (loseTail) {
        final UnifiedLog leaderLog = cluster.brokers().get(leader).replicaManager().getLog(tp).get();
        leaderLog.truncateTo(TRUNCATED_LEO);
        assertEquals(TRUNCATED_LEO, leaderLog.logEndOffset());
    }
    ...
    if (loseTail) {
        waitForCondition(() -> isOffline(leader, tp), "the leader to mark the partition offline");
    }
}

and a new test pass the flags to pin the behavior to both outcomes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in bf9a65e: the test is parametrized on loseTail. One difference from the snippet: the ISR-of-one and RECOVERING assertions also sit under loseTail. Without the loss, the leader can clear both through AlterPartition before the poll reads the registration. The loseTail=false case waits for RECOVERED instead, then checks that the ISR returns to 3, every log ends at 20 in phase 1, phase 2 starts, and the consumer reads 0 to 24. Both cases passed in two runs.

…ection

The switch doc describes two outcomes of the last-known-leader election
in phase 1, and the test ran only the one where the leader lost records.
The test is now parametrized on that loss. Without it, the leader keeps
the partition online and finishes recovery, the ISR returns to 3, phase
2 starts at the original seal, and a consumer reads every record.

The doc also says that the `eligible.leader.replicas.version` feature is
on by default at 4.1 and later, instead of reading as an opt-in.

Assisted-by: Claude Opus 5.5
@jeqo
jeqo merged commit 10a852b into main Nov 13, 2026
8 checks passed
@jeqo
jeqo deleted the claude/interactive-problem-solving-30a25f branch November 13, 2026 16:01
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.

3 participants

0