Skip to content

[v25.2.x] [CORE-16844] kafka/client: recover from offset_out_of_range in consumer group fetch - #31206

Merged
bartoszpiekny-redpanda merged 3 commits into
redpanda-data:v25.2.xfrom
vbotbuildovich:ai-backport-pr-31064-v25.2.x-1784623924
Jul 21, 2026
Merged

[v25.2.x] [CORE-16844] kafka/client: recover from offset_out_of_range in consumer group fetch#31206
bartoszpiekny-redpanda merged 3 commits into
redpanda-data:v25.2.xfrom
vbotbuildovich:ai-backport-pr-31064-v25.2.x-1784623924

Conversation

@vbotbuildovich

Copy link
Copy Markdown
Collaborator

Backport of PR #31064

  • Command: git cherry-pick -x f12f363 80417ce
  • Commits backported: 2
  • Conflicts resolved: 1
  • Commits skipped (already on target): 0
  • Backport branch: ai-backport-pr-31064-v25.2.x-1784623924

Conflict details

  • f12f363 (src/v/kafka/client/consumer.cc): conflict in the <seastar/core/*> include block — the target branch already had #include <seastar/core/loop.hh> while the cherry-picked commit added #include <seastar/core/lowres_clock.hh>. Both includes are needed (loop.hh on the target branch, lowres_clock.hh for the new fetch-round timeout logic), so both were kept in sorted order.

Pandaproxy's consumer group fetch started every fresh assignment at
offset 0 and never advanced once retention moved the log start offset
past it, so every fetch returned offset_out_of_range forever despite
auto.offset.reset=earliest -- the only reset policy pandaproxy accepts.

fetch_session now exposes three named operations instead of one
overloaded apply(): apply() advances offsets from a delivered response,
discard() advances only the session epoch, and reseed() sets a
partition's offset directly. consumer::fetch() collects every broker's
response, then:

  - reseeds out-of-range partitions to the broker-reported
    log_start_offset. earliest is exactly the log start, and the broker
    already returns it in the fetch response, so no separate ListOffsets
    is needed.
  - strips the out-of-range partitions from the response, since the
    pandaproxy serializer rejects any partition error. No offset
    advances past undelivered records, so nothing is silently skipped.
  - only on a dispatch failure -- a whole broker's response missing,
    where the topology may be stale -- discards the round and throws, so
    the retry in client::consumer_fetch() re-fetches and refreshes
    metadata.

A round whose only outcome was the reseed carries no records, so instead
of returning an empty poll and deferring the data -- which already
exists at the reseeded offset -- to the client's next poll, fetch()
repeats the round within the caller's timeout budget until it has data,
nothing was reseeded, or the budget is spent. A round that delivered
records (healthy partitions) returns immediately; an out-of-range
sibling resumes on the next poll. Recovering per partition rather than
discarding the whole round avoids re-reading the healthy partitions on
every retention/trim edge.

This mirrors the in-poll recovery of franz-go and the Java consumer
(Confluent REST proxy): a timer-bounded poll() loop that resets the
position and refetches across iterations, returning data once available
or empty when the timer expires (apache/kafka 3.6):

      do {
          updateAssignmentMetadataIfNeeded(timer, false); // resets position
          final Fetch<K, V> fetch = pollForFetches(timer); // (re)fetches
          if (!fetch.isEmpty()) { ...; return records; }
      } while (timer.notExpired());
      return ConsumerRecords.empty();

  poll loop:             https://github.com/apache/kafka/blob/3.6/clients/src/main/java/org/apache/kafka/clients/consumer/KafkaConsumer.java#L1174-L1207
  out-of-range detect:   https://github.com/apache/kafka/blob/3.6/clients/src/main/java/org/apache/kafka/clients/consumer/internals/AbstractFetch.java#L654-L666
  reset via ListOffsets: https://github.com/apache/kafka/blob/3.6/clients/src/main/java/org/apache/kafka/clients/consumer/internals/OffsetFetcher.java#L109-L116

Adds fetch_session unit tests for the split API.

(cherry picked from commit f12f363)
Trim a topic's log prefix past offset 0 and assert a fresh consumer
group polls its way to the records at the new log start offset,
producing one record per call so each lands in its own batch and the
trim offset falls on a batch boundary, as real retention does.

A second test trims only one of two partitions and asserts the healthy
sibling's records are delivered while the out-of-range partition
recovers, covering the per-partition recovery path.

(cherry picked from commit 80417ce)
@vbotbuildovich vbotbuildovich added this to the v25.2.x-next milestone Jul 21, 2026
@vbotbuildovich vbotbuildovich added the kind/backport PRs targeting a stable branch label Jul 21, 2026
The backported fetch_session.cc was formatted by dev's clang-format
(LLVM 22); v25.2.x lints with clang-format 18, which indents the
designated initializers differently. Reformat to satisfy the release
branch's toolchain.
@vbotbuildovich

Copy link
Copy Markdown
Collaborator Author

CI test results

test results on build#87469
test_status test_class test_method test_arguments test_kind job_url passed reason test_history
FLAKY(PASS) ClusterConfigLegacyDefaultTest test_legacy_default_explicit_after_upgrade {"wipe_cache": false} integration https://buildkite.com/redpanda/redpanda/builds/87469#019f8416-4223-4b63-a059-d6c3023f33cc 10/11 Test PASSES after retries.No significant increase in flaky rate(baseline=0.0000, p0=1.0000, reject_threshold=0.0100. adj_baseline=0.1000, p1=0.3487, trust_threshold=0.5000) https://redpanda.metabaseapp.com/dashboard/87-tests?tab=142-dt-individual-test-history&test_class=ClusterConfigLegacyDefaultTest&test_method=test_legacy_default_explicit_after_upgrade
FLAKY(PASS) CompactionRecoveryTest test_index_recovery null integration https://buildkite.com/redpanda/redpanda/builds/87469#019f8416-4222-486a-aec2-4ef97cb2a7d7 10/11 Test PASSES after retries.No significant increase in flaky rate(baseline=0.0000, p0=1.0000, reject_threshold=0.0100. adj_baseline=0.1000, p1=0.3487, trust_threshold=0.5000) https://redpanda.metabaseapp.com/dashboard/87-tests?tab=142-dt-individual-test-history&test_class=CompactionRecoveryTest&test_method=test_index_recovery
FLAKY(PASS) ShadowIndexingLocalRetentionTest test_shadow_indexing_non_default_local_retention {"cloud_storage_type": 2} integration https://buildkite.com/redpanda/redpanda/builds/87469#019f8416-8707-41ae-b755-f6b9f792f52a 10/11 Test PASSES after retries.No significant increase in flaky rate(baseline=0.0606, p0=1.0000, reject_threshold=0.0100. adj_baseline=0.1710, p1=0.1533, trust_threshold=0.5000) https://redpanda.metabaseapp.com/dashboard/87-tests?tab=142-dt-individual-test-history&test_class=ShadowIndexingLocalRetentionTest&test_method=test_shadow_indexing_non_default_local_retention

@bartoszpiekny-redpanda
bartoszpiekny-redpanda merged commit 2fe0fc0 into redpanda-data:v25.2.x Jul 21, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/redpanda kind/backport PRs targeting a stable branch

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants