You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@kafka.apache.org by gh...@apache.org on 2024/06/03 21:24:05 UTC
(kafka) branch 3.8 updated: KAFKA-16105: Reset read offsets when seeking to beginning in TBRLMM (#15165)
This is an automated email from the ASF dual-hosted git repository.
gharris pushed a commit to branch 3.8
in repository https://gitbox.apache.org/repos/asf/kafka.git
The following commit(s) were added to refs/heads/3.8 by this push:
new cd52f33746d KAFKA-16105: Reset read offsets when seeking to beginning in TBRLMM (#15165)
cd52f33746d is described below
commit cd52f33746de68c497843e4cd94416d5ec7270ee
Author: Anatoly Popov <an...@aiven.io>
AuthorDate: Mon Jun 3 23:46:40 2024 +0300
KAFKA-16105: Reset read offsets when seeking to beginning in TBRLMM (#15165)
Reviewers: Greg Harris <gr...@aiven.io>, Luke Chen <sh...@gmail.com>, Kamal Chandraprakash <ka...@gmail.com>
---
.../log/remote/metadata/storage/ConsumerTask.java | 15 ++++--
.../remote/metadata/storage/ConsumerTaskTest.java | 62 +++++++++++++++++++++-
2 files changed, 71 insertions(+), 6 deletions(-)
diff --git a/storage/src/main/java/org/apache/kafka/server/log/remote/metadata/storage/ConsumerTask.java b/storage/src/main/java/org/apache/kafka/server/log/remote/metadata/storage/ConsumerTask.java
index f36909b66b6..a328b256ee0 100644
--- a/storage/src/main/java/org/apache/kafka/server/log/remote/metadata/storage/ConsumerTask.java
+++ b/storage/src/main/java/org/apache/kafka/server/log/remote/metadata/storage/ConsumerTask.java
@@ -238,10 +238,15 @@ class ConsumerTask implements Runnable, Closeable {
this.assignedMetadataPartitions = Collections.unmodifiableSet(metadataPartitionSnapshot);
// for newly assigned user-partitions, read from the beginning of the corresponding metadata partition
final Set<TopicPartition> seekToBeginOffsetPartitions = assignedUserTopicIdPartitionsSnapshot
- .stream()
- .filter(utp -> !utp.isAssigned)
- .map(utp -> toRemoteLogPartition(utp.metadataPartition))
- .collect(Collectors.toSet());
+ .stream()
+ .filter(utp -> !utp.isAssigned)
+ .map(utp -> utp.metadataPartition)
+ // When reset to beginning is happening, we also need to reset the last read offset
+ // Otherwise if the next reassignment request for the same metadata partition comes in
+ // before the record of already assigned topic has been read, then the reset will happen again to the last read offset
+ .peek(readOffsetsByMetadataPartition::remove)
+ .map(ConsumerTask::toRemoteLogPartition)
+ .collect(Collectors.toSet());
consumer.seekToBeginning(seekToBeginOffsetPartitions);
// for other metadata partitions, read from the offset where the processing left last time.
remoteLogPartitions.stream()
@@ -463,4 +468,4 @@ class ConsumerTask implements Runnable, Closeable {
'}';
}
}
-}
\ No newline at end of file
+}
diff --git a/storage/src/test/java/org/apache/kafka/server/log/remote/metadata/storage/ConsumerTaskTest.java b/storage/src/test/java/org/apache/kafka/server/log/remote/metadata/storage/ConsumerTaskTest.java
index 424c86b6df6..cef1d335b93 100644
--- a/storage/src/test/java/org/apache/kafka/server/log/remote/metadata/storage/ConsumerTaskTest.java
+++ b/storage/src/test/java/org/apache/kafka/server/log/remote/metadata/storage/ConsumerTaskTest.java
@@ -17,6 +17,7 @@
package org.apache.kafka.server.log.remote.metadata.storage;
import org.apache.kafka.clients.consumer.ConsumerRecord;
+import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.MockConsumer;
import org.apache.kafka.clients.consumer.OffsetResetStrategy;
import org.apache.kafka.common.TopicIdPartition;
@@ -64,8 +65,12 @@ import static org.apache.kafka.server.log.remote.metadata.storage.ConsumerTask.t
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.spy;
public class ConsumerTaskTest {
@@ -85,7 +90,7 @@ public class ConsumerTaskTest {
public void beforeEach() {
final Map<TopicPartition, Long> offsets = remoteLogPartitions.stream()
.collect(Collectors.toMap(Function.identity(), e -> 0L));
- consumer = new MockConsumer<>(OffsetResetStrategy.EARLIEST);
+ consumer = spy(new MockConsumer<>(OffsetResetStrategy.EARLIEST));
consumer.updateBeginningOffsets(offsets);
consumerTask = new ConsumerTask(handler, partitioner, consumer, 10L, 300_000L, new SystemTime());
thread = new Thread(consumerTask);
@@ -254,6 +259,61 @@ public class ConsumerTaskTest {
assertEquals(3, handler.metadataCounter);
}
+ @Test
+ public void testCanReprocessSkippedRecords() throws InterruptedException {
+ final Uuid topicId = Uuid.fromString("Bp9TDduJRGa9Q5rlvCJOxg");
+ final TopicIdPartition tpId0 = new TopicIdPartition(topicId, new TopicPartition("sample", 0));
+ final TopicIdPartition tpId1 = new TopicIdPartition(topicId, new TopicPartition("sample", 1));
+ final TopicIdPartition tpId3 = new TopicIdPartition(topicId, new TopicPartition("sample", 3));
+ assertEquals(partitioner.metadataPartition(tpId0), partitioner.metadataPartition(tpId1));
+ assertNotEquals(partitioner.metadataPartition(tpId3), partitioner.metadataPartition(tpId0));
+
+ final int metadataPartition = partitioner.metadataPartition(tpId0);
+ final int anotherMetadataPartition = partitioner.metadataPartition(tpId3);
+
+ // Mocking the consumer to be able to wait for the second reassignment
+ doAnswer(invocation -> {
+ if (consumerTask.isUserPartitionAssigned(tpId1) && !consumerTask.isUserPartitionAssigned(tpId3)) {
+ return ConsumerRecords.empty();
+ } else {
+ return invocation.callRealMethod();
+ }
+ }).when(consumer).poll(any());
+
+ consumer.updateEndOffsets(Collections.singletonMap(toRemoteLogPartition(metadataPartition), 0L));
+ consumer.updateEndOffsets(Collections.singletonMap(toRemoteLogPartition(anotherMetadataPartition), 0L));
+ final Set<TopicIdPartition> assignments = Collections.singleton(tpId0);
+ consumerTask.addAssignmentsForPartitions(assignments);
+ thread.start();
+ TestUtils.waitForCondition(() -> consumerTask.isUserPartitionAssigned(tpId0), "Timed out waiting for " + tpId0 + " to be assigned");
+
+ // Adding metadata records in the order opposite to the order of assignments
+ addRecord(consumer, metadataPartition, tpId1, 0);
+ addRecord(consumer, metadataPartition, tpId0, 1);
+ TestUtils.waitForCondition(() -> consumerTask.readOffsetForMetadataPartition(metadataPartition).equals(Optional.of(1L)), "Couldn't read record");
+ // Only one record is processed, tpId1 record is skipped as unassigned
+ // but read offset is 1 e.g., record for tpId1 has been read by consumer
+ assertEquals(1, handler.metadataCounter);
+
+ // Adding assignment for tpId1 after related metadata records have already been read
+ consumerTask.addAssignmentsForPartitions(Collections.singleton(tpId1));
+ TestUtils.waitForCondition(() -> consumerTask.isUserPartitionAssigned(tpId1), "Timed out waiting for " + tpId1 + " to be assigned");
+
+ // Adding assignment for tpId0 to trigger the reset to last read offset
+ // and assignment for tpId3 that has different metadata partition to trigger the update of metadata snapshot
+ HashSet<TopicIdPartition> partitions = new HashSet<>();
+ partitions.add(tpId0);
+ partitions.add(tpId3);
+ consumerTask.addAssignmentsForPartitions(partitions);
+ // explicitly re-adding the records since MockConsumer drops them on poll.
+ addRecord(consumer, metadataPartition, tpId1, 0);
+ addRecord(consumer, metadataPartition, tpId0, 1);
+ // Waiting for all metadata records to be re-read from the first metadata partition number
+ TestUtils.waitForCondition(() -> consumerTask.readOffsetForMetadataPartition(metadataPartition).equals(Optional.of(1L)), "Couldn't read record");
+ // Verifying that all the metadata records from the first metadata partition were processed properly.
+ TestUtils.waitForCondition(() -> handler.metadataCounter == 2, "Couldn't read record");
+ }
+
@Test
public void testMaybeMarkUserPartitionsAsReady() throws InterruptedException {
final TopicIdPartition tpId = getIdPartitions("hello", 1).get(0);