Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
37b5c63
first version
guozhangwang Sep 6, 2019
4a34116
add unit tests
guozhangwang Sep 7, 2019
d964cea
Merge branch 'trunk' of https://github.com/apache/kafka into K8421-re…
guozhangwang Sep 10, 2019
f5b8f96
Merge branch 'trunk' of https://github.com/apache/kafka into K8421-re…
guozhangwang Sep 11, 2019
b07bac2
Merge branch 'trunk' of https://github.com/apache/kafka into K8421-re…
guozhangwang Sep 12, 2019
29f8942
fix unit tests
guozhangwang Sep 12, 2019
3000d6f
add RebalanceInProgressException
guozhangwang Sep 13, 2019
8464fa7
rebase from trunk
guozhangwang Sep 17, 2019
f804b3d
refactor unit tests
guozhangwang Sep 17, 2019
3c62029
Merge branch 'trunk' of https://github.com/apache/kafka into K8421-re…
guozhangwang Sep 17, 2019
284ef77
poll with 1ms than 0ms for discover coordinator
guozhangwang Sep 18, 2019
9825063
refactor the newly added test with auto-tick 1ms
guozhangwang Sep 18, 2019
929048a
rebase from trunk
guozhangwang Oct 21, 2019
58adc09
Merge branch 'trunk' of https://github.com/apache/kafka into K8421-re…
guozhangwang Oct 22, 2019
c79a93d
fix unit tests
guozhangwang Oct 22, 2019
75c4e88
rebase from trunk again
guozhangwang Oct 22, 2019
95ef3fb
Merge branch 'trunk' of https://github.com/apache/kafka into K8421-re…
guozhangwang Oct 27, 2019
3b844ff
use RetriableCommitFailedException
guozhangwang Oct 27, 2019
781514d
unit tests
guozhangwang Oct 27, 2019
8b08a3e
handle retrialbe commit failed in streams
guozhangwang Oct 27, 2019
8fa5d74
Merge branch 'trunk' of https://github.com/apache/kafka into K8421-re…
guozhangwang Oct 29, 2019
d75fcb9
Merge branch 'trunk' of https://github.com/apache/kafka into K8421-re…
guozhangwang Oct 29, 2019
b176e4e
fix unit tests
guozhangwang Oct 29, 2019
7a826f9
remove imports
guozhangwang Oct 29, 2019
31e372e
fix a real bug exposed by flaky test
guozhangwang Oct 30, 2019
366739b
remove debugging info
guozhangwang Oct 30, 2019
faf787e
minor fix on CommitFailed class
guozhangwang Oct 30, 2019
c71af82
rebase from trunk
guozhangwang Nov 8, 2019
ffe6146
more java doc
guozhangwang Nov 8, 2019
518049c
Merge branch 'trunk' of https://github.com/apache/kafka into K8421-re…
guozhangwang Dec 11, 2019
30d5e53
address comments
guozhangwang Dec 12, 2019
fa3456d
address comments
guozhangwang Dec 24, 2019
0220d96
Merge branch 'trunk' of https://github.com/apache/kafka into K8421-re…
guozhangwang Jan 2, 2020
27d013a
address comment
guozhangwang Jan 2, 2020
94611cc
do not reset if generation has changed
guozhangwang Jan 2, 2020
b529847
remove restrictive check on commit
guozhangwang Jan 3, 2020
33fd785
check assignment
guozhangwang Jan 3, 2020
1df9808
Merge branch 'trunk' of https://github.com/apache/kafka into K8421-re…
guozhangwang Jan 3, 2020
3057405
more comments
guozhangwang Jan 3, 2020
21f531d
remember the fatal exception and throw
guozhangwang Jan 4, 2020
effb29d
Merge branch 'trunk' of https://github.com/apache/kafka into K8421-re…
guozhangwang Jan 6, 2020
6fbd976
comments
guozhangwang Jan 6, 2020
f4e7111
Merge branch 'trunk' of https://github.com/apache/kafka into K8421-re…
guozhangwang Jan 8, 2020
91b6606
move back to RebalanceInProgressException
guozhangwang Jan 8, 2020
0ad52ec
address comments
guozhangwang Jan 9, 2020
27f76b7
rebase from trunk
guozhangwang Jan 9, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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");
Expand All @@ -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()

