-
Notifications
You must be signed in to change notification settings - Fork 15.4k
KAFKA-8421: Still return data during rebalance #7312
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 3 commits
37b5c63
4a34116
d964cea
f5b8f96
b07bac2
29f8942
3000d6f
8464fa7
f804b3d
3c62029
284ef77
9825063
929048a
58adc09
c79a93d
75c4e88
95ef3fb
3b844ff
781514d
8b08a3e
8fa5d74
d75fcb9
b176e4e
7a826f9
31e372e
366739b
faf787e
c71af82
ffe6146
518049c
30d5e53
fa3456d
0220d96
27d013a
94611cc
b529847
33fd785
1df9808
3057405
21f531d
effb29d
6fbd976
f4e7111
91b6606
0ad52ec
27f76b7
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 |
|---|---|---|
|
|
@@ -79,6 +79,7 @@ | |
| import java.util.concurrent.atomic.AtomicLong; | ||
| import java.util.concurrent.atomic.AtomicReference; | ||
| import java.util.regex.Pattern; | ||
| import java.util.stream.Collectors; | ||
|
|
||
| /** | ||
| * A client that consumes records from a Kafka cluster. | ||
|
|
@@ -1210,9 +1211,11 @@ private ConsumerRecords<K, V> poll(final Timer timer, final boolean includeMetad | |
| client.maybeTriggerWakeup(); | ||
|
|
||
| if (includeMetadataInTimeout) { | ||
| if (!updateAssignmentMetadataIfNeeded(timer)) { | ||
| return ConsumerRecords.empty(); | ||
| } | ||
| // try to update assignment metadata BUT do not need to block on the timer, | ||
| // since even if we are 1) in the middle of a rebalance or 2) have partitions | ||
| // with unknown starting positions we may still want to return some data | ||
| // as long as there are some partitions fetchable | ||
| updateAssignmentMetadataIfNeeded(time.timer(0L)); | ||
| } else { | ||
| while (!updateAssignmentMetadataIfNeeded(time.timer(Long.MAX_VALUE))) { | ||
| log.warn("Still waiting for metadata"); | ||
|
|
@@ -1231,7 +1234,13 @@ private ConsumerRecords<K, V> poll(final Timer timer, final boolean includeMetad | |
| client.pollNoWakeup(); | ||
| } | ||
|
|
||
| return this.interceptors.onConsume(new ConsumerRecords<>(records)); | ||
| // after the long poll, we should filter the returned data if their belonging | ||
| // partitions are no longer owned by the consumer | ||
| final Set<TopicPartition> assignedPartitions = subscriptions.assignedPartitions(); | ||
| return this.interceptors.onConsume(new ConsumerRecords<>(records.entrySet() | ||
| .stream() | ||
| .filter(entry -> assignedPartitions.contains(entry.getKey())) | ||
| .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)))); | ||
| } | ||
| } while (timer.notExpired()); | ||
|
|
||
|
|
@@ -1285,12 +1294,6 @@ private Map<TopicPartition, List<ConsumerRecord<K, V>>> pollForFetches(Timer tim | |
| }); | ||
| timer.update(pollTimer.currentTimeMs()); | ||
|
|
||
| // after the long poll, we should check whether the group needs to rebalance | ||
| // prior to returning data so that the group can stabilize faster | ||
| if (coordinator != null && coordinator.rejoinNeededOrPending()) { | ||
| return Collections.emptyMap(); | ||
| } | ||
|
|
||
|
Contributor
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. Why is this dropped? Are you thinking it's just redundant?
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. Isn't this the "main point" of this PR -- we don't want to return nothing just because a rebalance is needed or pending
Contributor
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, yeah... that makes sense. |
||
| return fetcher.fetchedRecords(); | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -222,21 +222,45 @@ public synchronized boolean assignFromUser(Set<TopicPartition> partitions) { | |
| if (this.assignment.partitionSet().equals(partitions)) | ||
| return false; | ||
|
|
||
| Map<TopicPartition, TopicPartitionState> assignedPartitionStates = partitionToStateMap(partitions); | ||
|
Contributor
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: should declare final as well?
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. Ack. |
||
|
|
||
| assignmentId++; | ||
|
|
||
| // update the subscribed topics | ||
| Set<String> manualSubscribedTopics = new HashSet<>(); | ||
| Map<TopicPartition, TopicPartitionState> partitionToState = new HashMap<>(); | ||
| for (TopicPartition partition : partitions) { | ||
| TopicPartitionState state = assignment.stateValue(partition); | ||
| if (state == null) | ||
| state = new TopicPartitionState(); | ||
| partitionToState.put(partition, state); | ||
| manualSubscribedTopics.add(partition.topic()); | ||
| } | ||
| this.assignment.set(partitionToState); | ||
|
|
||
| this.assignment.set(assignedPartitionStates); | ||
| return changeSubscription(manualSubscribedTopics); | ||
| } | ||
|
|
||
| /** | ||
| * Change the assignment to the specified partitions returned from the coordinator, note this is | ||
| * different from {@link #assignFromUser(Set)} which directly set the assignment from user inputs. | ||
| */ | ||
| public synchronized void assignFromSubscribed(Collection<TopicPartition> assignments) { | ||
| if (!this.partitionsAutoAssigned()) | ||
| throw new IllegalArgumentException("Attempt to dynamically assign partitions while manual assignment in use"); | ||
|
|
||
|
|
||
|
guozhangwang marked this conversation as resolved.
Outdated
|
||
| Map<TopicPartition, TopicPartitionState> assignedPartitionStates = partitionToStateMap(assignments); | ||
|
Contributor
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: should this be declared final?
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. Ack. |
||
| assignmentId++; | ||
| this.assignment.set(assignedPartitionStates); | ||
| } | ||
|
|
||
| private Map<TopicPartition, TopicPartitionState> partitionToStateMap(Collection<TopicPartition> assignments) { | ||
| Map<TopicPartition, TopicPartitionState> map = new HashMap<>(assignments.size()); | ||
|
guozhangwang marked this conversation as resolved.
Outdated
|
||
| for (TopicPartition tp : assignments) { | ||
| if (assignment.contains(tp)) | ||
| map.put(tp, assignment.stateValue(tp)); | ||
| else | ||
| map.put(tp, new TopicPartitionState()); | ||
| } | ||
| return map; | ||
| } | ||
|
|
||
| /** | ||
| * @return true if assignments matches subscription, otherwise false | ||
| */ | ||
|
|
@@ -262,20 +286,6 @@ public synchronized boolean checkAssignmentMatchedSubscription(Collection<TopicP | |
| return true; | ||
| } | ||
|
|
||
| /** | ||
| * Change the assignment to the specified partitions returned from the coordinator, note this is | ||
| * different from {@link #assignFromUser(Set)} which directly set the assignment from user inputs. | ||
| */ | ||
| public synchronized void assignFromSubscribed(Collection<TopicPartition> assignments) { | ||
| if (!this.partitionsAutoAssigned()) | ||
| throw new IllegalArgumentException("Attempt to dynamically assign partitions while manual assignment in use"); | ||
|
|
||
|
|
||
| Map<TopicPartition, TopicPartitionState> assignedPartitionStates = partitionToStateMap(assignments); | ||
| assignmentId++; | ||
| this.assignment.set(assignedPartitionStates); | ||
| } | ||
|
|
||
| private void registerRebalanceListener(ConsumerRebalanceListener listener) { | ||
| if (listener == null) | ||
| throw new IllegalArgumentException("RebalanceListener cannot be null"); | ||
|
|
@@ -669,13 +679,6 @@ public synchronized ConsumerRebalanceListener rebalanceListener() { | |
| return rebalanceListener; | ||
| } | ||
|
|
||
| private static Map<TopicPartition, TopicPartitionState> partitionToStateMap(Collection<TopicPartition> assignments) { | ||
| Map<TopicPartition, TopicPartitionState> map = new HashMap<>(assignments.size()); | ||
| for (TopicPartition tp : assignments) | ||
| map.put(tp, new TopicPartitionState()); | ||
| return map; | ||
| } | ||
|
|
||
| private static class TopicPartitionState { | ||
|
|
||
| private FetchState fetchState; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1685,6 +1685,108 @@ public void testRebalanceException() { | |
| assertTrue(subscription.assignedPartitions().isEmpty()); | ||
| } | ||
|
|
||
| @Test | ||
| public void testReturnRecordsDuringRebalance() { | ||
| Time time = new MockTime(); | ||
| SubscriptionState subscription = new SubscriptionState(new LogContext(), OffsetResetStrategy.EARLIEST); | ||
| ConsumerMetadata metadata = createMetadata(subscription); | ||
| MockClient client = new MockClient(time, metadata); | ||
| ConsumerPartitionAssignor assignor = new CooperativeStickyAssignor(); | ||
| KafkaConsumer<String, String> consumer = newConsumer(time, client, subscription, metadata, assignor, true, groupInstanceId); | ||
|
|
||
| initMetadata(client, Utils.mkMap(Utils.mkEntry(topic, 1), Utils.mkEntry(topic2, 1), Utils.mkEntry(topic3, 1))); | ||
|
|
||
| consumer.subscribe(Arrays.asList(topic, topic2), getConsumerRebalanceListener(consumer)); | ||
| Node node = metadata.fetch().nodes().get(0); | ||
| Node coordinator = prepareRebalance(client, node, assignor, Arrays.asList(tp0, t2p0), null); | ||
|
|
||
| // a first rebalance to get the assignment | ||
| consumer.updateAssignmentMetadataIfNeeded(time.timer(Long.MAX_VALUE)); | ||
| consumer.poll(Duration.ZERO); | ||
|
|
||
| assertEquals(Utils.mkSet(topic, topic2), consumer.subscription()); | ||
| assertEquals(Utils.mkSet(tp0, t2p0), consumer.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. Do you really need this checks to verify your code?
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. I want to make sure my edits on |
||
|
|
||
| // prepare a response of the outstanding fetch so that we have data available on the next poll | ||
| Map<TopicPartition, FetchInfo> fetches1 = new HashMap<>(); | ||
| fetches1.put(tp0, new FetchInfo(0, 1)); | ||
| fetches1.put(t2p0, new FetchInfo(0, 10)); | ||
| client.respondFrom(fetchResponse(fetches1), node); | ||
|
|
||
| ConsumerRecords<String, String> records = consumer.poll(Duration.ofMillis(1)); | ||
|
|
||
| // prepare the next response of the prefetch | ||
| fetches1.clear(); | ||
| fetches1.put(tp0, new FetchInfo(1, 1)); | ||
| fetches1.put(t2p0, new FetchInfo(10, 20)); | ||
| client.respondFrom(fetchResponse(fetches1), node); | ||
|
|
||
| // verify that the fetch occurred as expected | ||
| assertEquals(11, records.count()); | ||
| assertEquals(1L, consumer.position(tp0)); | ||
| assertEquals(10L, consumer.position(t2p0)); | ||
|
|
||
| // subscription change | ||
| consumer.subscribe(Arrays.asList(topic, topic3), getConsumerRebalanceListener(consumer)); | ||
|
|
||
| // verify that subscription has changed but assignment is still unchanged | ||
| assertEquals(Utils.mkSet(topic, topic3), consumer.subscription()); | ||
| assertEquals(Utils.mkSet(tp0, t2p0), consumer.assignment()); | ||
|
|
||
| // mock the offset commit response for to be revoked partitions | ||
| Map<TopicPartition, Long> partitionOffsets1 = new HashMap<>(); | ||
| partitionOffsets1.put(t2p0, 10L); | ||
| AtomicBoolean commitReceived = prepareOffsetCommitResponse(client, coordinator, partitionOffsets1); | ||
|
|
||
| // poll once which would not complete the rebalance | ||
| records = consumer.poll(Duration.ofMillis(1)); | ||
|
|
||
| // clear out the prefetch so it doesn't interfere with the rest of the test | ||
| fetches1.clear(); | ||
| fetches1.put(tp0, new FetchInfo(2, 1)); | ||
| client.respondFrom(fetchResponse(fetches1), node); | ||
|
|
||
| // verify that the fetch still occurred as expected | ||
| assertEquals(Utils.mkSet(topic, topic3), consumer.subscription()); | ||
| assertEquals(Collections.singleton(tp0), consumer.assignment()); | ||
| assertEquals(1, records.count()); | ||
| assertEquals(2L, consumer.position(tp0)); | ||
|
|
||
| // verify that the offset commits occurred as expected | ||
| assertTrue(commitReceived.get()); | ||
|
|
||
| // mock rebalance responses | ||
| client.respondFrom(joinGroupFollowerResponse(assignor, 2, "memberId", "leaderId", Errors.NONE), coordinator); | ||
| client.prepareResponseFrom(syncGroupResponse(Arrays.asList(tp0, t3p0), Errors.NONE), coordinator); | ||
|
|
||
| consumer.updateAssignmentMetadataIfNeeded(time.timer(Long.MAX_VALUE)); | ||
| records = consumer.poll(Duration.ZERO); | ||
|
|
||
| assertEquals(Utils.mkSet(topic, topic3), consumer.subscription()); | ||
| assertEquals(Utils.mkSet(tp0, t3p0), consumer.assignment()); | ||
| assertEquals(1, records.count()); | ||
| assertEquals(3L, consumer.position(tp0)); | ||
|
|
||
| // mock a response to the third fetch from the new assignment | ||
| fetches1.clear(); | ||
| fetches1.put(tp0, new FetchInfo(3, 1)); | ||
| fetches1.put(t3p0, new FetchInfo(0, 100)); | ||
| client.respondFrom(fetchResponse(fetches1), node); | ||
|
|
||
| records = consumer.poll(Duration.ofMillis(1)); | ||
|
|
||
| // verify that subscription is still the same, and now assignment has caught up | ||
| assertEquals(Utils.mkSet(topic, topic3), consumer.subscription()); | ||
| assertEquals(Utils.mkSet(tp0, t3p0), consumer.assignment()); | ||
| assertEquals(101, records.count()); | ||
| assertEquals(4L, consumer.position(tp0)); | ||
| assertEquals(100L, consumer.position(t3p0)); | ||
|
|
||
| client.requests().clear(); | ||
| consumer.unsubscribe(); | ||
| consumer.close(); | ||
| } | ||
|
|
||
| private KafkaConsumer<String, String> consumerWithPendingAuthenticationError() { | ||
| Time time = new MockTime(); | ||
| SubscriptionState subscription = new SubscriptionState(new LogContext(), OffsetResetStrategy.EARLIEST); | ||
|
|
||
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.
A meta-comment here.
I think that filtering the records under most conditions would be redundant since the assignment usually does not change. Since the assignment remains static unless a rebalance occurs, I think that we only need to filter records when we have found that the assignment has changed like after a rebalance is finished. In the case where the assignment does not change, this segment of the code has no effect since the assignment has remained the same. It probably would act as a substantial performance hit since this portion of the code is often heavily congested.
We probably should have some sort of check that only filters the records when the assignment has changed.
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.
That's a fair point. Originally I thought that the returned records are in the form of
Map<TopicPartition, List<ConsumerRecord<K, V>>>and we are filtering per topic-partition not per-record, so it may be okay; but if there's a better way to avoid unnecessarystream()call we should do it.One thing I can think of is to leverage on
Fetcher#clearBufferedDataForUnassignedPartitionsand call that upon partition assignment changes. But since the background thread can also handle the fetch response it means that concurrent access oncompletedFetchesis possible, and itsiterator()is only weakly consistent: if the background thread is adding new batches to that list while the caller thread is iterating / removing from that list, it may not get removed.Seems that with the locking on
ConsumerNetworkClient#pollthe above should never happen (cc @hachikuji to confirm), in which case I think the above idea should work.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, I think this check is not needed, since in
fetcher#fetchRecordswe already do this filtering:And since the assignment would ONLY be updated at the caller thread, and never at the background thread, we are safe to use this field to filter the fetched records which are only returned to the caller thread and hence ordering is guaranteed.
Uh 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.
Ah, I see.
Edit: Actually, upon closer inspection, it appears that I had got the order mixed up. Looks like all things are accounted for. That was my mistake.