KAFKA-7548 : KafkaConsumer should not throw away already fetched data for paused partitions.#5844
KAFKA-7548 : KafkaConsumer should not throw away already fetched data for paused partitions.#5844MayureshGharat wants to merge 1 commit into
Conversation
|
@cmccabe @lindong28 @jjkoshy can you please take a look. |
|
Cc @hachikuji |
| 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.
Maybe put a log statement, indicating that we are putting some data aside in the pausedNextInLineRecordsPerTopicPartition plus its size.
lindong28
left a comment
There was a problem hiding this comment.
Thanks for the patch. Left some comment.
| 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; |
There was a problem hiding this comment.
HashMap can be replaced by Map.
| private final IsolationLevel isolationLevel; | ||
| private final Map<Integer, FetchSessionHandler> sessionHandlers; | ||
| private final AtomicReference<RuntimeException> cachedListOffsetsException = new AtomicReference<>(); | ||
| private final Map<TopicPartition, PartitionRecords> pausedNextInLineRecordsPerTopicPartition; |
There was a problem hiding this comment.
There is an extra unnecessary space.
| if (pausedCompletedFetches == null || pausedCompletedFetches.isEmpty()) { | ||
| pausedCompletedFetchesPerTopicPartition.remove(tp); | ||
| } else { | ||
| CompletedFetch bufferedCompletedFetch = pausedCompletedFetches.peek(); |
There was a problem hiding this comment.
There is an extra space.
Also, should we just do pausedCompletedFetches.poll() here?
| } else { | ||
| CompletedFetch bufferedCompletedFetch = pausedCompletedFetches.peek(); | ||
| // remove the TopicPartition if there is no more buffered CompletedFetch left to be drained | ||
| if (pausedCompletedFetches.isEmpty()) { |
There was a problem hiding this comment.
This statement is always false.
| * Remove the TopicPartition from list of recently unpaused partitions. | ||
| * @param tp {@link TopicPartition} | ||
| */ | ||
| public void removeRecentlyUnpausedPartition(TopicPartition tp) { |
There was a problem hiding this comment.
Can we simplify the patch by not having this method? We pause a partition after putting it in recentlyUnPausedTopicPartitions, we can still use fetcher.subscriptions to check whether the partition is unpaused while going over the partition in recentlyUnPausedTopicPartitions.
There was a problem hiding this comment.
Hi @lindong28 , can you shed more light on "We pause a partition after putting it in recentlyUnPausedTopicPartitions".
recentlyUnPausedTopicPartitions is used to give priority for returning buffered data for partitions that were paused earlier. When a partition is UnPaused, we add it to the "recentlyUnPausedTopicPartitions". When we Pause a partition, we remove it from "recentlyUnPausedPartitions".
If we have a method for adding to recentlyUnPausedPartitions, we should have a method for removing it as well, right?
The way I understand your suggested approach, if we want to use fetcher.subscriptions, we will have to iterate over all the fetchablePartitions in fetchedRecords() till we find the TopicPartition that is present in recentlyUnPausedPartitions and then check if there is data buffered for it. Iterating over fetcher.subscriptions, seems like an O(n) operation whereas using the recentlyUnpausedTopicPartitions as here, seems like O(m) operation where m < n.
There was a problem hiding this comment.
Regarding If we have a method for adding to recentlyUnPausedPartitions, we should have a method for removing it as well, right?, there is no strict rule that requires a public API to remove entry, as long as those entries can eventually be removed. Currently fetchedRecords() will actually remove entry from recentlyUnPausedPartitions if !pausedCompletedFetchesPerTopicPartition.containsKey(tp) && !pausedNextInLineRecordsPerTopicPartition.containsKey(tp) is evaluated to true.
I agree that recentlyUnPausedPartitions is needed to improve the time complexity as you mentioned. Here the suggestion is to simplify the patch by removing the method removeRecentlyUnpausedPartition(TopicPartition tp). Is there any performance or correctness concern if we remove removeRecentlyUnpausedPartition(...)?
There was a problem hiding this comment.
As described above recentlyUnpausedPartitions is used to keep track of the partitions that were just unpaused before calling poll(), so that we can give preference to buffered data for previously paused partitions.
Whenever we unpause a partition, we add it to recentlyUnPausedPartitions.
So if we had completedFetch for a paused partiton P1 along with the completedFetches for unpaused partitions [P2,P3,P4], the completedFetch for paused partition P1 will be added to the pausedCompletedFetchesPerTopicPartition.
Now if we unpause this partition P1 before next poll(), it would be one of the partition eligible for returning data to the user.
But at this point we are still scraping the records from a completedFetch of some other partition and might have reached the end and the maxPollRecords limit at the same time.
So in the next poll() we are going to iterate over a new completedFetch. At this point, we would be selecting the buffered completedFetch for P1, as it is the recently unpaused partition.
But before next poll(), if we pause the partition P1 and not remove it from the recetlyUnPausedPartitions, then in fetcher.fetchedRecords(), we will see P1 as a part of recentlyUnPausedPartition and we also have the buffered data for it in pausedCompletedFetchesPerTopicPartition, so we will
iterate over this completedFetch and return data for a paused partition, which is incorrect.
Ofcourse, we can avoid this by adding a check subscriptions.isFetchable(P1) in fetcher.fetchedRecords() something like this :
if (!recentlyUnPausedTopicPartitions.isEmpty()) {
Iterator<TopicPartition> itr = recentlyUnPausedTopicPartitions.iterator();
TopicPartition tp = itr.next();
if (subscriptions.isFetchable(P1)) {
......
}
....
But I am not sure if this is less complex then just having a method to remove P1 from recentlyUnPausedPartitions. On the contrary, I feel having a method to remove P1 from recentlyUnPausedPartitions when P1 is paused is more explicit and makes the code more readable.
There was a problem hiding this comment.
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 removeRecentlyUnpausedPartition.
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.
There was a problem hiding this comment.
Hi @lindong28, thanks a lot for the comments.
I am not 100% convinced with "it is always preferred to have less API (and logic) exposed from Fetcher.java". This class is internal to the KafkaConsumer logic and not exposed by an api directly from KafkaConsumer. We already have more powerful public apis like : sendFetches(), getTopicMetadata(), fetchedRecords() exposed in this class as public methods. Also we are publicly exposing an api to add to recentlyUnPausedPartitions. So not having a method to remove from it, seems odd to me.
I agree with "Whether all not this is more readable is an objective opinion". We can wait for someone else to take a look at the discussion here and let us know there opinions. Ping : @jjkoshy @junrao @onurkaraman
| /** | ||
| * Clear the data for all paused partitions. | ||
| */ | ||
| public void clearBufferedDataForPausedPartitions() { |
There was a problem hiding this comment.
Can we re-use the existing method clearBufferedDataForUnassignedPartitions() and clear data as necessary in that method?
| * @param tp {@link TopicPartition} | ||
| * @return {@code true} if there are buffered {@link CompletedFetch}, {@false} otherwise | ||
| */ | ||
| public boolean hasPausedCompletedFetchesFor(TopicPartition tp) { |
There was a problem hiding this comment.
Can you make this method package private by removing the keyword public and add comment that this method is used by test only?
There was a problem hiding this comment.
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.
| public void onAssignment(Set<TopicPartition> assignment) { | ||
| sensors.updatePartitionLagAndLeadSensors(assignment); | ||
| /* | ||
| * clear the buffered data for paused partitions on new assignment |
There was a problem hiding this comment.
We usually only add comment inside method using // rather than /* */. Also, the method name here is clear enough and the comment seems unnecessary.
|
Cc @hachikuji |
hachikuji
left a comment
There was a problem hiding this comment.
@MayureshGharat I'm not sure you are still working on this, but an alternative solution you might consider would be to leave the paused data inside completedFetches. When we peek the data in fetchedRecords, you can check whether the partition is paused. If it is, it can be moved to the back of the queue. The advantage is less bookkeeping.
|
Since this issue has gone a little stale I thought I would take a fresh look. @hachikuji In your last comment you made a suggestion to add any @MayureshGharat Do you have plans to pick up this PR again some time soon? If not, I would love the opportunity to dive into the Consumer client code to come up with a workable solution. |
|
I spoke with @MayureshGharat in email and we agreed that I'll follow up on this work (thanks!) I'll create a new PR. |
|
I've put up a new PR #6988 |
…partitions (#6988) This is an updated implementation of #5844 by @MayureshGharat (with Mayuresh's permission). As described in the original ticket: > Today when we call KafkaConsumer.poll(), it will fetch data from Kafka asynchronously and is put in to a local buffer (completedFetches). > > If now we pause some TopicPartitions and call KafkaConsumer.poll(), we might throw away any buffered data that we might have in the local buffer for these TopicPartitions. Generally, if an application is calling pause on some TopicPartitions, it is likely to resume those TopicPartitions in near future, which would require KafkaConsumer to re-issue a fetch for the same data that it had buffered earlier for these TopicPartitions. This is a wasted effort from the application's point of view. This patch fixes the problem by retaining the paused data in the completed fetches queue, essentially moving it to the back on each call to `fetchedRecords`. Reviewers: Jason Gustafson <jason@confluent.io>
|
Going to close this since we merged #6988. |
Today when we call KafkaConsumer.poll(), it will fetch data from Kafka asynchronously and is put in to a local buffer (completedFetches).
If now we pause some TopicPartitions and call KafkaConsumer.poll(), we might throw away any buffered data that we might have in the local buffer for these TopicPartitions. Generally, if an application is calling pause on some TopicPartitions, it is likely to resume those TopicPartitions in near future, which would require KafkaConsumer to re-issue a fetch for the same data that it had buffered earlier for these TopicPartitions. This is a wasted effort from the application's point of view.
The current patch does not throw away the already buffered data for paused partitions, but stores it in a separate buffer (pausedCompletedFetchesPerTopicPartition). If we have data in this separate buffer for a recently resumed partition, it is given higher preference as compared to other partitions.
The patch also makes sure that if we have data in this separate buffer (pausedCompletedFetchesPerTopicPartition) for a recently resumed partition, it will not send out another fetch request for that TopicPartition until that buffered data has been processed. This guarantees that we would have at the most 1 completedFetch buffered for a paused TopicPartition.