Copy link
Copy Markdown
Contributor

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.

Copy link
Copy Markdown
Contributor Author

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 unnecessary stream() call we should do it.

One thing I can think of is to leverage on Fetcher#clearBufferedDataForUnassignedPartitions and call that upon partition assignment changes. But since the background thread can also handle the fetch response it means that concurrent access on completedFetches is possible, and its iterator() 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#poll the above should never happen (cc @hachikuji to confirm), in which case I think the above idea should work.

Copy link
Copy Markdown
Contributor Author

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#fetchRecords we already do this filtering:

if (!subscriptions.isAssigned(completedFetch.partition)) {
            // this can happen when a rebalance happened before fetched records are returned to the consumer's poll call
            log.debug("Not returning fetched records for partition {} since it is no longer assigned",
                    completedFetch.partition);
        } else if (!subscriptions.isFetchable(completedFetch.partition)) {
            // this can happen when a partition is paused before fetched records are returned to the consumer's
            // poll call or if the offset is being reset
            log.debug("Not returning fetched records for assigned partition {} since it is no longer fetchable",
                    completedFetch.partition);
        }

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.

@ConcurrencyPractitioner ConcurrencyPractitioner Sep 12, 2019

Copy link
Copy Markdown
Contributor

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.

.stream()
.filter(entry -> assignedPartitions.contains(entry.getKey()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))));
}
} while (timer.notExpired());

Expand Down Expand Up @@ -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();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this dropped? Are you thinking it's just redundant?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, yeah... that makes sense.

return fetcher.fetchedRecords();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: should declare final as well?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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");


Comment thread
guozhangwang marked this conversation as resolved.
Outdated
Map<TopicPartition, TopicPartitionState> assignedPartitionStates = partitionToStateMap(assignments);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: should this be declared final?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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());
Comment thread
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
*/
Expand All @@ -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");
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,6 @@ private long elapsedTimeMs(long currentTimeMs, long startTimeMs) {
return Math.max(0, currentTimeMs - startTimeMs);
}


private void checkTimeoutOfPendingRequests(long nowMs) {
ClientRequest request = requests.peek();
while (request != null && elapsedTimeMs(nowMs, request.createdTimeMs()) > request.requestTimeoutMs()) {
Expand Down Expand Up @@ -330,7 +329,6 @@ public void respond(RequestMatcher matcher, AbstractResponse response) {

// Utility method to enable out of order responses
public void respondToRequest(ClientRequest clientRequest, AbstractResponse response) {
AbstractRequest request = clientRequest.requestBuilder().build();
requests.remove(clientRequest);
short version = clientRequest.requestBuilder().latestAllowedVersion();
responses.add(new ClientResponse(clientRequest.makeHeader(version), clientRequest.callback(), clientRequest.destination(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you really need this checks to verify your code?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to make sure my edits on updateAssignmentMetadataIfNeeded did not change any existing logic.


// 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -866,8 +866,10 @@ public void testFetchDuringRebalance() {
client.prepareResponse(fullFetchResponse(tp0, this.records, Errors.NONE, 100L, 0));
consumerClient.poll(time.timer(0));

// The active fetch should be ignored since its position is no longer valid
assertTrue(fetcher.fetchedRecords().isEmpty());
// The active fetch should NOT be ignored since its position is retained
final Map<TopicPartition, List<ConsumerRecord<Object, Object>>> fetched = fetchedRecords();
assertEquals(1, fetched.size());
assertEquals(3, fetched.get(tp0).size());
}

@Test
Expand Down