-
Notifications
You must be signed in to change notification settings - Fork 15.4k
KAFKA-7548 : KafkaConsumer should not throw away already fetched data for paused partitions. #5844
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 |
|---|---|---|
|
|
@@ -71,6 +71,7 @@ | |
|
|
||
| import java.io.Closeable; | ||
| import java.nio.ByteBuffer; | ||
| import java.util.ArrayDeque; | ||
| import java.util.ArrayList; | ||
| import java.util.Collection; | ||
| import java.util.Collections; | ||
|
|
@@ -79,10 +80,12 @@ | |
| import java.util.HashSet; | ||
| import java.util.Iterator; | ||
| import java.util.LinkedHashMap; | ||
| import java.util.LinkedHashSet; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Optional; | ||
| import java.util.PriorityQueue; | ||
| import java.util.Queue; | ||
| import java.util.Set; | ||
| import java.util.concurrent.ConcurrentLinkedQueue; | ||
| import java.util.concurrent.atomic.AtomicInteger; | ||
|
|
@@ -130,6 +133,9 @@ public class Fetcher<K, V> implements SubscriptionState.Listener, Closeable { | |
| private final IsolationLevel isolationLevel; | ||
| private final Map<Integer, FetchSessionHandler> sessionHandlers; | ||
| private final AtomicReference<RuntimeException> cachedListOffsetsException = new AtomicReference<>(); | ||
| private final Map<TopicPartition, PartitionRecords> pausedNextInLineRecordsPerTopicPartition; | ||
| private final HashMap<TopicPartition, Queue<CompletedFetch>> pausedCompletedFetchesPerTopicPartition; | ||
|
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.
|
||
| private final LinkedHashSet<TopicPartition> recentlyUnPausedTopicPartitions; | ||
|
|
||
| private PartitionRecords nextInLineRecords = null; | ||
|
|
||
|
|
@@ -171,6 +177,10 @@ public Fetcher(LogContext logContext, | |
| this.requestTimeoutMs = requestTimeoutMs; | ||
| this.isolationLevel = isolationLevel; | ||
| this.sessionHandlers = new HashMap<>(); | ||
| this.pausedNextInLineRecordsPerTopicPartition = new HashMap<>(); | ||
| this.pausedCompletedFetchesPerTopicPartition = new HashMap<>(); | ||
| this.recentlyUnPausedTopicPartitions = new LinkedHashSet<>(); | ||
|
|
||
|
|
||
| subscriptions.addListener(this); | ||
| } | ||
|
|
@@ -198,6 +208,41 @@ public boolean hasCompletedFetches() { | |
| return !completedFetches.isEmpty(); | ||
| } | ||
|
|
||
| /** | ||
| * Return if there are buffered CompletedFetches for a paused TopicPartition. | ||
| * | ||
| * @param tp {@link TopicPartition} | ||
| * @return {@code true} if there are buffered {@link CompletedFetch}, {@false} otherwise | ||
| */ | ||
| public boolean hasPausedCompletedFetchesFor(TopicPartition 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. Can you make this method package private by removing the keyword
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. changed it to. : hasBufferedDataForPausedPartition(). I was thinking if there will be an usecase for apps to query if there is any buffered data for a paused partitions. I currently can't think of any. So we can make it package private for now and think of exposing it if there is a need in future. |
||
| return pausedCompletedFetchesPerTopicPartition.containsKey(tp); | ||
| } | ||
|
|
||
| /** | ||
| * Add the TopicPartition to the list of recently unpaused partitions. | ||
| * @param tp {@link TopicPartition} | ||
| */ | ||
| public void addRecentlyUnpausedPartition(TopicPartition tp) { | ||
| this.recentlyUnPausedTopicPartitions.add(tp); | ||
| } | ||
|
|
||
| /** | ||
| * Remove the TopicPartition from list of recently unpaused partitions. | ||
| * @param tp {@link TopicPartition} | ||
| */ | ||
| public void removeRecentlyUnpausedPartition(TopicPartition 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. Can we simplify the patch by not having this method? We pause a partition after putting it 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. Hi @lindong28 , can you shed more light on "We pause a partition after putting it in recentlyUnPausedTopicPartitions".
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. Regarding I agree that
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. As described above But I am not sure if this is less complex then just having a method to remove P1 from
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. Right, the alternative solution in the code snippet above is what I am suggesting. So we agree that we can achieve the same correctness and performance without having method All things being equal, it is always preferred to have less API (and logic) exposed from Fetcher.java. So the question here is, what is the benefit of having this extra API. I am just not convinced by the statement that this method makes code more explicit and readable. Whether all not this is more readable is an objective opinion and may be you for opinion from other developers.
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. Hi @lindong28, thanks a lot for the comments. |
||
| this.recentlyUnPausedTopicPartitions.remove(tp); | ||
| } | ||
|
|
||
| /** | ||
| * Clear the data for all paused partitions. | ||
| */ | ||
| public void clearBufferedDataForPausedPartitions() { | ||
|
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. Can we re-use the existing method |
||
| this.recentlyUnPausedTopicPartitions.clear(); | ||
| this.pausedNextInLineRecordsPerTopicPartition.clear(); | ||
| this.pausedCompletedFetchesPerTopicPartition.clear(); | ||
| } | ||
|
|
||
| /** | ||
| * Set-up a fetch request for any node that we have assigned partitions for which doesn't already have | ||
| * an in-flight fetch or pending fetch data. | ||
|
|
@@ -466,6 +511,21 @@ private Map<TopicPartition, Long> beginningOrEndOffset(Collection<TopicPartition | |
| return offsets; | ||
| } | ||
|
|
||
| private void readBufferedCompletedFetchesForPausedTopicPartitions(TopicPartition tp) { | ||
| Queue<CompletedFetch> pausedCompletedFetches = pausedCompletedFetchesPerTopicPartition.get(tp); | ||
| if (pausedCompletedFetches == null || pausedCompletedFetches.isEmpty()) { | ||
| pausedCompletedFetchesPerTopicPartition.remove(tp); | ||
| } else { | ||
| CompletedFetch bufferedCompletedFetch = pausedCompletedFetches.peek(); | ||
|
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. There is an extra space. Also, should we just do |
||
| // remove the TopicPartition if there is no more buffered CompletedFetch left to be drained | ||
| if (pausedCompletedFetches.isEmpty()) { | ||
|
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 statement is always false. |
||
| pausedCompletedFetchesPerTopicPartition.remove(tp); | ||
| } | ||
| nextInLineRecords = parseCompletedFetch(bufferedCompletedFetch); | ||
| pausedCompletedFetches.poll(); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Return the fetched records, empty the record buffer and update the consumed position. | ||
| * | ||
|
|
@@ -483,24 +543,38 @@ public Map<TopicPartition, List<ConsumerRecord<K, V>>> fetchedRecords() { | |
| try { | ||
| while (recordsRemaining > 0) { | ||
| if (nextInLineRecords == null || nextInLineRecords.isFetched) { | ||
| CompletedFetch completedFetch = completedFetches.peek(); | ||
| if (completedFetch == null) break; | ||
|
|
||
| try { | ||
| nextInLineRecords = parseCompletedFetch(completedFetch); | ||
| } catch (Exception e) { | ||
| // Remove a completedFetch upon a parse with exception if (1) it contains no records, and | ||
| // (2) there are no fetched records with actual content preceding this exception. | ||
| // The first condition ensures that the completedFetches is not stuck with the same completedFetch | ||
| // in cases such as the TopicAuthorizationException, and the second condition ensures that no | ||
| // potential data loss due to an exception in a following record. | ||
| FetchResponse.PartitionData partition = completedFetch.partitionData; | ||
| if (fetched.isEmpty() && (partition.records == null || partition.records.sizeInBytes() == 0)) { | ||
| completedFetches.poll(); | ||
| if (!recentlyUnPausedTopicPartitions.isEmpty()) { | ||
| Iterator<TopicPartition> itr = recentlyUnPausedTopicPartitions.iterator(); | ||
| TopicPartition tp = itr.next(); | ||
| if (pausedNextInLineRecordsPerTopicPartition.containsKey(tp)) { | ||
| nextInLineRecords = pausedNextInLineRecordsPerTopicPartition.remove(tp); | ||
| } else if (pausedCompletedFetchesPerTopicPartition.containsKey(tp)) { | ||
| readBufferedCompletedFetchesForPausedTopicPartitions(tp); | ||
| } | ||
| // remove the TopicPartition if there is no more buffered CompletedFetch or PartitionRecord left to be drained | ||
| if (!pausedCompletedFetchesPerTopicPartition.containsKey(tp) && !pausedNextInLineRecordsPerTopicPartition.containsKey(tp)) { | ||
| itr.remove(); | ||
| } | ||
| throw e; | ||
| } else { | ||
| CompletedFetch completedFetch = completedFetches.peek(); | ||
| if (completedFetch == null) break; | ||
|
|
||
| try { | ||
| nextInLineRecords = parseCompletedFetch(completedFetch); | ||
| } catch (Exception e) { | ||
| // Remove a completedFetch upon a parse with exception if (1) it contains no records, and | ||
| // (2) there are no fetched records with actual content preceding this exception. | ||
| // The first condition ensures that the completedFetches is not stuck with the same completedFetch | ||
| // in cases such as the TopicAuthorizationException, and the second condition ensures that no | ||
| // potential data loss due to an exception in a following record. | ||
| FetchResponse.PartitionData partition = completedFetch.partitionData; | ||
| if (fetched.isEmpty() && (partition.records == null || partition.records.sizeInBytes() == 0)) { | ||
| completedFetches.poll(); | ||
| } | ||
| throw e; | ||
| } | ||
| completedFetches.poll(); | ||
| } | ||
| completedFetches.poll(); | ||
| } else { | ||
| List<ConsumerRecord<K, V>> records = fetchRecords(nextInLineRecords, recordsRemaining); | ||
| TopicPartition partition = nextInLineRecords.partition; | ||
|
|
@@ -529,13 +603,21 @@ public Map<TopicPartition, List<ConsumerRecord<K, V>>> fetchedRecords() { | |
| } | ||
|
|
||
| private List<ConsumerRecord<K, V>> fetchRecords(PartitionRecords partitionRecords, int maxRecords) { | ||
| boolean shouldDrainRecords = true; | ||
| if (!subscriptions.isAssigned(partitionRecords.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", | ||
| partitionRecords.partition); | ||
| } else if (!subscriptions.isFetchable(partitionRecords.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 | ||
| // check if the TopicPartition is assigned but is paused. If it is paused, buffer the fetched records for | ||
| // reuse on unpause | ||
| if (subscriptions.isPaused(partitionRecords.partition)) { | ||
| this.pausedNextInLineRecordsPerTopicPartition.put(partitionRecords.partition, partitionRecords); | ||
| shouldDrainRecords = false; | ||
| nextInLineRecords = null; | ||
| } | ||
| log.debug("Not returning fetched records for assigned partition {} since it is no longer fetchable", | ||
|
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. Maybe put a log statement, indicating that we are putting some data aside in the |
||
| partitionRecords.partition); | ||
| } else { | ||
|
|
@@ -566,7 +648,9 @@ private List<ConsumerRecord<K, V>> fetchRecords(PartitionRecords partitionRecord | |
| } | ||
| } | ||
|
|
||
| partitionRecords.drain(); | ||
| if (shouldDrainRecords) { | ||
| partitionRecords.drain(); | ||
| } | ||
| return emptyList(); | ||
| } | ||
|
|
||
|
|
@@ -855,6 +939,9 @@ private List<TopicPartition> fetchablePartitions() { | |
| for (CompletedFetch completedFetch : completedFetches) { | ||
| exclude.add(completedFetch.partition); | ||
| } | ||
| // if there are already buffered CompletedFetches do not send another fetch for such TopicPartitions | ||
| exclude.addAll(pausedCompletedFetchesPerTopicPartition.keySet()); | ||
|
|
||
| fetchable.removeAll(exclude); | ||
| return fetchable; | ||
| } | ||
|
|
@@ -915,12 +1002,24 @@ private PartitionRecords parseCompletedFetch(CompletedFetch completedFetch) { | |
| long fetchOffset = completedFetch.fetchedOffset; | ||
| PartitionRecords partitionRecords = null; | ||
| Errors error = partition.error; | ||
| boolean paused = false; | ||
|
|
||
| try { | ||
| if (!subscriptions.isFetchable(tp)) { | ||
| // this can happen when a rebalance happened or a partition consumption paused | ||
| // while fetch is still in-flight | ||
| log.debug("Ignoring fetched records for partition {} since it is no longer fetchable", tp); | ||
| // if the topic partition is paused, buffer the completed fetch to be reuse on unpause | ||
| if (subscriptions.isAssigned(tp) && subscriptions.hasValidPosition(tp) && subscriptions.isPaused(tp)) { | ||
| Queue<CompletedFetch> pausedCompletedFetches = new ArrayDeque(); | ||
| Queue<CompletedFetch> existingPausedCompletedFetches = this.pausedCompletedFetchesPerTopicPartition.putIfAbsent(tp, pausedCompletedFetches); | ||
| if (existingPausedCompletedFetches != null) { | ||
| existingPausedCompletedFetches.add(completedFetch); | ||
| } else { | ||
| pausedCompletedFetches.add(completedFetch); | ||
| } | ||
| paused = true; | ||
| } | ||
| } else if (error == Errors.NONE) { | ||
| // we are interested in this fetch only if the beginning offset matches the | ||
| // current consumed position | ||
|
|
@@ -998,7 +1097,7 @@ private PartitionRecords parseCompletedFetch(CompletedFetch completedFetch) { | |
| if (partitionRecords == null) | ||
| completedFetch.metricAggregator.record(tp, 0, 0); | ||
|
|
||
| if (error != Errors.NONE) | ||
| if (!paused && error != Errors.NONE) | ||
| // we move the partition to the end if there was an error. This way, it's more likely that partitions for | ||
| // the same topic can remain together (allowing for more efficient serialization). | ||
| subscriptions.movePartitionToEnd(tp); | ||
|
|
@@ -1043,6 +1142,10 @@ private Optional<Integer> maybeLeaderEpoch(int leaderEpoch) { | |
| @Override | ||
| public void onAssignment(Set<TopicPartition> assignment) { | ||
| sensors.updatePartitionLagAndLeadSensors(assignment); | ||
| /* | ||
| * clear the buffered data for paused partitions on new 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. We usually only add comment inside method using |
||
| */ | ||
| clearBufferedDataForPausedPartitions(); | ||
| } | ||
|
|
||
| /** | ||
|
|
||
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.
There is an extra unnecessary space.