Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,7 @@ private void resetOffsets() {
final long epoch = getCurrentTimeNanos();
ownedPartitionsEpoch.put(partition, epoch);
} catch (Exception e) {
topicMetrics.getNumberOfOffsetResetFailures().increment();

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.

This will catch both a seek failure and a commit error. Should these be the same metric?

LOG.error("Failed to seek to last committed offset upon negative acknowledgement {}", partition, e);
}
});
Expand Down Expand Up @@ -381,9 +382,11 @@ private void commitOffsets(boolean forceCommit) {
consumer.commitSync(offsetsToCommit);
lastCommitTime = currentTimeMillis;
} catch (final RebalanceInProgressException ex) {
topicMetrics.getNumberOfCommitFailures().increment();
LOG.error("Failed to commit offsets in topic {} due to rebalance in progress", topicName, ex);
return;
} catch (Exception e) {
topicMetrics.getNumberOfCommitFailures().increment();
LOG.error("Failed to commit offsets in topic {}", topicName, e);
}

Expand Down Expand Up @@ -541,6 +544,7 @@ private void processRecords(final AcknowledgementSet acknowledgementSet, final L
if (e instanceof SizeOverflowException) {
topicMetrics.getNumberOfBufferSizeOverflows().increment();
} else {
topicMetrics.getNumberOfBufferWriteFailures().increment();
LOG.debug("Error while adding record to buffer, retrying ", e);
}
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ public class KafkaTopicConsumerMetrics {
static final String NUMBER_OF_RECORDS_COMMITTED = "numberOfRecordsCommitted";
static final String NUMBER_OF_RECORDS_CONSUMED = "numberOfRecordsConsumed";
static final String NUMBER_OF_BYTES_CONSUMED = "numberOfBytesConsumed";
static final String NUMBER_OF_COMMIT_FAILURES = "numberOfCommitFailures";
static final String NUMBER_OF_OFFSET_RESET_FAILURES = "numberOfOffsetResetFailures";
static final String NUMBER_OF_BUFFER_WRITE_FAILURES = "numberOfBufferWriteFailures";
static final String ACTUAL_POLL_INTERVAL = "actualPollInterval";

private final String topicName;
Expand All @@ -49,6 +52,9 @@ public class KafkaTopicConsumerMetrics {
private final Counter numberOfRecordsCommitted;
private final Counter numberOfRecordsConsumed;
private final Counter numberOfBytesConsumed;
private final Counter numberOfCommitFailures;
private final Counter numberOfOffsetResetFailures;
private final Counter numberOfBufferWriteFailures;
private final Timer timeBetweenPollCalls;
private Instant lastPollTime;

Expand All @@ -69,6 +75,9 @@ public KafkaTopicConsumerMetrics(final String topicName, final PluginMetrics plu
this.numberOfPollAuthErrors = pluginMetrics.counter(getTopicMetricName(NUMBER_OF_POLL_AUTH_ERRORS, topicNameInMetrics));
this.numberOfPositiveAcknowledgements = pluginMetrics.counter(getTopicMetricName(NUMBER_OF_POSITIVE_ACKNOWLEDGEMENTS, topicNameInMetrics));
this.numberOfNegativeAcknowledgements = pluginMetrics.counter(getTopicMetricName(NUMBER_OF_NEGATIVE_ACKNOWLEDGEMENTS, topicNameInMetrics));
this.numberOfCommitFailures = pluginMetrics.counter(getTopicMetricName(NUMBER_OF_COMMIT_FAILURES, topicNameInMetrics));
this.numberOfOffsetResetFailures = pluginMetrics.counter(getTopicMetricName(NUMBER_OF_OFFSET_RESET_FAILURES, topicNameInMetrics));
this.numberOfBufferWriteFailures = pluginMetrics.counter(getTopicMetricName(NUMBER_OF_BUFFER_WRITE_FAILURES, topicNameInMetrics));
this.timeBetweenPollCalls = pluginMetrics.timer(getTopicMetricName(ACTUAL_POLL_INTERVAL, topicNameInMetrics));
lastPollTime = Instant.now();
}
Expand Down Expand Up @@ -175,6 +184,18 @@ public Counter getNumberOfPositiveAcknowledgements() {
return numberOfPositiveAcknowledgements;
}

public Counter getNumberOfCommitFailures() {
return numberOfCommitFailures;
}

public Counter getNumberOfOffsetResetFailures() {

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.

The name numberOfOffsetResetFailures is easy to misread. In Kafka, "offset reset" usually points at the auto.offset.reset policy (earliest/latest when there is no committed offset). This counter is about a different thing: the seek back to the last committed offset in resetOffsets() after a negative acknowledgement, so those records get redelivered instead of skipped.

Two details about what it actually counts, in case the name should reflect them:

  • It wraps both the consumer.committed(partition) lookup and the seek, so a failure in either step increments it, not just the seek.
  • It increments per partition inside the loop, once per failed reset attempt.

Because it fires only on the negative-ack redelivery path, a name tied to that path reads more clearly on a dashboard and avoids the auto.offset.reset confusion. A couple of options:

  • numberOfSeekFailures — matches Kafka's own API term (consumer.seek) and stays short.
  • numberOfNegativeAckSeekFailures — longer, but explicit about the trigger.

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.

I like numberOfSeekFailures.

return numberOfOffsetResetFailures;
}

public Counter getNumberOfBufferWriteFailures() {
return numberOfBufferWriteFailures;
}

public void recordTimeBetweenPolls() {
final long timeBetweenPolls = Instant.now().toEpochMilli() - lastPollTime.toEpochMilli();
timeBetweenPollCalls.record(timeBetweenPolls, TimeUnit.MILLISECONDS);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.times;
Expand Down Expand Up @@ -176,6 +177,9 @@ public void setUp() throws JsonProcessingException {
when(topicMetrics.getNumberOfDeserializationErrors()).thenReturn(counter);
when(topicMetrics.getNumberOfInvalidTimeStamps()).thenReturn(counter);
when(topicMetrics.getNumberOfPollAuthErrors()).thenReturn(counter);
when(topicMetrics.getNumberOfCommitFailures()).thenReturn(counter);
when(topicMetrics.getNumberOfOffsetResetFailures()).thenReturn(counter);
when(topicMetrics.getNumberOfBufferWriteFailures()).thenReturn(counter);
when(topicConfig.getThreadWaitingTime()).thenReturn(Duration.ofSeconds(1));
when(topicConfig.getSerdeFormat()).thenReturn(MessageFormat.PLAINTEXT);
when(topicConfig.getAutoCommit()).thenReturn(false);
Expand Down Expand Up @@ -875,11 +879,107 @@ private ConsumerRecords createJsonRecords(String topic) throws Exception {
return new ConsumerRecords(records);
}

@ParameterizedTest
@MethodSource("provideExceptionsFromCommit")
public void testCommitOffsets_whenCommitFails_thenIncrementsCommitFailureCounter(final Exception commitException) throws Exception {
final Counter commitFailureCounter = mock(Counter.class);
when(topicMetrics.getNumberOfCommitFailures()).thenReturn(commitFailureCounter);

final String topic = topicConfig.getName();
final TopicPartition topicPartition = new TopicPartition(topic, testPartition);
when(topicConfig.getCommitInterval()).thenReturn(Duration.ofMillis(0));

consumer = createObjectUnderTest("plaintext", false);
consumer.onPartitionsAssigned(List.of(topicPartition));

consumerRecords = createPlainTextRecords(topic, 100L);
when(kafkaConsumer.poll(any(Duration.class))).thenReturn(consumerRecords);
consumer.consumeRecords();

doThrow(commitException).when(kafkaConsumer).commitSync(anyMap());

// onPartitionsRevoked forces a commit of the offsets gathered above
consumer.onPartitionsRevoked(List.of(topicPartition));

verify(commitFailureCounter).increment();
}

@Test
public void testResetOffsets_whenSeekFails_thenIncrementsOffsetResetFailureCounter() throws Exception {
final Counter offsetResetFailureCounter = mock(Counter.class);
when(topicMetrics.getNumberOfOffsetResetFailures()).thenReturn(offsetResetFailureCounter);

final String topic = topicConfig.getName();
when(topicConfig.getCommitInterval()).thenReturn(Duration.ofMillis(0));
consumerRecords = createPlainTextRecords(topic, 0L);
when(kafkaConsumer.poll(any(Duration.class))).thenReturn(consumerRecords);

consumer = createObjectUnderTest("plaintext", true);
consumer.onPartitionsAssigned(List.of(new TopicPartition(topic, testPartition)));
consumer.consumeRecords();

final Map.Entry<Collection<Record<Event>>, CheckpointState> bufferRecords = buffer.read(1000);
for (final Record<Event> record : new ArrayList<>(bufferRecords.getKey())) {
record.getData().getEventHandle().release(false);
}
// Negative acknowledgement adds the partition to the set that resetOffsets() seeks
await().atMost(delayTime.plusMillis(5000))
.until(() -> consumer.getTopicMetrics().getNumberOfNegativeAcknowledgements().count() == 1.0);

doThrow(new RuntimeException("Failed to look up committed offset"))
.when(kafkaConsumer).committed(any(TopicPartition.class));

final java.lang.reflect.Method method = consumer.getClass().getDeclaredMethod("resetOffsets");
method.setAccessible(true);
method.invoke(consumer);

verify(offsetResetFailureCounter).increment();
}

@Test
public void testConsumeRecords_whenBufferWriteFails_thenIncrementsBufferWriteFailureCounter() throws Exception {
final Counter bufferWriteFailureCounter = mock(Counter.class);
when(topicMetrics.getNumberOfBufferWriteFailures()).thenReturn(bufferWriteFailureCounter);

when(topicConfig.getMaxPollInterval()).thenReturn(Duration.ofMillis(4000));
final String topic = topicConfig.getName();
consumerRecords = createPlainTextRecords(topic, 0L);
doAnswer((i) -> {
if (!paused && !resumed) {
throw new TimeoutException();
}
buffer.writeAll(i.getArgument(0), i.getArgument(1));
return null;
}).when(mockBuffer).writeAll(any(), anyInt());
doAnswer((i) -> {
if (paused && !resumed) {
return List.of();
}
return consumerRecords;
}).when(kafkaConsumer).poll(any(Duration.class));

consumer = createObjectUnderTestWithMockBuffer("plaintext");
try {
consumer.onPartitionsAssigned(List.of(new TopicPartition(topic, testPartition)));
consumer.consumeRecords();
} catch (Exception e) {}

verify(bufferWriteFailureCounter, atLeastOnce()).increment();
// A write timeout is not a size overflow, so the overflow counter must stay untouched
assertEquals(0.0, overflowCount);
}

private static Stream<Arguments> provideExceptionsFromBufferWrite() {
return Stream.of(
Arguments.of(new SizeOverflowException("size overflow")),
Arguments.of(new TimeoutException()));
}

private static Stream<Arguments> provideExceptionsFromCommit() {
return Stream.of(
Arguments.of(new RebalanceInProgressException("Rebalance in progress")),
Arguments.of(new RuntimeException("Generic commit failure")));
}
}


Loading