Skip to content

fix(inkless:consolidation): stop over-reclaiming the classic prefix on rebuilt leaders [KC-332]#711

Open
viktorsomogyi wants to merge 9 commits into
mainfrom
svv/ts-unification-reclaim-floor
Open

fix(inkless:consolidation): stop over-reclaiming the classic prefix on rebuilt leaders [KC-332]#711
viktorsomogyi wants to merge 9 commits into
mainfrom
svv/ts-unification-reclaim-floor

Conversation

@viktorsomogyi

@viktorsomogyi viktorsomogyi commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Builds on the two merged consolidation fixes (#709 ListOffsets(EARLIEST) routing and #710 DeleteRecords forwarding). Those made the control plane the authoritative cross-tier earliest for reads and the DeleteRecords write path. This PR carries that same authority into remote-retention reclaim and the become-leader log-start report, so
a consolidating topic never deletes remote data that is still within the delete/retention boundary, and never reports a stale earliest.

Background

After a classic topic switches to diskless, the classic prefix [0, seal) lives only in the remote tier. A broker that rebuilds the partition resumes its local log at the seal, so the whole-log UnifiedLog.logStartOffset is pinned at the seal by the tier-state rebuild and only ever increments. Two things then read that stale seal instead of the
true cross-tier earliest X that DeleteRecords/retention left in the control plane:

  • the RemoteLogManager reclaim floor deleted the whole remote prefix [X, seal) and reported the seal as the earliest on becoming leader;
  • a fetch in [X, seal) was rejected as out of range and the rebuild restarted at the seal. The reclaim leg is irreversible, so this is a data-loss path, not just a stale read.

What changed

  • Whole-log start for the consolidation fetch/rebuild (DisklessLeaderEndPoint): a consolidating partition now advertises min(crossTierEarliest, localLog.logStartOffset) as the whole-log start instead of the raw local seal, so a fetch in [X, seal) rebuilds from the remote tier and the rebuild restarts the log at X.
  • Reclaim floor and become-leader report (RemoteLogManager, wired in BrokerServer): both now consult a control-plane override for consolidating partitions. On an empty override they fail safe to the remote earliest (findLogStartOffset), never the seal; classic/non-inkless topics keep the upstream broker-local behavior.
  • Raw remote-log-start accessor (ReplicaManager.crossTierRemoteLogStartOffset + new ControlPlane.getCrossTierLogStart): the reclaim floor reads the raw remote_log_start_offset, null-aware, rather than ListOffsets(EARLIEST). EARLIEST returns COALESCE(remote_log_start_offset, log_start_offset), which falls back to the pruned WAL frontier when the remote start is unreported. Using that as the floor would delete still-live remote segments and lock the wrong value in through the forward-only advance. Returning empty here makes the RLM fail safe to the true remote earliest instead. crossTierEarliestOffset keeps the COALESCE fallback for reads, where the worst case is a transient, self-healing over-rejection.
  • New control-plane query and metric: GetCrossTierLogStartJob (in-memory + Postgres) reads the raw column directly, with a GetCrossTierLogStart timer in PostgresControlPlaneMetrics and a metrics.rst entry.
  • Error handling: the cross-tier accessors catch Exception rather than Throwable, so Errors propagate instead of silently failing open.

Testing

  • RemoteLogManagerTest: reclaim floor honored over the seal; fail-safe on empty override for consolidating vs. upstream behavior for classic; retention x DeleteRecords composition; and hardening tests proving the become-leader report resolves to the remote earliest (never the seal, never skipped), that findLogStartOffset returns the remote earliest despite a seal-pinned local start, and a combined bootstrap-plus-reclaim guard.
  • ReplicaManagerInklessTest / DisklessLeaderEndPointTest: the accessors, the consolidating predicate, and the whole-log-start min() behavior.
  • AbstractControlPlaneTest: getCrossTierLogStart is null-aware (returns empty for an unreported remote start) unlike EARLIEST.
  • End-to-end coverage lives on the stacked e2e branch (DeleteRecordsAcrossTiersTest), which asserts the earliest settles at exactly the delete boundary on every broker with retention unset.

Compatibility / risk

No behavior change for classic or non-inkless partitions: the override defaults to empty and the predicate to false, so the reclaim floor stays the broker-local log start. The control-plane advance is forward-only, and a stale cross-tier cache entry can only be too low, which under-reclaims and over-serves rather than losing data.

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.

Pull request overview

This PR fixes an Inkless consolidation safety issue where a freshly rebuilt consolidating leader could cause RemoteLogManager to over-reclaim the remote classic prefix by using a broker-local logStartOffset pinned at the classic-to-diskless seal. It introduces a control-plane driven log-start override for reclaim decisions and adjusts Diskless fetch behavior to use a broker-agnostic whole-log start.

Changes:

  • Add a logStartOffsetOverride and isConsolidatingDisklessPartition predicate to RemoteLogManager to drive a safe reclaim floor (and fail-safe when the override is unavailable).
  • Wire both lambdas from BrokerServer via ReplicaManager (control-plane cross-tier earliest + consolidating predicate).
  • Update DisklessLeaderEndPoint to report min(crossTierEarliest, localLogStart) as the whole-log start, with new targeted tests.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
storage/src/test/java/org/apache/kafka/server/log/remote/storage/RemoteLogManagerTest.java Adds regression tests covering override-driven reclaim floor, fail-safe behavior when override is absent, and interactions with active retention.
storage/src/main/java/org/apache/kafka/server/log/remote/storage/RemoteLogManager.java Introduces override + consolidating predicate plumbing and uses them to compute a safe remote-reclaim floor (and leader log-start reporting).
core/src/test/scala/io/aiven/inkless/consolidation/DisklessLeaderEndPointTest.scala Adds tests asserting whole-log start uses cross-tier earliest for consolidating leaders (and documents fallback behavior).
core/src/main/scala/kafka/server/BrokerServer.scala Wires ReplicaManager-backed override and consolidating predicate into RemoteLogManager.
core/src/main/scala/io/aiven/inkless/consolidation/DisklessLeaderEndPoint.scala Uses min(crossTierEarliest, localLogStart) as whole-log start to avoid rejecting reads of surviving remote prefixes.

@viktorsomogyi
viktorsomogyi force-pushed the svv/ts-unification-delete-records-routing branch 2 times, most recently from 2549301 to 634299e Compare July 22, 2026 13:29
@viktorsomogyi
viktorsomogyi force-pushed the svv/ts-unification-reclaim-floor branch 2 times, most recently from 654af13 to 2336424 Compare July 22, 2026 13:48
@viktorsomogyi
viktorsomogyi force-pushed the svv/ts-unification-delete-records-routing branch from 634299e to bbebd3d Compare July 22, 2026 15:13
@viktorsomogyi
viktorsomogyi force-pushed the svv/ts-unification-reclaim-floor branch from 2336424 to ea588ff Compare July 22, 2026 15:14
@viktorsomogyi
viktorsomogyi force-pushed the svv/ts-unification-delete-records-routing branch from bbebd3d to e4eac32 Compare July 23, 2026 07:06
Base automatically changed from svv/ts-unification-delete-records-routing to main July 23, 2026 07:39
@viktorsomogyi
viktorsomogyi changed the base branch from main to svv/ts-unification-wal-fetcher July 23, 2026 07:47
@viktorsomogyi
viktorsomogyi changed the base branch from svv/ts-unification-wal-fetcher to main July 23, 2026 07:47
@viktorsomogyi
viktorsomogyi marked this pull request as ready for review July 23, 2026 07:47
@viktorsomogyi
viktorsomogyi force-pushed the svv/ts-unification-reclaim-floor branch from ea588ff to 66ddbb7 Compare July 23, 2026 07:53
@viktorsomogyi
viktorsomogyi force-pushed the svv/ts-unification-reclaim-floor branch from 52cf871 to 66ddbb7 Compare July 23, 2026 08:53
Comment on lines +925 to +928
OptionalLong override = logStartOffsetOverride.apply(topicIdPartition.topicPartition());
long logStartOffset = override.isPresent()
? override.getAsLong()
: findLogStartOffset(topicIdPartition, log);

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.

ReplicaManager.crossTierEarliestOffset returns ListOffsets(EARLIEST) = COALESCE(remote_log_start_offset, log_start_offset). If remote_log_start_offset is NULL due to an error in reporting the value, the cross tier earliest offset returned is the pruned WAL frontier, so there's the chance that all data from remote gets deleted in case this happens.

Example:

  1. Born-consolidated topic; records [0,1000) produced, [0,700) tiered to remote.
  2. The CrossTierLogStartReporter never lands a successful flush for this partition — crash between enqueue and the 1s flush, a persistent partition-specific advanceCrossTierLogStartOffset failure, or an enqueue guard drop during post-switch metadata propagation. remote_log_start_offset stays NULL.
  3. The 5-min WAL pruner advances log_start_offset to 700.
  4. A leader runs become-leader / expiration. crossTierEarliestOffset returns COALESCE(NULL, 700) = 700 → present.
  5. reclaimFloorLogStartOffset returns 700 → deletes remote [0,700), which was still within retention. And become-leader reports 700 → the reporter persists it via the forward-only advance_cross_tier_log_start_v1, so remote_log_start_offset is now permanently 700 and EARLIEST is permanently wrong.

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.

I think this is a good catch, let me fix it.

@viktorsomogyi
viktorsomogyi force-pushed the svv/ts-unification-reclaim-floor branch 2 times, most recently from 86e3b1a to a6f0a03 Compare July 23, 2026 15:37
@viktorsomogyi

Copy link
Copy Markdown
Contributor Author

@giuseppelillo addressed the bug you discovered, PTAL.

@viktorsomogyi
viktorsomogyi force-pushed the svv/ts-unification-reclaim-floor branch 2 times, most recently from bbca8b1 to f6c0f80 Compare July 23, 2026 15:47
@viktorsomogyi
viktorsomogyi marked this pull request as draft July 24, 2026 09:25
@viktorsomogyi

Copy link
Copy Markdown
Contributor Author

Moving this back to draft until I resolve the test failures.

@viktorsomogyi
viktorsomogyi force-pushed the svv/ts-unification-reclaim-floor branch from f6c0f80 to e3f351d Compare July 24, 2026 12:17
…n rebuilt leaders [KC-332]

A freshly-rebuilt consolidating leader had its local
logStartOffset pinned at the classic-to-diskless seal, so the
RemoteLogManager reclaimed the entire remote classic prefix
[earliest, seal) regardless of DeleteRecords / retention. RemoteLogManager
now takes a logStartOffsetOverride (the control-plane cross-tier earliest)
as its reclaim floor, and DisklessLeaderEndPoint reports
min(crossTierEarliest, localLogStart) as the whole-log start.

When the override is unavailable (control plane down / metadata not yet
propagated) the RLM fails safe for a consolidating partition. It uses
the remote earliest and defers rather than falling back to the local
seal, so a transient outage can never trigger irreversible over-deletion.
A new isConsolidatingDisklessPartition predicate distinguishes this from
classic topics, which keep the upstream local-log-start floor. BrokerServer
wires both lambdas into the RLM, backed by the shared
ReplicaManager.crossTierEarliestOffset / isConsolidatingDisklessPartition
accessors added here (control-plane cross-tier earliest, write-through cache).

Co-authored-by: Cursor <cursoragent@cursor.com>
viktorsomogyi and others added 7 commits July 24, 2026 15:06
…r [KC-332]

The reclaim floor used ListOffsets(EARLIEST), which COALESCEs to the
WAL prune frontier when remote_log_start_offset is NULL. That frontier
can run ahead of the true remote start, so using it as the floor would
over-reclaim live remote segments and lock the wrong value in via the
forward-only advance.

Add a dedicated crossTierRemoteLogStartOffset that reads
remote_log_start_offset directly, null-aware. When it is NULL the RLM
fails safe to the true remote earliest instead. crossTierEarliestOffset
keeps the COALESCE fallback for the read path.
…floor [KC-332]

Update the reclaim-floor comments to reference the raw remote log start
instead of the COALESCE'd cross-tier earliest, and explain why
ListOffsets(EARLIEST) is not used. Rename the local to match.
…KC-332]

