-
Notifications
You must be signed in to change notification settings - Fork 15.4k
KAFKA-12926: ConsumerGroupCommand's java.lang.NullPointerException at negative offsets while running kafka-consumer-groups.sh #10858
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
b7f857a
154ba07
edaf354
53a84cd
d0a3b8b
0a22095
34bebc6
4e3b237
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -62,6 +62,84 @@ class ConsumerGroupServiceTest { | |
| verify(admin, times(1)).listOffsets(offsetsArgMatcher, any()) | ||
| } | ||
|
|
||
| @Test | ||
| def testAdminRequestsForDescribeNegativeOffsets(): Unit = { | ||
| val args = Array("--bootstrap-server", "localhost:9092", "--group", group, "--describe", "--offsets") | ||
| val groupService = consumerGroupService(args) | ||
|
|
||
| val testTopicPartition0 = new TopicPartition("testTopic1", 0); | ||
| val testTopicPartition1 = new TopicPartition("testTopic1", 1); | ||
| val testTopicPartition2 = new TopicPartition("testTopic1", 2); | ||
| val testTopicPartition3 = new TopicPartition("testTopic2", 0); | ||
| val testTopicPartition4 = new TopicPartition("testTopic2", 1); | ||
| val testTopicPartition5 = new TopicPartition("testTopic2", 2); | ||
|
|
||
| // Some topic's partitions gets valid OffsetAndMetada values, other gets nulls values (negative integers) and others aren't defined | ||
| val commitedOffsets = Map( | ||
| testTopicPartition1 -> new OffsetAndMetadata(100), | ||
| testTopicPartition2 -> null, | ||
| testTopicPartition4 -> new OffsetAndMetadata(100), | ||
| testTopicPartition5 -> null, | ||
| ).asJava | ||
|
|
||
| val resultInfo = new ListOffsetsResult.ListOffsetsResultInfo(100, System.currentTimeMillis, Optional.of(1)) | ||
| val endOffsets = Map( | ||
| testTopicPartition0 -> KafkaFuture.completedFuture(resultInfo), | ||
| testTopicPartition1 -> KafkaFuture.completedFuture(resultInfo), | ||
| testTopicPartition2 -> KafkaFuture.completedFuture(resultInfo), | ||
| testTopicPartition3 -> KafkaFuture.completedFuture(resultInfo), | ||
| testTopicPartition4 -> KafkaFuture.completedFuture(resultInfo), | ||
| testTopicPartition5 -> KafkaFuture.completedFuture(resultInfo), | ||
| ) | ||
| val assignedTopicPartitions = Set(testTopicPartition0, testTopicPartition1, testTopicPartition2) | ||
| val unassignedTopicPartitions = Set(testTopicPartition3, testTopicPartition4, testTopicPartition5) | ||
|
|
||
| val consumerGroupDescription = new ConsumerGroupDescription(group, | ||
| true, | ||
| Collections.singleton(new MemberDescription("member1", Optional.of("instance1"), "client1", "host1", new MemberAssignment(assignedTopicPartitions.asJava))), | ||
| classOf[RangeAssignor].getName, | ||
| ConsumerGroupState.STABLE, | ||
| new Node(1, "localhost", 9092)) | ||
|
|
||
| def offsetsArgMatcher: util.Map[TopicPartition, OffsetSpec] = { | ||
| val expectedOffsetsUnassignedTopics = commitedOffsets.asScala.filter{ case (tp, _) => unassignedTopicPartitions.contains(tp) }.keySet.map(tp => tp -> OffsetSpec.latest).toMap | ||
| val expectedOffsetsAssignedTopics = endOffsets.filter{ case (tp, _) => assignedTopicPartitions.contains(tp) }.keySet.map(tp => tp -> OffsetSpec.latest).toMap | ||
| ArgumentMatchers.argThat[util.Map[TopicPartition, OffsetSpec]] { map => | ||
| (map.keySet.asScala == expectedOffsetsUnassignedTopics.keySet || map.keySet.asScala == expectedOffsetsAssignedTopics.keySet) && map.values.asScala.forall(_.isInstanceOf[OffsetSpec.LatestSpec]) | ||
| } | ||
| } | ||
|
|
||
| when(admin.describeConsumerGroups(ArgumentMatchers.eq(Collections.singletonList(group)), any())) | ||
| .thenReturn(new DescribeConsumerGroupsResult(Collections.singletonMap(group, KafkaFuture.completedFuture(consumerGroupDescription)))) | ||
| when(admin.listConsumerGroupOffsets(ArgumentMatchers.eq(group), any())) | ||
| .thenReturn(AdminClientTestUtils.listConsumerGroupOffsetsResult(commitedOffsets)) | ||
| when(admin.listOffsets(offsetsArgMatcher, any())) | ||
| .thenReturn(new ListOffsetsResult(endOffsets.asJava)) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was not fully satisfied with providing all the Note the Interestingly, the test fails when I use this modified version. I think that this is due to the fact that not all the end offsets are returned all the time now and that What do you think?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Run the test defining all the topic-partitoins with some value but failed too. The stack:
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Going to do some research, but seems like the problem I had before, the second call to when(..).then(..) doesnt get call, so the admin.listOffsets is call directly with no mock results. Going to take a look.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You might need to adjust the expected arguments.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually yes, is because of the fact that not all the end offsets are returned all the time now, but only for unassigned partitions case. Because the ConsumerGroupCommand defines unassigned topic partition as: The definition that I gave to the test is breaking that logic, because if the topic partition is not in the commitedOffsets, by definition shouldn't be on the unassignedPartitions. So the test case should be: Just the testTopicPartition0 (which is assigned) as non defined, because as I seem there is never going to be a case where there is a non defined unassigned partition, because it is a requirement to be defined on commitedOffsets in order to be consider as unasigned.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Once defined that way, test runs OK.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And why the test was passing before, was because of this: The expectedOffsetsUnassigned was a subset of commitedOffsets ( The expectedOffsetsAssignedTopics was a subset of endOffsets ( It was defined that way to preserve the logic of But your suggestion of use endOffsets on both as a filter seems cleaner to me. Just needed to adjust the test case. |
||
|
|
||
| val (state, assignments) = groupService.collectGroupOffsets(group) | ||
| val returnedOffsets = assignments.map { results => | ||
| results.map { assignment => | ||
| new TopicPartition(assignment.topic.get, assignment.partition.get) -> assignment.offset | ||
| }.toMap | ||
| }.getOrElse(Map.empty) | ||
| // Results should have information for all assigned topic partition (even if there is not Offset's information at all, because they get fills with None) | ||
| // Results should have information only for unassigned topic partitions if and only if there is information about them (including with null values) | ||
|
IgnacioAcunaF marked this conversation as resolved.
Outdated
|
||
| val expectedOffsets = Map( | ||
| testTopicPartition0 -> None, | ||
| testTopicPartition1 -> Some(100), | ||
| testTopicPartition2 -> None, | ||
| testTopicPartition4 -> Some(100), | ||
| testTopicPartition5 -> None | ||
| ) | ||
| assertEquals(Some("Stable"), state) | ||
| assertTrue(assignments.nonEmpty) | ||
|
IgnacioAcunaF marked this conversation as resolved.
Outdated
|
||
| assertEquals(expectedOffsets, returnedOffsets) | ||
|
|
||
| verify(admin, times(1)).describeConsumerGroups(ArgumentMatchers.eq(Collections.singletonList(group)), any()) | ||
| verify(admin, times(1)).listConsumerGroupOffsets(ArgumentMatchers.eq(group), any()) | ||
| verify(admin, times(2)).listOffsets(offsetsArgMatcher, any()) | ||
| } | ||
|
|
||
| @Test | ||
| def testAdminRequestsForResetOffsets(): Unit = { | ||
| val args = Seq("--bootstrap-server", "localhost:9092", "--group", group, "--reset-offsets", "--to-latest") | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, it seems that we should always have
nullor anOffsetAndMetadatahere for each partition as the API always provided an answer for the requested partitions.commitedOffsets->committedOffsetsUh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, put that case based on what I saw on that consumer-group:
This was the consumer-group state:
(groupId=order-validations, isSimpleConsumerGroup=false, members=(memberId=order-validations-d5fbca62-ab2b-48d7-96ba-0ae72dff72a6, groupInstanceId=null, clientId=order-validations, host=/127.0.0.1, assignment=(topicPartitions=rtl_orderReceive-0,rtl_orderReceive-1,rtl_orderReceive-2,rtl_orderReceive-3,rtl_orderReceive-4,rtl_orderReceive-5,rtl_orderReceive-6,rtl_orderReceive-7,rtl_orderReceive-8,rtl_orderReceive-9)), partitionAssignor=RoundRobinAssigner, state=Stable, coordinator=f0527.cluster.cl:31047 (id: 1 rack: null), authorizedOperations=[])It has assigned all the partitions to rtl_orderReceive, but when getting the commited offsets:
Map(rtl_orderReceive-0 -> null, rtl_orderReceive-1 -> OffsetAndMetadata{offset=39, leaderEpoch=null, metadata=''}, rtl_orderReceive-2 -> null, rtl_orderReceive-3 -> OffsetAndMetadata{offset=33, leaderEpoch=null, metadata=''}, rtl_orderReceive-4 -> null, rtl_orderReceive-5 -> null, rtl_orderReceive-7 -> null, rtl_orderReceive-8 -> null)Even that partition 6 was assigned, the aren't values commited for it (even not a -1).
That is for the case of assigned partitions, but for unassigned partitions, thinking it now, as it is the subsets of commited offsets that aren't assigned, it makes no sense to have non defined commited unassigned partition (because if there weren't a commited partition, then wouldn't exist the unassigned partition).
So I am thinking on:
Just letting testTopicPartition0 as undefined, but defining testTopicPartition3 (unassigned).
Makes sense?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the clarification. In this case, it might be better to keep committing in both cases in oder to be robust and to adjust the rest of the test accordingly.