Skip to content

KAFKA-20711: Measure restore-remaining-records in offset slots#22614

Open
nicktelford wants to merge 1 commit into
apache:trunkfrom
nicktelford:KAFKA-20711-restore-remaining-offset-slots
Open

KAFKA-20711: Measure restore-remaining-records in offset slots#22614
nicktelford wants to merge 1 commit into
apache:trunkfrom
nicktelford:KAFKA-20711-restore-remaining-offset-slots

Conversation

@nicktelford

@nicktelford nicktelford commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

The restore-remaining-records-total task metric overshoots and never
reaches zero under EOS (and, more mildly, on compacted changelogs). The
metric is initialized in prepareChangelogs from the offset range
restoreEndOffset - startOffset, which counts every offset slot —
including those occupied by transaction markers and
aborted/compacted-away records. It was then decremented in
restoreChangelog by the number of records actually restored
(bufferedLimitIndex). Because the read_committed restore consumer
never returns markers or aborted records (though its position advances
past them), the sum of the decrements is always smaller than the initial
value by exactly the number of those offset slots, so the metric stays
stuck at a positive value even after restoration completes.

The fix makes the decrements consistent with the initialization by
expressing the remaining-records metric's progress in changelog offset
slots rather than record counts. Each batch advances the metric by the
change in the store's offset (storeMetadata.offset(), which
restore() sets to the last restored record's offset), and on
completion any trailing slots between the last restored record and the
end offset are accounted for, so the metric lands at exactly zero. A new
restoreStartOffset field records the consumer position at which
restoration began, so the first batch is measured from the true start
(covering the from-the-beginning case and any leading marker offsets).

Task#recordRestoration now takes the offset-slot count separately from
the record count: the offset-slot count drives
restore-remaining-records-total, while restore-total/restore-rate
(and the standby update-total/update-rate) measure the records
actually restored. The per-batch listener callbacks
(onBatchRestored/onBatchLoaded) and totalRestored likewise report
true record counts.

A unit test reproduces the gap (a changelog whose consumer position
advances past offsets that are never returned as records) and asserts
the metric decrements to exactly zero on completion.

🤖 Generated with Claude Code

Reviewers: Matthias J. Sax matthias@confluent.io

@github-actions github-actions Bot added triage PRs from the community streams labels Jun 18, 2026

@mjsax mjsax left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the fix.

On thing I am wondering is about restore-total and restore-rate metric. My understanding is, that both would measure how many actual records we did restore. So if we add TX-markers and offset-gaps (from compaction), it seems these metrics would not become incorrect?

There a point about keeping restore-remaining and restore-total "in sync", but in the end restore-remaining can only be an estimation of how many actually records are remaining. Ideally restore-remaining would tell us how many records are remaining, but this is something we just don't know, and can only estimate via offsets -- but it seem to go a step too far to "mess" with the other two metrics which are correct now.