advanceCrossTierEarliestForDeleteRecords caught Throwable, which would
swallow Errors (OutOfMemoryError, StackOverflowError) and fail the
control-plane report silently. Narrow to Exception so Errors propagate.
…hared-state branch [KC-332]

Add a test for the sharedState == null branch of
crossTierRemoteLogStartOffset, mirroring the existing
isConsolidatingDisklessPartition test. Verifies the accessor returns
empty without touching the control plane.
…C-332]

Wire GetCrossTierLogStartJob through the timed JobUtils.run and add a
PostgresControlPlaneMetrics entry for it. Regenerate metrics.rst.
…st comments [KC-332]

The Problem A/B names only exist in local design notes, so reviewers have
no context for them. Replace them with self-describing wording (cross-broker
EARLIEST consistency, seal-based over-reclaim) and keep the comments' intent.

Co-authored-by: Cursor <cursoragent@cursor.com>
…eport against over-reclaim [KC-332]

Cover the consolidating data-loss paths directly. When the cross-tier
remote start override is empty, the become-leader report resolves to the
remote earliest (findLogStartOffset), never the local seal and never
skipped, so remote_log_start_offset heals instead of leaving EARLIEST to
COALESCE to the pruned WAL frontier. When the override is present it is
reported verbatim. findLogStartOffset returns the remote earliest even
though the rebuilt leader's local start is pinned at the seal, and a
combined bootstrap-plus-reclaim guard asserts both the report and the
reclaim floor use that earliest so the classic prefix is not deleted.

Co-authored-by: Cursor <cursoragent@cursor.com>
@viktorsomogyi
viktorsomogyi force-pushed the svv/ts-unification-reclaim-floor branch from e3f351d to 5079dcd Compare July 24, 2026 13:07
@viktorsomogyi
viktorsomogyi marked this pull request as ready for review July 24, 2026 13:07
@viktorsomogyi

Copy link
Copy Markdown
Contributor Author

@giuseppelillo the tests should pass now, it's ready for review again.

…ched topic [KC-332]

Add an integration test that deletes records below the seal on a two-partition
consolidated topic and asserts the earliest settles at exactly the requested
per-partition boundary, never over-reclaiming the surviving classic prefix down
to the seal. Also covers the forward-only control-plane advance (a repeat delete
below the current start is a no-op) and asserts remote_log_start_offset
bootstraps to 0 in the control plane rather than staying NULL.

Co-authored-by: Cursor <cursoragent@cursor.com>
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