Skip to content

Commit c434993

Browse files
xiangfu0claude
andcommitted
Fix actual seek-on-empty-poll wedge in KafkaPartitionLevelConsumer
Spotted a bug in my own previous attempt while staring at the unchanged CI failure: setting _lastFetchedOffset = startOffset - 1 in the empty-poll branch does not skip the re-seek when startOffset == 0 (= -1, which still trips the _lastFetchedOffset < 0 clause of the seek-check). That clause was the original intent's "have we ever fetched" sentinel and a sentinel value of -1 is indistinguishable from "we haven't fetched yet" and "we fetched at offset 0". Replace with an explicit _lastSeekedStartOffset (Long.MIN_VALUE initially) that tracks whether we've already issued a seek for the caller's startOffset. The seek-check is now: firstSeekForThisOffset = _lastSeekedStartOffset != startOffset if (firstSeekForThisOffset || _lastFetchedOffset != startOffset - 1) The first time the caller asks for startOffset = N, we seek and record N. On subsequent calls with the same startOffset we do NOT re-seek, so the consumer's internal fetch session can advance through aborted records on later polls instead of being reset to startOffset every time. This is the part that the previous fix missed -- _lastFetchedOffset = -1 + the existing < 0 check kept re-seeking even after we marked the offset. Mirrored to both pinot-kafka-3.0 and pinot-kafka-4.0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 27d4c4b commit c434993

2 files changed

Lines changed: 34 additions & 7 deletions

File tree

pinot-plugins/pinot-stream-ingestion/pinot-kafka-3.0/src/main/java/org/apache/pinot/plugin/stream/kafka30/KafkaPartitionLevelConsumer.java

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,14 @@ public class KafkaPartitionLevelConsumer extends KafkaPartitionLevelConnectionHa
4747
private static final Logger LOGGER = LoggerFactory.getLogger(KafkaPartitionLevelConsumer.class);
4848

4949
private long _lastFetchedOffset = -1;
50-
// Diagnostic counters: number of consecutive fetch calls that returned no records, and
51-
// the last log of the empty-poll path. Logged at WARN/ERROR every ~50 empty polls so a
52-
// long stall surfaces in CI surefire output without spamming the success path.
50+
// True once the consumer has been positioned (via seek or assign) for the current
51+
// startOffset and has had at least one poll attempt. Tracked separately from
52+
// _lastFetchedOffset because, with read_committed isolation, the very first poll can
53+
// legitimately return zero records (all records were aborted) and we must NOT re-seek
54+
// on the next call -- doing so would undo the consumer's internal advance through the
55+
// aborted region and wedge us forever at startOffset = 0.
56+
private long _lastSeekedStartOffset = Long.MIN_VALUE;
57+
// Diagnostic counter: consecutive fetch calls that returned no records.
5358
private int _consecutiveEmptyPolls = 0;
5459

5560
public KafkaPartitionLevelConsumer(String clientId, StreamConfig streamConfig, int partition) {
@@ -67,11 +72,19 @@ public synchronized KafkaMessageBatch fetchMessages(StreamPartitionMsgOffset sta
6772
if (LOGGER.isDebugEnabled()) {
6873
LOGGER.debug("Polling partition: {}, startOffset: {}, timeout: {}ms", _topicPartition, startOffset, timeoutMs);
6974
}
70-
if (_lastFetchedOffset < 0 || _lastFetchedOffset != startOffset - 1) {
75+
// Seek if (a) we've never positioned the consumer, OR (b) the caller's startOffset
76+
// moved off the position we last tracked. _lastFetchedOffset < 0 alone is NOT a
77+
// sufficient seek trigger because, with read_committed, an empty first poll keeps
78+
// _lastFetchedOffset at its initial -1 even though the consumer's internal position
79+
// has moved past aborted records; re-seeking on every empty poll would undo that
80+
// progress and wedge consumption forever at startOffset.
81+
boolean firstSeekForThisOffset = _lastSeekedStartOffset != startOffset;
82+
if (firstSeekForThisOffset || _lastFetchedOffset != startOffset - 1) {
7183
if (LOGGER.isDebugEnabled()) {
7284
LOGGER.debug("Seeking to offset: {}", startOffset);
7385
}
7486
_consumer.seek(_topicPartition, startOffset);
87+
_lastSeekedStartOffset = startOffset;
7588
}
7689

7790
ConsumerRecords<Bytes, Bytes> consumerRecords = _consumer.poll(Duration.ofMillis(timeoutMs));

pinot-plugins/pinot-stream-ingestion/pinot-kafka-4.0/src/main/java/org/apache/pinot/plugin/stream/kafka40/KafkaPartitionLevelConsumer.java

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,14 @@ public class KafkaPartitionLevelConsumer extends KafkaPartitionLevelConnectionHa
4747
private static final Logger LOGGER = LoggerFactory.getLogger(KafkaPartitionLevelConsumer.class);
4848

4949
private long _lastFetchedOffset = -1;
50-
// Diagnostic counter: consecutive fetch calls that returned no records. Logged at WARN
51-
// every ~50 empty polls so a long stall surfaces in CI surefire output without spam.
50+
// True once the consumer has been positioned (via seek or assign) for the current
51+
// startOffset and has had at least one poll attempt. Tracked separately from
52+
// _lastFetchedOffset because, with read_committed isolation, the very first poll can
53+
// legitimately return zero records (all records were aborted) and we must NOT re-seek
54+
// on the next call -- doing so would undo the consumer's internal advance through the
55+
// aborted region and wedge us forever at startOffset = 0.
56+
private long _lastSeekedStartOffset = Long.MIN_VALUE;
57+
// Diagnostic counter: consecutive fetch calls that returned no records.
5258
private int _consecutiveEmptyPolls = 0;
5359

5460
public KafkaPartitionLevelConsumer(String clientId, StreamConfig streamConfig, int partition) {
@@ -66,11 +72,19 @@ public synchronized KafkaMessageBatch fetchMessages(StreamPartitionMsgOffset sta
6672
if (LOGGER.isDebugEnabled()) {
6773
LOGGER.debug("Polling partition: {}, startOffset: {}, timeout: {}ms", _topicPartition, startOffset, timeoutMs);
6874
}
69-
if (_lastFetchedOffset < 0 || _lastFetchedOffset != startOffset - 1) {
75+
// Seek if (a) we've never positioned the consumer for this startOffset, OR (b) the
76+
// caller's startOffset moved off the position we last tracked. _lastFetchedOffset < 0
77+
// alone is NOT a sufficient seek trigger because, with read_committed, an empty
78+
// first poll keeps _lastFetchedOffset at its initial -1 even though the consumer's
79+
// internal position has moved past aborted records; re-seeking on every empty poll
80+
// would undo that progress and wedge consumption forever at startOffset.
81+
boolean firstSeekForThisOffset = _lastSeekedStartOffset != startOffset;
82+
if (firstSeekForThisOffset || _lastFetchedOffset != startOffset - 1) {
7083
if (LOGGER.isDebugEnabled()) {
7184
LOGGER.debug("Seeking to offset: {}", startOffset);
7285
}
7386
_consumer.seek(_topicPartition, startOffset);
87+
_lastSeekedStartOffset = startOffset;
7488
}
7589

7690
ConsumerRecords<Bytes, Bytes> consumerRecords = _consumer.poll(Duration.ofMillis(timeoutMs));

0 commit comments

Comments
 (0)