final Long previousOffset,
final long restoredToOffset) {
final long restoredFrom = previousOffset == null ? changelogMetadata.restoreStartOffset - 1 : previousOffset;
if (restoredToOffset > restoredFrom) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why do we need this guard? Should this not be always true? For which case it would be false?

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.

This is to defend against previousOffset == restoredToOffset. Maybe this is overly defensive. I think this might be possible if the StateStore has a committed offset > restoredToOffset, but I don't think that's possible, since the committed offset will dictate where the restore begins.

Would you like me to remove this?

@mjsax mjsax added ci-approved and removed triage PRs from the community labels Jun 24, 2026
@nicktelford

Copy link
Copy Markdown
Contributor Author

Thanks for the fix.

On thing I am wondering is about restore-total and restore-rate metric. My understanding is, that both would measure how many actual records we did restore. So if we add TX-markers and offset-gaps (from compaction), it seems these metrics would not become incorrect?

There a point about keeping restore-remaining and restore-total "in sync", but in the end restore-remaining can only be an estimation of how many actually records are remaining. Ideally restore-remaining would tell us how many records are remaining, but this is something we just don't know, and can only estimate via offsets -- but it seem to go a step too far to "mess" with the other two metrics which are correct now.

@mjsax Yeah, I opted to change the semantics of restore-total and restore-rate, because I think it will be more problematic for users if they measure something different to restore-remaining. Ultimately, I doubt anyone is dependent on restore-total/restore-rate not including transaction markers and offset gaps.

A concrete example here is that if we ensure these metrics all measure the same thing, you can derive an estimated time remaining for restore by calculating restore-remaining / restore-rate, which would be much less accurate if the two metrics tracked different things.

It would of course be best for restore-remaining to report actual records, but since that's not possible, I think aligning the other metrics on offsets is the next-best option.

@mjsax

mjsax commented Jul 15, 2026

Copy link
Copy Markdown
Member

@nicktelford -- I was thinking about this more, and also discussed with other. -- I don't think we should tightly couple restore-remaining-records-total and restore-total/restore-rate.

Looking into the code, it seems both are already captured by independent Sensors inside StreamTask, so it seems a proper fix would be to change Task#recordRestoration(...) and pass in both the existing numRecords (to be used for restore-total/restore-rate) and the newly added restoredToOffset - restoredFrom from you fix (to be used for restore-remaining-records-total).

We might also want to update the description of both metrics, to call out the difference, and that one is exact, while the other is an estimation.

@nicktelford

nicktelford commented Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

@nicktelford -- I was thinking about this more, and also discussed with other. -- I don't think we should tightly couple restore-remaining-records-total and restore-total/restore-rate.

Looking into the code, it seems both are already captured by independent Sensors inside StreamTask, so it seems a proper fix would be to change Task#recordRestoration(...) and pass in both the existing numRecords (to be used for restore-total/restore-rate) and the newly added restoredToOffset - restoredFrom from you fix (to be used for restore-remaining-records-total).

We might also want to update the description of both metrics, to call out the difference, and that one is exact, while the other is an estimation.

@mjsax the problem with having those metrics record subtly different things is that you can't combine them to determine "time remaining" = restore-remaining / restore-rate. This, for me at least, is the killer feature of these metrics.

More generally, users don't have visibility into consumable records vs. transaction markers/aborted records on a Kafka topic: we just see offsets. All other Kafka topic metrics are based on log offsets, not number of consumable records, so this is a bit of an outlier.

To provide a hypothetical, pathological example: it's possible for a topic to contain a large number of aborted transactions, compacted-away records, transaction markers etc. and very few real records. If this happens, users would see their application is "restoring", but then see the restore-rate as much lower than they would expect.

Another perspective: when consumers use a regular Kafka consumer group, the consumed offsets are stored on the broker, and made available via metrics. This enables users to calculate things like lag, consumption rate, etc. from just the broker metrics, and these metrics are entirely offset-based. However, since the restore consumer doesn't use a consumer group (it stores its offsets inside StateStores) the only metrics available to users are the restore-total/restore-rate/restore-remaining metrics. For symmetry, it would be useful to users for these to also be offset-based.

I understand why you're unwilling to change the semantics of existing metrics: perhaps the answer is to create some new metrics that track offsets instead of restored records?

@mjsax

mjsax commented Jul 16, 2026

Copy link
Copy Markdown
Member

Well, consumers do already exposes records-lag on a per-partition basis. Won't this do the trick? But sure, we could also do a KIP to add a new offset base metric similar to restore-total / restore-rate. But it seems this would be a follow up PR?

While your example with aborted TX and TX markers is "valid", the same problem exist for log-compaction with the main difference that many offsets don't exist any longer. The log compaction case is the more common one (I would only expect a low percentage of aborted TX), and if we would count non-existing offset as part of restore rate, it could be very off, and it might also vary a lot (ie, older segments with more compaction would have more missing offsets, while newer segment might not be compacted yet, appearing to restore slower, even if they are not, if we look at offsets).

This enables users to calculate things like lag, consumption rate, etc. from just the broker metrics, and these metrics are entirely offset-based.

Maybe, but for compacted topic this approach seems to be broken? In the end, restore-total / restore-rate should be a metric about actually performed restore work, ie, data transferred over the network etc. Counting non-existing offsets from log-compacted topics would make these metrics "useless" to get a sense of actual restore "speed"?

The restore-remaining-records-total metric is initialized from the
changelog offset range (restoreEndOffset - startOffset), which counts
every offset slot, but it was decremented by the number of records
actually restored. These diverge whenever the changelog contains
offsets the restore consumer never returns as records -- transaction
markers or compacted-away records -- leaving the metric stuck above
zero after restoration completed.

The metric is now decremented in offset slots: each batch advances it
by the change in store offset, and on completion any trailing slots
between the last restored record and the end offset are accounted for,
so it lands at exactly zero. Task#recordRestoration takes the
offset-slot count separately from the record count so that
restore-total and restore-rate continue to measure records restored.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@nicktelford nicktelford changed the title KAFKA-20711: Measure restore progress in offset slots KAFKA-20711: Measure restore-remaining-records in offset slots Jul 17, 2026
@nicktelford
nicktelford force-pushed the KAFKA-20711-restore-remaining-offset-slots branch from 803a85f to 3ad3020 Compare July 17, 2026 10:47
@nicktelford

nicktelford commented Jul 17, 2026

Copy link
Copy Markdown
Contributor Author

@mjsax I've made the requested changes and rebased against the latest trunk.

You mention records-lag, which makes me ask: is restore-remaining-records-total now essentially a duplicate of records-lag?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants