Repository navigation
fix(inkless:switch): recreate phase 2 replicas that don't hold the committed classic prefix [KC-702] - #857
fix(inkless:switch): recreate phase 2 replicas that don't hold the committed classic prefix [KC-702]#857giuseppelillo wants to merge 3 commits into
Conversation
a0816d2 to
dc5f809
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Recovery can restore discarded state beyond the seal and skip diskless records, while deleted-prefix handling can retry indefinitely after local-log loss.
2 open issues
What changed in this PR
This PR strengthens phase 2 of Inkless’s classic-to-diskless switch by recording the last classic record’s leader epoch and using it to detect divergent replicas and guide recovery.
Changes:
- Carry the recorded epoch through the broker request, controller metadata, and broker metadata view.
- Check replica prefixes, adjust consolidation recovery before the first prune, and protect remote segments during rebuilds.
- Add regression tests and document the recovery behavior.
| File | Description |
|---|---|
storage/src/test/java/org/apache/kafka/server/log/remote/storage/RemoteLogManagerTest.java |
Tests remote-prefix retention during rebuilds. |
storage/src/main/java/org/apache/kafka/server/log/remote/storage/RemoteLogManager.java |
Protects retained segments from epoch-cache cleanup. |
metadata/src/test/java/org/apache/kafka/metadata/PartitionRegistrationTest.java |
Tests epoch persistence, merging, and switch undo. |
metadata/src/test/java/org/apache/kafka/controller/ReplicationControlManagerInklessTest.java |
Checks controller persistence of the recorded epoch. |
metadata/src/test/java/org/apache/kafka/controller/PartitionChangeBuilderTest.java |
Tests epoch tags in phase-2 records. |
metadata/src/main/java/org/apache/kafka/metadata/PartitionRegistration.java |
Stores and propagates the recorded epoch. |
metadata/src/main/java/org/apache/kafka/metadata/InitDisklessLogFields.java |
Adds encoding and decoding for tag 104. |
metadata/src/main/java/org/apache/kafka/controller/ReplicationControlManager.java |
Passes the requested epoch to the change builder. |
metadata/src/main/java/org/apache/kafka/controller/PartitionChangeBuilder.java |
Includes the recorded epoch in phase-2 metadata. |
docs/inkless/DISKLESS_CONSOLIDATION.md |
Documents recovery bounds and retention protection. |
docs/inkless/CLASSIC_TO_DISKLESS_SWITCH.md |
Explains epoch recording and divergent-prefix recovery. |
core/src/test/scala/unit/kafka/server/ReplicaManagerInklessTest.scala |
Tests replica checks and remote-segment selection. |
core/src/test/scala/unit/kafka/server/InitDisklessLogManagerTest.scala |
Checks epoch values in flag requests. |
core/src/test/scala/io/aiven/inkless/consolidation/DisklessLeaderEndPointTest.scala |
Tests epoch responses and first-prune behavior. |
core/src/test/scala/io/aiven/inkless/consolidation/ConsolidationFetcherThreadTest.scala |
Supplies recorded-epoch metadata in the fetcher test. |
core/src/main/scala/kafka/server/ReplicaManager.scala |
Checks and truncates divergent classic prefixes. |
core/src/main/scala/kafka/server/metadata/InklessMetadataView.scala |
Exposes the recorded epoch to brokers. |
core/src/main/scala/kafka/server/InitDisklessLogState.scala |
Captures the epoch for the phase-2 request. |
core/src/main/scala/io/aiven/inkless/consolidation/DisklessLeaderEndPoint.scala |
Applies seal bounds and returns the recorded epoch. |
clients/src/main/resources/common/message/SetClassicPrefixInRemoteStorageRequest.json |
Adds the optional tagged epoch field. |
Review effort: Balanced
…re the first prune [KC-702] The control plane keeps a switched partition's WAL start at the classic log start until the leader's first prune moves it to the seal. That prune runs only once the leader's remote copy reaches `seal - 1`, which a leader with an empty local log never reaches. Its consolidation fetch at offset 0 then reads the first diskless batch from the WAL, and the local log hides the classic prefix that remote storage holds. `DisklessLeaderEndPoint` now uses the seal as the lowest diskless start for the tiered-storage redirect and for `fetchEarliestLocalOffset`. The tier-state machine then rebuilds the prefix and starts the local log at the seal, as it does after the first prune. If remote storage doesn't cover `seal - 1`, the endpoint returns `NOT_LEADER_OR_FOLLOWER` and sets `ConsolidationRemotePrefixUnknown` instead of consolidating below the seal. Retention or `DeleteRecords` can move the cross-tier earliest offset to the seal while the WAL start stays below it. `DeleteRecords` at the seal runs only the local leg. Without another check, the fetcher waits for coverage of `seal - 1` that never returns, or rebuilds a deleted prefix from a segment that remote storage still lists. If the cross-tier earliest offset is at the diskless start or past it, the endpoint now returns `OFFSET_OUT_OF_RANGE` before it asks remote storage. The out-of-range reset starts the local log at that offset. ## Testing `DisklessLeaderEndPointTest` covers the redirect, the retry without remote coverage, the reset when the cross-tier earliest offset is at the seal, a fetch at the seal, and the earliest local offset before the first prune. Assisted-by: Claude Opus 5.5
dc5f809 to
c6a11b5
Compare
c6a11b5 to
9f8deec
Compare
There was a problem hiding this comment.
🟡 Changes recommended
A critical truncation bug retains stale remote-storage progress, allowing the WAL pruner to delete diskless records before they are uploaded and making them unrecoverable past the seal.
1 open issue
1 resolved since last review
Review effort: Balanced
9f8deec to
4094c80
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Stale remote-progress updates after truncation and insufficient epoch validation during seal recovery can cause unuploaded records to be pruned and correct classic records to be skipped.
2 open issues
1 resolved since last review
Review effort: Balanced
4094c80 to
10bc94c
Compare
10bc94c to
b71e175
Compare
There was a problem hiding this comment.
🟡 Changes recommended
A partial log-deletion failure can allow replica recreation while stale remote-copy tasks remain active, potentially skipping uploads and allowing WAL pruning over missing data.
1 open issue
3 resolved since last review
Review effort: Balanced
b71e175 to
6509237
Compare
There was a problem hiding this comment.
🟡 Changes recommended
A critical race allows a surviving remote task to install stale progress after log truncation, letting the WAL pruner delete diskless records before consolidation uploads them.
1 open issue
1 resolved since last review
Review effort: Balanced
6509237 to
fdf1556
Compare
There was a problem hiding this comment.
🟡 Changes recommended
The unlocked truncation-count getter can expose a new count with stale log state, allowing discarded consolidation progress to pass validation and trigger WAL pruning of records that have not been uploaded.
1 open issue
1 resolved since last review
Review effort: Balanced
…mmitted classic prefix [KC-702] In phase 2 of the classic-to-diskless switch, the controller adds every live replica to the ISR without checking its local copy of the classic prefix, and any replica can lead. A replica can hold records below the seal that the partition doesn't hold. It led an older leader epoch and wrote records that the next leader didn't have, or an unclean leader election before the switch discarded records below its high watermark. Forced deletion keeps the leader epoch cache of that replica, and the remote log manager uses that cache to pick remote segments. On that replica, a tiered read below the seal fails with `OFFSET_OUT_OF_RANGE`. The leader that requests the phase 2 flag now sends the leader epoch of its record at `seal - 1`. The controller stores it with the flag as tag `104` (`lastClassicLeaderEpoch`). Each epoch has one leader, so a replica whose leader epoch cache has that epoch at `seal - 1` holds the committed prefix, at any high watermark. The controller rejects an epoch below `-1` or higher than the current leader epoch, which no replica can match. `DescribeTopicPartitions` returns the epoch with the other switch fields, and `kafka-topic-switch.sh state` prints it. A consolidating replica checks its local log when it first applies the partition and on every new leader epoch, after the broker stops the replica, consolidation, and log directory move fetchers of the partition. Stopping the move fetcher waits for a running promotion of the future log. The replica keeps its log only if its last record below the seal is in the recorded epoch, its records past the seal are consolidated diskless records, its highest offset in remote storage is below its log end offset, and its future log ends in the same epoch as the current log at that offset. Otherwise, `applyDelta` stops the replica as it stops a deleted partition. It deletes the current and future logs, and the remote log manager cancels the partition tasks but keeps the remote log metadata subscription. The leadership change in the same delta creates an empty log, and the existing recovery rebuilds it from remote storage. A storage error can stop the deletion before it reaches the future log, which would then restart the move on the new log. If the broker can't delete both logs or cancel the remote log tasks, it marks the replica offline with its topic ID, as after a storage error in a transition, so later deltas skip it. `stopPartitions` cancels the remote log tasks even if a deletion fails, because a copy task that keeps running also keeps the copy progress of the deleted log. The broker never truncates a log that it keeps in place. Truncating in place left state behind that later steps trusted: the future log of a log directory move, a highest offset in remote storage that only grows, and remote log tasks that compute offsets without the log lock. Each of them needed its own guard. A recreated log starts without that state, and the remote log tasks that still run update only the deleted log. A kept log can still have truncated before the check, to the seal in phase 1 or to the new leader after an unclean leader election. The remote log tasks compute the highest offset in remote storage without the log lock. A follower task that read the discarded tail, or a copy task that uploaded it, could install its offset after the check accepted the log. `UnifiedLog` now counts truncations. A truncation counts itself and changes the log under the log lock. The follower and copy tasks read the count under that lock before they read the leader epoch cache or the segments. A task never pairs the new count with the state from before the truncation. `updateHighestOffsetInRemoteStorage(offset, truncationCount)` installs the offset under the log lock only if the count didn't change. After a truncation, the copy task also finds its copied offset again instead of resuming after the discarded tail. Upstream `TierStateMachine` rebuilds from the remote segment that holds the record before the local log start of the source. In phase 2, a classic leader or the current log of a log directory move can still hold local classic segments below the seal. A segment from a discarded epoch can hold that record in the same epoch as the committed prefix and diverge after it, and it can also hold records past the seal. Restoring such a segment restores the wrong epochs and starts the local log at its end, past diskless records that the WAL prune then deletes. A phase 2 rebuild now looks up the segment that holds `seal - 1` in the recorded epoch, whatever the local log start of the source is, and starts the local log at the seal. Its records below the seal are the committed prefix, even if it also holds discarded records past the seal. The rebuild keeps the epochs that start below the seal and skips the epoch lookup on the source. It restores the producer snapshot only if the segment ends at `seal - 1`. Sealing aborts undecided transactions, so the empty producer state is correct at the seal. Partitions without a recorded epoch keep the upstream rebuild. ## Testing - `testSwitchedLeaderInPhaseTwoRecreatesReplicaWithoutCommittedHistory` covers a leader with a divergent epoch at `seal - 1`, and a leader whose future log ends past the current log or in another epoch. The broker deletes both logs, stops the remote log tasks before the leadership change, and starts consolidation at 0 on an empty log. A matching future log keeps the move running. - `testSwitchedLeaderInPhaseTwoDropsRemoteProgressPastTheSeal` runs the WAL pruner on leaders with a divergent epoch, a classic tail past the seal, or remote progress at their log end offset. The broker recreates them, and the pruner doesn't prune. A leader that holds the committed prefix keeps its log, and the pruner prunes at `seal - 1`. - `testSwitchedLeaderInPhaseTwoKeepsConsolidatedReplica` keeps a leader whose log holds the committed prefix and consolidated records past the seal, and resumes consolidation at its log end offset. - `testSwitchedConsolidatingFollowerInPhaseTwoRecreatesReplicaWithoutCommittedHistory` covers a follower at the seal with a matching and a divergent epoch. - `testOnlineSwitchedReplicaWithoutCommittedHistoryRecreatedOnNewLeaderEpoch` checks that an ISR change doesn't recreate an online replica, and that the next leader epoch does. - `testSwitchedReplicaStaysOfflineWhenItsDeletionFails` removes the directory of the current or the future log before the check. The broker cancels the remote log tasks and marks the replica offline. The next leader epoch doesn't recreate it, and no fetcher starts. Without the change, the broker recreates the replica. - `testUpdateHighestOffsetInRemoteStorageIgnoresOffsetComputedBeforeTruncation` checks that `truncateTo` and `truncateFullyAndStartAt` make the log drop an offset from before them, and that a `truncateTo` without effect doesn't. - `testTruncationCountWaitsForRunningTruncation` pauses `truncateTo` after it counts itself and before it truncates the leader epoch cache. A read of the count waits for the truncation to finish. If the read doesn't take the log lock, it returns the new count while the cache still holds the removed epoch, and the test fails. - `testFollowerTaskInstallsHighestRemoteOffsetWithTruncationCountFromBeforeTheLookup` and `testCopyTaskFindsCopiedOffsetAgainAfterTruncationDuringUpload` truncate the log during the lookup and during the upload. Both tasks install the offset with the count from before, and the copy task finds its copied offset again on the next run. Without the change, both tests fail. - `testTierStateMachineLooksUpLastClassicSegmentByRecordedEpoch` checks that the rebuild ignores a segment from a discarded epoch that covers `seal - 1`. - `testTierStateMachineStopsAtSealForSegmentCrossingIt` rebuilds from a segment that crosses the seal. The segment contains one epoch, or a discarded epoch past the seal. The rebuild starts the log at the seal, keeps only the epochs below it, and skips the producer snapshot. Without the change, the fetch starts at the segment end. - `testTierStateMachineLooksUpRecordedEpochForSourceWithLocalClassicPrefix` rebuilds from a source whose local log starts at 50, below seal 100. The segment that holds 49 in epoch 0 comes from discarded epoch 1, which epoch 2 replaced at 80. The rebuild uses the segment that holds 99 in epoch 2 instead, which ends at the seal or crosses it. Without the change, the rebuild restores epoch 1. - `PartitionRegistrationTest`, `PartitionChangeBuilderTest`, `ReplicationControlManagerInklessTest`, `InitDisklessLogManagerTest`, `InklessMetadataViewTest`, `DescribeTopicPartitionsRequestHandlerTest`, and `TopicSwitchCommandTest` cover the new field from the request to the broker metadata view, including the range check in the controller and the `kafka-topic-switch.sh state` output. Assisted-by: Claude Opus 5.5
… epoch cache rebuilds [KC-702] A phase 2 leader that the broker recreates caches only its own leader epoch until `TierStateMachine` restores the older epochs from remote storage. A leader that lost its local log is in the same state. The remote log manager runs its expiration task as soon as the broker becomes the leader. That task deletes every remote segment whose epochs are all older than the earliest cached epoch, and it doesn't check offsets, because upstream assumes that those segments end below the log start. For such a leader, the assumption doesn't hold, and the task deletes the classic prefix that the rebuild reads and consumers fetch. For a consolidating partition, this cleanup now deletes only the segments that end below the cross-tier log start, which is the authoritative log start of the partition. The other segments wait until retention moves that start past them, like a divergent segment inside the cached epochs does upstream. Classic topics keep the upstream behavior. ## Testing - `testEpochCacheCleanupKeepsConsolidatingSegmentsFromCrossTierStart` runs the expiration task for a leader that caches only epoch 5, with remote segments from epochs 0 and 1 on both sides of the cross-tier log start. For a consolidating partition, the task deletes only the segment below that start. Without the change, it deletes both. For a classic partition, it deletes both, as upstream does. - `testRecreatedLeaderKeepsRemotePrefixUntilRebuild` starts a real empty log in epoch 7, as a recreated leader does. Remote storage holds segments from classic epochs 0, 2, and 3 below seal 100 and from the diskless epoch 5, and the cross-tier log start is 40. Both before and after the rebuild, expiration deletes only the segment below 40. Without the guard, the first expiration deletes every remote segment. Assisted-by: Claude Opus 5.5
fdf1556 to
037afb6
Compare
There was a problem hiding this comment.
🔵 Needs a closer look
The coupled replica-deletion, remote-retention, and recovery changes need storage-domain review and integration validation of concurrent remote tasks, failover, and log-directory moves to assess data-safety invariants.
No open issues 🎉
1 resolved since last review
Review effort: Balanced


In phase 2 of the classic-to-diskless switch, the controller adds every live replica to the ISR without checking its local copy of the classic prefix, and any replica can lead. A replica can hold records below the seal that the partition doesn't hold. It led an older leader epoch and wrote records that the next leader didn't have, or an unclean leader election before the switch discarded records below its high watermark. Forced deletion keeps the leader epoch cache of that replica, and the remote log manager uses it to compute the highest offset in remote storage and to pick remote segments. On that replica, a tiered read below the seal fails with
OFFSET_OUT_OF_RANGE, and the consumer resets its offset. On an idle topic, the highest offset in remote storage also stays below the seal, so forced deletion never completes.Example
Broker B leads epoch 1 and appends
[80, 100), but no follower fetches those records before B goes offline. The controller elects A in epoch 2, and A appends its own[80, 100). A seals the partition at 100 and uploads the classic prefix while B stays offline.A requests the phase 2 flag with
lastClassicLeaderEpoch = 2, the epoch of its record at 99. In phase 2, B returns, joins the ISR, and can lead. Its log ends at the seal, so it looks complete. Without this change, B serves its epoch 1 records at[80, 100), which no other replica holds. The cache of B also maps that range to epoch 1, and no remote segment has that epoch.With this change, B checks its log in
applyDeltabefore it leads. Its epoch at 99 is 1, so the broker deletes the log and lets the leadership change create an empty one, which the existing recovery rebuilds from remote storage:sequenceDiagram participant K as Controller participant B as ReplicaManager of B participant RLM as Remote log manager of B participant D as DisklessLeaderEndPoint participant R as Remote storage K-->>B: Delta: B leads epoch 5, lastClassicLeaderEpoch = 2 B->>B: Stop the replica, move, and consolidation fetchers B->>B: Epoch at 99 is 1, so the log fails the check B->>RLM: Cancel the partition tasks, keep the metadata subscription B->>B: Delete the current and future logs B->>B: makeLeader creates an empty log in epoch 5 B->>RLM: onLeadershipChange starts new tasks on the new log B->>D: Consolidation fetch at 0 D-->>B: OFFSET_MOVED_TO_TIERED_STORAGE, earliest local offset 100 B->>R: Segment at 99 in epoch 2, the recorded epoch R-->>B: Segment with epochs 0 and 2 B->>B: Restore the epochs and producer state, start the log at 100 B->>D: Consolidation fetch at 100Recreate instead of truncate
The broker never truncates a log that it keeps in place. A truncation leaves state that later steps trust, and each piece needs its own guard. A recreated log starts without any of it. In this example, broker B led epoch 3, uploaded
[50, 119], and still holds[100, 120)when an unclean leader election makes A the leader in epoch 4. A seals at 100 and recordslastClassicLeaderEpoch = 2.A truncation of B to the seal leaves this state behind:
[100, 120)before consolidation uploads them.-1until the remote log manager computes it from the rebuilt log.[100, 120)in epoch 3 and replaces the current log once both end at 120.[100, 120).The epoch of B at 99 is 2, but the check still rejects B because it holds classic records past the seal. If B applied the seal while it was online, it already truncated its log to 100. The check then rejects B because its remote progress 119 isn't below its log end offset 100. A remote log task that computed 119 before that truncation can't install it after the check, because the log drops an offset that a task computed before a truncation.
Remote segment that crosses the seal
Leader X appends
[100, 120)in epoch 3 and uploads a segment[50, 119]with epochs 2 and 3. An unclean leader election then makes Y the leader in epoch 4. Y's log ends at 100. The partition loses[100, 120). Y seals at 100 before any new produce and recordslastClassicLeaderEpoch = 2.The segment at 99 in epoch 2 is the right one to rebuild from, but it ends at 119. Without this change,
TierStateMachinestarts the local log at 120. The consolidating leader then never materializes the diskless records[100, 120), and the WAL prune deletes them. With this change, a phase 2 rebuild starts the local log at the seal with only the epoch 2 entry, and the leader consolidates the diskless records from 100 on.Source with local classic records below the seal
In phase 2, a classic leader can still hold local classic segments below the seal, and so can the current log of a log directory move. Upstream
TierStateMachinerebuilds from the segment that contains the record before the local log start of the source. Broker X leads epoch 1 and uploads a segment[0, 119]with its records[80, 120). An unclean leader election then makes A the leader in epoch 2. A appends its own records from 80 and seals at 100. It recordslastClassicLeaderEpoch = 2. The local log of A starts at 50.A follower that rebuilds from A looks up the record at 49 in epoch 0. The segment of X holds that record in the same epoch, so remote storage can return that segment:
[80, 100)and starts its local log at 120, past the diskless records[100, 120).Changes
seal - 1inSetClassicPrefixInRemoteStorage. The controller stores it with the flag as tag104(lastClassicLeaderEpoch) and drops it when the switch is undone. The value is-1if the leader's log doesn't contain a record below the seal. The controller rejects a value below-1or higher than the current leader epoch, which no replica can match.DescribeTopicPartitionsreturns the epoch with the other switch fields, andkafka-topic-switch.sh stateprints it.applyDeltachecks each consolidating phase 2 replica that is new to the broker or gets a higher leader epoch, before the leader and follower transitions. The flag record bumps the leader epoch, which makes live replicas run the check when phase 2 starts. The broker first stops the replica, move, and consolidation fetchers of the partition. It keeps the log only if all of these are true:min(seal, LEO) - 1is the recorded epoch, or the log has no record below that offset. Each epoch has one leader, so a matching epoch proves that the records below the seal are the committed ones, at any high watermark.hasConsolidatedDisklessSuffixFromSeal).highestOffsetInRemoteStorageis below the log end offset.stopPartitionsanddeleteLocalLog = true, as for a deleted partition. That removes the partition, deletes the current and future logs, and cancels the remote log tasks. A newkeepRemoteLogMetadataflag keeps the remote log metadata subscription and the topic ID of the partition, because the partition stays on the broker. The leader or follower transition in the same delta then creates an empty log, and the existing recovery rebuilds the log, the leader epoch cache, and the producer state from remote storage.UnifiedLogand the remote log manager need no change for this path.stopPartitionscancels the remote log tasks even if a deletion fails, because a copy task that keeps running also keeps the copy progress of the deleted log.UnifiedLognow counts truncations. A truncation counts itself and changes the log under the log lock. The follower and copy tasks read the count under that lock before they read the leader epoch cache or the segments. A task never pairs the new count with the state from before the truncation.updateHighestOffsetInRemoteStorage(offset, truncationCount)installs the offset under the log lock only if the count didn't change. After a truncation, the copy task also finds its copied offset again instead of resuming after the discarded tail.TierStateMachinelooks up the remote segment that holdsseal - 1in the recorded epoch, whatever the local log start of the source is, and starts the local log at the seal. Each epoch has one leader, so the records of that segment below the seal are the committed prefix, even if the segment also covers discarded records past the seal. The rebuild keeps the epochs that start below the seal and skips the epoch lookup on the source. It restores the producer snapshot only if the segment ends atseal - 1. Sealing aborts undecided transactions, so the empty producer state is correct at the seal. Partitions without a recorded epoch, including partitions in phase 1, keep the upstream rebuild.TierStateMachine.javais a newINTERLEAVEDentry inINKLESS_OWNERSHIP.fetchEarliestLocalOffsetapplies the same bound. The control plane keeps the WAL start at the classic log start until the first prune moves it to the seal. Without the bound, a leader that lost its local log before that prune reads the first diskless batch at offset 0, and its local log hides the remote prefix.DeleteRecordsmoved the cross-tier earliest offset to the diskless start or past it, the endpoint returnsOFFSET_OUT_OF_RANGEbefore it asks remote storage. The reset then starts the local log at that offset.DeleteRecordsat the seal runs only the local leg, so before the first prune the WAL start stays below the seal. Without this check, the fetcher waits for coverage ofseal - 1that never returns, or rebuilds a deleted prefix from a segment that remote storage still lists.CLASSIC_TO_DISKLESS_SWITCH.mddescribes the recorded epoch, the replica check, the recreate, and the phase 2 rebuild.DISKLESS_CONSOLIDATION.mddescribes the first-prune bound, the deleted-prefix reset, the retention guard, and why a replica with stale remote progress is recreated.A leader uploads only segments below its last stable offset, so the partition had committed every record in remote storage when its leader uploaded it. Phase 1 blocks unclean leader election. An unclean leader election before the switch, through the cluster default or an
ElectLeadersrequest, can still discard uploaded records, and the phase 2 rebuild handles that case.inkless-release-0.50has theSetClassicPrefixInRemoteStorageRPC and tag103, but its brokers don't send the flag request. So no released partition is in phase 2 without the recorded epoch.Limitations
lastClassicLeaderEpochis-1, brokers skip the check and use the upstream rebuild. Besides an empty prefix, that covers a flag that a controller or leader without this change set, as during a rolling upgrade. It also covers a metadata snapshot that such a controller wrote.Partition.makeLeaderrecords at the log end offset is later replaced when consolidation appends batches in the older diskless epoch. The leader epoch cache then maps consolidated records to the classic epoch, sohasConsolidatedDisklessSuffixFromSealfails on the next leadership change.maintruncates that replica to the seal and consolidates again. With this change, the broker recreates it, which costs the same consolidation plus the rebuild of the epoch cache. The cache issue predates this change and needs its own fix, tracked in KC-727.TierStateMachinerebuild, like a leader that lost its local log. A gap in that rebuild affects both cases.RemoteLogManager.Assisted-by: Claude Opus 5.5