-
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 6 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 | ||
|
|
@@ -119,15 +120,13 @@ public Map<String, List<TopicPartition>> assign(Map<String, Integer> partitionsP | |
| } | ||
|
|
||
| /** | ||
| * Returns true iff all consumers have an identical subscription. Also fills out the passed in | ||
| * {@code consumerToOwnedPartitions} with each consumer's previously owned and still-subscribed partitions, | ||
| * and the {@code partitionsWithMultiplePreviousOwners} with any partitions claimed by multiple previous owners | ||
| * Returns the mapping of consumer to its owned TopicPartition, and fill the {@code | ||
| * partitionsWithMultiplePreviousOwners} with any partitions claimed by multiple previous owners | ||
| */ | ||
| 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<>(); | ||
|
|
@@ -137,7 +136,18 @@ private boolean allSubscriptionsEqual(Set<String> allTopics, | |
| Map<TopicPartition, String> allPreviousPartitionsToOwner = new HashMap<>(); | ||
|
|
||
| for (Map.Entry<String, Subscription> subscriptionEntry : subscriptions.entrySet()) { | ||
| String consumer = subscriptionEntry.getKey(); | ||
| Subscription subscription = subscriptionEntry.getValue(); | ||
| maxGeneration = Math.max(maxGeneration, | ||
| subscription.generationId().orElse(DEFAULT_GENERATION)); | ||
| } | ||
|
|
||
| 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()) { | ||
| 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,46 +163,59 @@ 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 | ||
| || !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(); | ||
| } | ||
| // if the consumer is earlier than the maxGeneration - 1, its partitions are irrelevant | ||
| if (!hasValidGeneration(maxGeneration - 1, memberData.generation) && | ||
| !hasNoKnownGeneration(maxGeneration, memberData.generation)) { | ||
| continue; | ||
| } | ||
|
|
||
| membersOfCurrentHighestGeneration.clear(); | ||
| maxGeneration = memberData.generation.get(); | ||
| } | ||
| for (final TopicPartition tp : memberData.partitions) { | ||
| 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 { | ||
| int memberGeneration = memberData.generation.orElse(DEFAULT_GENERATION); | ||
| int otherMemberGeneration = subscriptions.get(otherConsumer).generationId().orElse(DEFAULT_GENERATION); | ||
|
|
||
| if (memberGeneration == otherMemberGeneration) { | ||
|
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 put a comment in this branch like we did for the others? |
||
| if (subscriptions.get(otherConsumer).generationId().orElse(DEFAULT_GENERATION) == memberData.generation.orElse(DEFAULT_GENERATION)) { | ||
|
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. Aren't those two if statements the same? |
||
| 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); | ||
|
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. So my understanding is that partitions put in
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. that seems like the case, reference to the snippet here:
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. From KAFKA-12984: |
||
| } | ||
| consumerToOwnedPartitions.get(otherConsumer).remove(tp); | ||
| allPreviousPartitionsToOwner.put(tp, consumer); | ||
| continue; | ||
|
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. This does not look nice. Should we remove it and use
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. It could be. I got into the habit of returning early, I thought it makes it easier to read. |
||
| } | ||
|
|
||
| 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); | ||
| if (memberGeneration > otherMemberGeneration) { | ||
| log.warn("Found multiple consumers {} and {} claiming the same TopicPartition {} in " + | ||
| "different " + | ||
| "generations. The topic partition wil be assigned to the member with higher generation.", | ||
| consumer, otherConsumer, tp); | ||
| consumerToOwnedPartitions.get(consumer).add(tp); | ||
|
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: We could use |
||
| // this partition is owned by other consumer in the same generation | ||
| consumerToOwnedPartitions.get(otherConsumer).remove(tp); | ||
| partitionsWithMultiplePreviousOwners.add(tp); | ||
| allPreviousPartitionsToOwner.put(tp, consumer); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return isAllSubscriptionsEqual; | ||
| } | ||
|
|
||
| private static boolean hasValidGeneration(final int maxGeneration, final Optional<Integer> memberGeneration) { | ||
| return memberGeneration.orElse(Integer.MAX_VALUE) >= maxGeneration; | ||
| } | ||
|
|
||
| private static boolean hasNoKnownGeneration(final int maxGeneration, final Optional<Integer> memberGeneration) { | ||
| return !memberGeneration.isPresent() && maxGeneration == DEFAULT_GENERATION; | ||
| } | ||
|
|
||
| public boolean isSticky() { | ||
| return partitionMovements.isSticky(); | ||
| } | ||
|
|
||
| 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.