|
38 | 38 | import static org.apache.kafka.coordinator.group.GroupCoordinatorConfig.GROUP_MIN_SESSION_TIMEOUT_MS_CONFIG; |
39 | 39 | import static org.apache.kafka.coordinator.group.GroupCoordinatorConfig.OFFSETS_TOPIC_PARTITIONS_CONFIG; |
40 | 40 | import static org.apache.kafka.coordinator.group.GroupCoordinatorConfig.OFFSETS_TOPIC_REPLICATION_FACTOR_CONFIG; |
| 41 | +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; |
41 | 42 | import static org.junit.jupiter.api.Assertions.assertEquals; |
42 | 43 | import static org.junit.jupiter.api.Assertions.assertNotNull; |
43 | 44 | import static org.junit.jupiter.api.Assertions.assertNull; |
@@ -298,6 +299,58 @@ private void testAssignAndRetrievingCommittedOffsetsMultipleTimes(GroupProtocol |
298 | 299 | } |
299 | 300 | } |
300 | 301 |
|
| 302 | + @ClusterTest |
| 303 | + public void testAsyncPollAfterTopicDeleted() throws Exception { |
| 304 | + testPollAfterTopicDeleted(GroupProtocol.CONSUMER); |
| 305 | + } |
| 306 | + |
| 307 | + @ClusterTest |
| 308 | + public void testClassicPollAfterTopicDeleted() throws Exception { |
| 309 | + testPollAfterTopicDeleted(GroupProtocol.CLASSIC); |
| 310 | + } |
| 311 | + |
| 312 | + /** |
| 313 | + * Test that poll() doesn't fail to retrieve committed offsets after the assigned topic is deleted. |
| 314 | + * Validates fix for KAFKA-20165. |
| 315 | + */ |
| 316 | + private void testPollAfterTopicDeleted(GroupProtocol groupProtocol) throws Exception { |
| 317 | + String topicToDelete = "topic-to-delete-assign"; |
| 318 | + clusterInstance.createTopic(topicToDelete, 1, (short) BROKER_COUNT); |
| 319 | + TopicPartition tpToDelete = new TopicPartition(topicToDelete, 0); |
| 320 | + |
| 321 | + int numRecords = 10; |
| 322 | + long startingTimestamp = System.currentTimeMillis(); |
| 323 | + |
| 324 | + Map<String, Object> consumerConfig = Map.of( |
| 325 | + GROUP_PROTOCOL_CONFIG, groupProtocol.name, |
| 326 | + GROUP_ID_CONFIG, "test-group", |
| 327 | + ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); |
| 328 | + |
| 329 | + try (Consumer<byte[], byte[]> consumer = clusterInstance.consumer(consumerConfig); |
| 330 | + var admin = clusterInstance.admin()) { |
| 331 | + |
| 332 | + // Send records and consume them (this caches the topic ID in the consumer) |
| 333 | + ClientsTestUtils.sendRecords(clusterInstance, tpToDelete, numRecords, startingTimestamp); |
| 334 | + consumer.assign(List.of(tpToDelete)); |
| 335 | + consumer.seek(tpToDelete, 0); |
| 336 | + ClientsTestUtils.consumeAndVerifyRecords(consumer, tpToDelete, numRecords, 0, 0, startingTimestamp); |
| 337 | + consumer.commitSync(); |
| 338 | + |
| 339 | + // Delete the topic and wait for deletion to propagate |
| 340 | + admin.deleteTopics(List.of(topicToDelete)).all().get(); |
| 341 | + Thread.sleep(1000); |
| 342 | + |
| 343 | + // Change assignment to force the consumer to fetch committed offsets on next poll() |
| 344 | + // The consumer still has the topic ID cached, so it will use version 10+ |
| 345 | + // and get UNKNOWN_TOPIC_ID from the broker |
| 346 | + consumer.unsubscribe(); |
| 347 | + consumer.assign(List.of(tpToDelete)); |
| 348 | + |
| 349 | + // poll() should not throw - internally fetches committed offsets for deleted topic and recovers |
| 350 | + assertDoesNotThrow(() -> consumer.poll(java.time.Duration.ofMillis(5000))); |
| 351 | + } |
| 352 | + } |
| 353 | + |
301 | 354 | private static class CountConsumerCommitCallback implements OffsetCommitCallback { |
302 | 355 | int successCount = 0; |
303 | 356 | int failCount = 0; |
|
0 commit comments