-
Notifications
You must be signed in to change notification settings - Fork 15.4k
KAKFA-8950: Fix KafkaConsumer Fetcher breaking on concurrent disconnect #7511
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 all commits
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 |
|---|---|---|
|
|
@@ -258,82 +258,86 @@ public synchronized int sendFetches() { | |
| if (log.isDebugEnabled()) { | ||
| log.debug("Sending {} {} to broker {}", isolationLevel, data.toString(), fetchTarget); | ||
| } | ||
| client.send(fetchTarget, request) | ||
| .addListener(new RequestFutureListener<ClientResponse>() { | ||
| @Override | ||
| public void onSuccess(ClientResponse resp) { | ||
| synchronized (Fetcher.this) { | ||
| try { | ||
| @SuppressWarnings("unchecked") | ||
| FetchResponse<Records> response = (FetchResponse<Records>) resp.responseBody(); | ||
| FetchSessionHandler handler = sessionHandler(fetchTarget.id()); | ||
| if (handler == null) { | ||
| log.error("Unable to find FetchSessionHandler for node {}. Ignoring fetch response.", | ||
| fetchTarget.id()); | ||
| return; | ||
| } | ||
| if (!handler.handleResponse(response)) { | ||
| return; | ||
| } | ||
| RequestFuture<ClientResponse> future = client.send(fetchTarget, request); | ||
| // We add the node to the set of nodes with pending fetch requests before adding the | ||
| // listener because the future may have been fulfilled on another thread (e.g. during a | ||
| // disconnection being handled by the heartbeat thread) which will mean the listener | ||
| // will be invoked synchronously. | ||
| this.nodesWithPendingFetchRequests.add(entry.getKey().id()); | ||
| future.addListener(new RequestFutureListener<ClientResponse>() { | ||
| @Override | ||
| public void onSuccess(ClientResponse resp) { | ||
| synchronized (Fetcher.this) { | ||
| try { | ||
| @SuppressWarnings("unchecked") | ||
| FetchResponse<Records> response = (FetchResponse<Records>) resp.responseBody(); | ||
| FetchSessionHandler handler = sessionHandler(fetchTarget.id()); | ||
| if (handler == null) { | ||
| log.error("Unable to find FetchSessionHandler for node {}. Ignoring fetch response.", | ||
| fetchTarget.id()); | ||
| return; | ||
| } | ||
| if (!handler.handleResponse(response)) { | ||
| return; | ||
| } | ||
|
|
||
| Set<TopicPartition> partitions = new HashSet<>(response.responseData().keySet()); | ||
| FetchResponseMetricAggregator metricAggregator = new FetchResponseMetricAggregator(sensors, partitions); | ||
|
|
||
| for (Map.Entry<TopicPartition, FetchResponse.PartitionData<Records>> entry : response.responseData().entrySet()) { | ||
| TopicPartition partition = entry.getKey(); | ||
| FetchRequest.PartitionData requestData = data.sessionPartitions().get(partition); | ||
| if (requestData == null) { | ||
| String message; | ||
| if (data.metadata().isFull()) { | ||
| message = MessageFormatter.arrayFormat( | ||
| "Response for missing full request partition: partition={}; metadata={}", | ||
| new Object[]{partition, data.metadata()}).getMessage(); | ||
| } else { | ||
| message = MessageFormatter.arrayFormat( | ||
| "Response for missing session request partition: partition={}; metadata={}; toSend={}; toForget={}", | ||
| new Object[]{partition, data.metadata(), data.toSend(), data.toForget()}).getMessage(); | ||
| } | ||
|
|
||
| // Received fetch response for missing session partition | ||
| throw new IllegalStateException(message); | ||
| } else { | ||
| long fetchOffset = requestData.fetchOffset; | ||
| FetchResponse.PartitionData<Records> partitionData = entry.getValue(); | ||
|
|
||
| log.debug("Fetch {} at offset {} for partition {} returned fetch data {}", | ||
| isolationLevel, fetchOffset, partition, partitionData); | ||
|
|
||
| Iterator<? extends RecordBatch> batches = partitionData.records.batches().iterator(); | ||
| short responseVersion = resp.requestHeader().apiVersion(); | ||
|
|
||
| completedFetches.add(new CompletedFetch(partition, partitionData, | ||
| metricAggregator, batches, fetchOffset, responseVersion)); | ||
| } | ||
| Set<TopicPartition> partitions = new HashSet<>(response.responseData().keySet()); | ||
| FetchResponseMetricAggregator metricAggregator = new FetchResponseMetricAggregator(sensors, partitions); | ||
|
|
||
| for (Map.Entry<TopicPartition, FetchResponse.PartitionData<Records>> entry : response.responseData().entrySet()) { | ||
| TopicPartition partition = entry.getKey(); | ||
| FetchRequest.PartitionData requestData = data.sessionPartitions().get(partition); | ||
| if (requestData == null) { | ||
| String message; | ||
| if (data.metadata().isFull()) { | ||
| message = MessageFormatter.arrayFormat( | ||
| "Response for missing full request partition: partition={}; metadata={}", | ||
| new Object[]{partition, data.metadata()}).getMessage(); | ||
| } else { | ||
| message = MessageFormatter.arrayFormat( | ||
| "Response for missing session request partition: partition={}; metadata={}; toSend={}; toForget={}", | ||
| new Object[]{partition, data.metadata(), data.toSend(), data.toForget()}).getMessage(); | ||
| } | ||
|
|
||
| sensors.fetchLatency.record(resp.requestLatencyMs()); | ||
| } finally { | ||
| nodesWithPendingFetchRequests.remove(fetchTarget.id()); | ||
| // Received fetch response for missing session partition | ||
| throw new IllegalStateException(message); | ||
| } else { | ||
| long fetchOffset = requestData.fetchOffset; | ||
| FetchResponse.PartitionData<Records> partitionData = entry.getValue(); | ||
|
|
||
| log.debug("Fetch {} at offset {} for partition {} returned fetch data {}", | ||
| isolationLevel, fetchOffset, partition, partitionData); | ||
|
|
||
| Iterator<? extends RecordBatch> batches = partitionData.records.batches().iterator(); | ||
| short responseVersion = resp.requestHeader().apiVersion(); | ||
|
|
||
| completedFetches.add(new CompletedFetch(partition, partitionData, | ||
| metricAggregator, batches, fetchOffset, responseVersion)); | ||
| } | ||
| } | ||
|
|
||
| sensors.fetchLatency.record(resp.requestLatencyMs()); | ||
| } finally { | ||
| nodesWithPendingFetchRequests.remove(fetchTarget.id()); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| public void onFailure(RuntimeException e) { | ||
| synchronized (Fetcher.this) { | ||
| try { | ||
| FetchSessionHandler handler = sessionHandler(fetchTarget.id()); | ||
| if (handler != null) { | ||
| handler.handleError(e); | ||
| } | ||
| } finally { | ||
| nodesWithPendingFetchRequests.remove(fetchTarget.id()); | ||
| } | ||
| @Override | ||
| public void onFailure(RuntimeException e) { | ||
| synchronized (Fetcher.this) { | ||
| try { | ||
| FetchSessionHandler handler = sessionHandler(fetchTarget.id()); | ||
| if (handler != null) { | ||
| handler.handleError(e); | ||
| } | ||
| } finally { | ||
| nodesWithPendingFetchRequests.remove(fetchTarget.id()); | ||
| } | ||
| }); | ||
| } | ||
| } | ||
| }); | ||
|
|
||
| this.nodesWithPendingFetchRequests.add(entry.getKey().id()); | ||
|
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 line is moved above to https://github.com/apache/kafka/pull/7511/files#diff-b45245913eaae46aa847d2615d62cde0R262 the rest of the changes in this file are indentation changes. 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. (@thomaslee reminds me that |
||
| } | ||
| return fetchRequestMap.size(); | ||
| } | ||
|
|
||
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.
Wondering if the subtleties of this fairly benign-looking code warrant a comment here or in the test?
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.
Was hoping the test would catch the issues with it - but I guess it's possible in the future some change causes the test to be less useful. Could stick a comment to the effect of "ensure
nodesWithPendingFetchRequestsis always present before finalizer could be called" or something.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.
Was also wondering if there could ever be an exception thrown by
addListenerwhich would cause the listener to not be added or the completion handler to not be called?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.
Hm good question ... find it hard to imagine as implemented unless we end up with multiple listeners executing on the consumer thread & a listener that precedes this one throws or something along those lines. And in that scenario right now I think we'd expect the exception to bubble out of KafkaConsumer.poll(), which would at least give us a clear signal that something went terribly wrong.
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.
We rely on
addListeneralways resuling in listener being invoked in various places. So the code should be ok. But a comment that explains the sequence and exception scenarios would be useful.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.
Ok I've added a comment describing the rationale behind the sequence. If this looks good, what are the next steps for getting this merged?