-
Notifications
You must be signed in to change notification settings - Fork 15.4k
KAFKA-14639: A single partition may be revoked and assign during a single round of rebalance #13550
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 9 commits
76f0a15
93ad6f4
08c4e94
23ae4e8
9610543
8ec312d
dbd0359
29bf846
6248852
3ac56a9
2c468b1
ee10026
152c102
319886f
946a31e
1d3deb3
5ed02a7
5c93207
897321f
9cfefad
767c04d
cf3d778
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 |
|---|---|---|
|
|
@@ -16,6 +16,13 @@ | |
| */ | ||
| package org.apache.kafka.clients.consumer.internals; | ||
|
|
||
| import org.apache.kafka.clients.consumer.internals.Utils.PartitionComparator; | ||
|
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. this is "optimized" by the IDE |
||
| import org.apache.kafka.common.Node; | ||
| import org.apache.kafka.common.PartitionInfo; | ||
| import org.apache.kafka.common.TopicPartition; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| import java.io.Serializable; | ||
| import java.util.ArrayList; | ||
| import java.util.Arrays; | ||
|
|
@@ -33,12 +40,6 @@ | |
| import java.util.Set; | ||
| import java.util.TreeSet; | ||
| import java.util.stream.Collectors; | ||
| import org.apache.kafka.clients.consumer.internals.Utils.PartitionComparator; | ||
| import org.apache.kafka.common.Node; | ||
| import org.apache.kafka.common.PartitionInfo; | ||
| import org.apache.kafka.common.TopicPartition; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| /** | ||
| * Sticky assignment implementation used by {@link org.apache.kafka.clients.consumer.StickyAssignor} and | ||
|
|
@@ -127,7 +128,6 @@ private boolean allSubscriptionsEqual(Set<String> allTopics, | |
| Map<String, Subscription> subscriptions, | ||
| Map<String, List<TopicPartition>> consumerToOwnedPartitions, | ||
| Set<TopicPartition> partitionsWithMultiplePreviousOwners) { | ||
| Set<String> membersOfCurrentHighestGeneration = new HashSet<>(); | ||
| boolean isAllSubscriptionsEqual = true; | ||
|
|
||
| Set<String> subscribedTopics = new HashSet<>(); | ||
|
|
@@ -136,8 +136,13 @@ private boolean allSubscriptionsEqual(Set<String> allTopics, | |
| // detected, eg two consumers somehow claiming the same partition in the same/current generation | ||
| Map<TopicPartition, String> allPreviousPartitionsToOwner = new HashMap<>(); | ||
|
|
||
| maxGeneration = subscriptions.values().stream() | ||
| .map(v -> v.generationId().orElse(DEFAULT_GENERATION)) | ||
| .max(Integer::compareTo) | ||
| .orElse(DEFAULT_GENERATION); | ||
|
|
||
| for (Map.Entry<String, Subscription> subscriptionEntry : subscriptions.entrySet()) { | ||
| String consumer = subscriptionEntry.getKey(); | ||
| final String consumer = subscriptionEntry.getKey(); | ||
| Subscription subscription = subscriptionEntry.getValue(); | ||
|
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. nit: While here, should this one be final as well? |
||
|
|
||
| // initialize the subscribed topics set if this is the first subscription | ||
|
|
@@ -153,43 +158,45 @@ private boolean allSubscriptionsEqual(Set<String> allTopics, | |
| List<TopicPartition> ownedPartitions = new ArrayList<>(); | ||
| consumerToOwnedPartitions.put(consumer, ownedPartitions); | ||
|
|
||
| // Only consider this consumer's owned partitions as valid if it is a member of the current highest | ||
| // generation, or it's generation is not present but we have not seen any known generation so far | ||
| if (memberData.generation.isPresent() && memberData.generation.get() >= maxGeneration | ||
| if (memberData.generation.isPresent() && memberData.generation.get() >= maxGeneration - 1 | ||
| || !memberData.generation.isPresent() && maxGeneration == DEFAULT_GENERATION) { | ||
|
|
||
| // If the current member's generation is higher, all the previously owned partitions are invalid | ||
| if (memberData.generation.isPresent() && memberData.generation.get() > maxGeneration) { | ||
|
philipnee marked this conversation as resolved.
|
||
| allPreviousPartitionsToOwner.clear(); | ||
| partitionsWithMultiplePreviousOwners.clear(); | ||
| for (String droppedOutConsumer : membersOfCurrentHighestGeneration) { | ||
| consumerToOwnedPartitions.get(droppedOutConsumer).clear(); | ||
| } | ||
|
|
||
| membersOfCurrentHighestGeneration.clear(); | ||
| maxGeneration = memberData.generation.get(); | ||
| } | ||
|
|
||
| membersOfCurrentHighestGeneration.add(consumer); | ||
| for (final TopicPartition tp : memberData.partitions) { | ||
| // filter out any topics that no longer exist or aren't part of the current subscription | ||
| if (allTopics.contains(tp.topic())) { | ||
| String otherConsumer = allPreviousPartitionsToOwner.put(tp, consumer); | ||
| if (otherConsumer == null) { | ||
| // this partition is not owned by other consumer in the same generation | ||
| ownedPartitions.add(tp); | ||
| } else { | ||
| log.error("Found multiple consumers {} and {} claiming the same TopicPartition {} in the " | ||
| + "same generation {}, this will be invalidated and removed from their previous assignment.", | ||
| consumer, otherConsumer, tp, maxGeneration); | ||
| consumerToOwnedPartitions.get(otherConsumer).remove(tp); | ||
| partitionsWithMultiplePreviousOwners.add(tp); | ||
| int memberGeneration = memberData.generation.orElse(DEFAULT_GENERATION); | ||
| int otherMemberGeneration = subscriptions.get(otherConsumer).generationId().orElse(DEFAULT_GENERATION); | ||
|
|
||
| if (memberGeneration == otherMemberGeneration) { | ||
| if (subscriptions.get(otherConsumer).generationId().orElse(DEFAULT_GENERATION) == memberData.generation.orElse(DEFAULT_GENERATION)) { | ||
| log.error("Found multiple consumers {} and {} claiming the same TopicPartition {} in the " | ||
| + "same generation {}, this will be invalidated and removed from their previous assignment.", | ||
| consumer, otherConsumer, tp, maxGeneration); | ||
| partitionsWithMultiplePreviousOwners.add(tp); | ||
| } | ||
| consumerToOwnedPartitions.get(otherConsumer).remove(tp); | ||
| allPreviousPartitionsToOwner.put(tp, consumer); | ||
| continue; | ||
| } | ||
|
|
||
| if (memberGeneration > otherMemberGeneration) { | ||
| log.warn("Found multiple consumers {} and {} claiming the same TopicPartition {} in " + | ||
|
philipnee marked this conversation as resolved.
Outdated
|
||
| "different generations. The topic partition wil be assigned to the member with higher generation.", | ||
| consumer, otherConsumer, tp); | ||
| // move partition from the member with an older generation to the member with the newer generation | ||
| consumerToOwnedPartitions.get(consumer).add(tp); | ||
| consumerToOwnedPartitions.get(otherConsumer).remove(tp); | ||
| allPreviousPartitionsToOwner.put(tp, consumer); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return isAllSubscriptionsEqual; | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,18 +16,6 @@ | |
| */ | ||
| package org.apache.kafka.clients.consumer.internals; | ||
|
|
||
| import java.nio.ByteBuffer; | ||
| import java.util.ArrayList; | ||
| import java.util.Arrays; | ||
| import java.util.Collections; | ||
| import java.util.HashMap; | ||
| import java.util.HashSet; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Optional; | ||
| import java.util.Random; | ||
| import java.util.Set; | ||
|
|
||
| import org.apache.kafka.clients.consumer.ConsumerPartitionAssignor.Subscription; | ||
| import org.apache.kafka.clients.consumer.StickyAssignor; | ||
| import org.apache.kafka.clients.consumer.internals.AbstractPartitionAssignorTest.RackConfig; | ||
|
|
@@ -42,10 +30,22 @@ | |
| import org.junit.jupiter.params.provider.EnumSource; | ||
| import org.junit.jupiter.params.provider.ValueSource; | ||
|
|
||
| import java.nio.ByteBuffer; | ||
|
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. nit: Could we revert this as it is not related to the fix?
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. oh yes, will do it.... thanks for my IDE's import optimization. |
||
| import java.util.ArrayList; | ||
| import java.util.Arrays; | ||
| import java.util.Collections; | ||
| import java.util.HashMap; | ||
| import java.util.HashSet; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Optional; | ||
| import java.util.Random; | ||
| import java.util.Set; | ||
|
|
||
| import static java.util.Arrays.asList; | ||
| import static java.util.Collections.emptyList; | ||
| import static org.apache.kafka.clients.consumer.internals.AbstractPartitionAssignorTest.TEST_NAME_WITH_RACK_CONFIG; | ||
| import static org.apache.kafka.clients.consumer.internals.AbstractPartitionAssignorTest.TEST_NAME_WITH_CONSUMER_RACK; | ||
| import static org.apache.kafka.clients.consumer.internals.AbstractPartitionAssignorTest.TEST_NAME_WITH_RACK_CONFIG; | ||
| import static org.apache.kafka.clients.consumer.internals.AbstractPartitionAssignorTest.nullRacks; | ||
| import static org.apache.kafka.clients.consumer.internals.AbstractPartitionAssignorTest.racks; | ||
| import static org.apache.kafka.clients.consumer.internals.AbstractPartitionAssignorTest.verifyRackAssignment; | ||
|
|
@@ -1038,6 +1038,63 @@ public void testPartitionsTransferringOwnershipIncludeThePartitionClaimedByMulti | |
| assertTrue(isFullyBalanced(assignment)); | ||
| } | ||
|
|
||
| @ParameterizedTest(name = TEST_NAME_WITH_RACK_CONFIG) | ||
| @EnumSource(RackConfig.class) | ||
| public void testOwnedPartitionsAreStableForConsumerWithMultipleGeneration(RackConfig rackConfig) { | ||
|
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. nit: It seems that we also test the case where a partition is claimed by two consumers with different generation in this test. Should we try to reflect this in its name?
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 actually wonder if this was expected or if it is just a typo in the test case because there is also
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. Ok I renamed the test to |
||
| initializeRacks(rackConfig); | ||
| Map<String, List<PartitionInfo>> partitionsPerTopic = new HashMap<>(); | ||
| partitionsPerTopic.put(topic, partitionInfos(topic, 3)); | ||
| partitionsPerTopic.put(topic2, partitionInfos(topic2, 3)); | ||
| partitionsPerTopic.put(topic3, partitionInfos(topic3, 3)); | ||
|
|
||
| int currentGeneration = 10; | ||
|
|
||
| subscriptions.put(consumer1, buildSubscriptionV2Above(topics(topic, topic2, topic3), | ||
| partitions(tp(topic, 0), tp(topic2, 0), tp(topic3, 0)), currentGeneration, 0)); | ||
| subscriptions.put(consumer2, buildSubscriptionV2Above(topics(topic, topic2, topic3), | ||
| partitions(tp(topic, 1), tp(topic2, 1), tp(topic3, 1)), currentGeneration - 1, 1)); | ||
| subscriptions.put(consumer3, buildSubscriptionV2Above(topics(topic, topic2, topic3), | ||
| partitions(tp(topic, 2), tp(topic2, 2), tp(topic3, 2)), currentGeneration - 2, 1)); | ||
|
|
||
| Map<String, List<TopicPartition>> assignment = assignor.assignPartitions(partitionsPerTopic, subscriptions); | ||
| assertEquals(new HashSet<>(partitions(tp(topic, 0), tp(topic2, 0), tp(topic3, 0))), | ||
| new HashSet<>(assignment.get(consumer1))); | ||
| assertEquals(new HashSet<>(partitions(tp(topic, 1), tp(topic2, 1), tp(topic3, 1))), | ||
| new HashSet<>(assignment.get(consumer2))); | ||
| assertEquals(new HashSet<>(partitions(tp(topic, 2), tp(topic2, 2), tp(topic3, 2))), | ||
| new HashSet<>(assignment.get(consumer3))); | ||
| assertTrue(assignor.partitionsTransferringOwnership.isEmpty()); | ||
|
|
||
| verifyValidityAndBalance(subscriptions, assignment, partitionsPerTopic); | ||
| assertTrue(isFullyBalanced(assignment)); | ||
| } | ||
|
|
||
| @ParameterizedTest(name = TEST_NAME_WITH_RACK_CONFIG) | ||
| @EnumSource(RackConfig.class) | ||
| public void testOwnedPartitionsAreInvalidatedForConsumerWithMultipleGeneration(RackConfig rackConfig) { | ||
| initializeRacks(rackConfig); | ||
| Map<String, List<PartitionInfo>> partitionsPerTopic = new HashMap<>(); | ||
| partitionsPerTopic.put(topic, partitionInfos(topic, 3)); | ||
| partitionsPerTopic.put(topic2, partitionInfos(topic2, 3)); | ||
|
|
||
| int currentGeneration = 10; | ||
|
|
||
| subscriptions.put(consumer1, buildSubscriptionV2Above(topics(topic, topic2), | ||
| partitions(tp(topic, 0), tp(topic2, 1), tp(topic, 1)), currentGeneration, 0)); | ||
| subscriptions.put(consumer2, buildSubscriptionV2Above(topics(topic, topic2), | ||
| partitions(tp(topic, 0), tp(topic2, 1), tp(topic2, 2)), currentGeneration - 2, 1)); | ||
|
|
||
| Map<String, List<TopicPartition>> assignment = assignor.assignPartitions(partitionsPerTopic, subscriptions); | ||
| assertEquals(new HashSet<>(partitions(tp(topic, 0), tp(topic2, 1), tp(topic, 1))), | ||
| new HashSet<>(assignment.get(consumer1))); | ||
| assertEquals(new HashSet<>(partitions(tp(topic, 2), tp(topic2, 2), tp(topic2, 0))), | ||
| new HashSet<>(assignment.get(consumer2))); | ||
| assertTrue(assignor.partitionsTransferringOwnership.isEmpty()); | ||
|
|
||
| verifyValidityAndBalance(subscriptions, assignment, partitionsPerTopic); | ||
| assertTrue(isFullyBalanced(assignment)); | ||
| } | ||
|
|
||
|
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. Should we also add a test for the case where a partition is claimed by two consumers with the same generation?
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. good idea, I didn't check because i thought there could be an existing test case covering it but let me check
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. ok |
||
| @Test | ||
| public void testRackAwareAssignmentWithUniformSubscription() { | ||
| Map<String, Integer> topics = mkMap(mkEntry("t1", 6), mkEntry("t2", 7), mkEntry("t3", 2)); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.