-
Notifications
You must be signed in to change notification settings - Fork 14.8k
KAFKA-19800-2: Created an implementation class NetworkPartitionMetadataClient for PartitionMetadataClient #20852
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
base: trunk
Are you sure you want to change the base?
Conversation
apoorvmittal10
left a comment
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.
Thanks for the PR, some basic questions in the comments. Also can you please add the change for the usage of PartitionMetadataClient from where it will be initialized.
| Set<TopicPartition> topicPartitions | ||
| ) { | ||
| if (topicPartitions == null || topicPartitions.isEmpty()) { | ||
| return new HashMap<>(); |
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.
| return new HashMap<>(); | |
| return Map.of(); |
| this.sendThread = new SendThread( | ||
| "NetworkPartitionMetadataClientSendThread", | ||
| networkClient, | ||
| Math.toIntExact(CommonClientConfigs.DEFAULT_SOCKET_CONNECTION_SETUP_TIMEOUT_MAX_MS), //30 seconds | ||
| this.time | ||
| ); | ||
| this.sendThread.start(); |
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.
Are we going to crerate a new NetworkClient with new connection to every broker or re-use some existing network client?
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.
Can we have the usgae of this class in the same PR?
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.
Again if it needs to be lazily loaded then we might have to delay the start.
AndrewJSchofield
left a comment
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.
Thanks for the PR. It looks pretty complete with just a few comments.
| return requests; | ||
| } | ||
|
|
||
| private void handleErrorResponse(PendingRequest pendingRequest, ClientResponse clientResponse) { |
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.
nit: I'd put this method beneath handleResponse just for ease of reading and maintenance.
| } | ||
| } | ||
|
|
||
| private ListOffsetsPartitionResponse createErrorPartitionResponse(TopicPartition tp, short errorCode) { |
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.
nit: Let's put this utility method right at the end of the class.
| assertTrue(futures.containsKey(tp)); | ||
|
|
||
| ListOffsetsPartitionResponse response = futures.get(tp).get(); | ||
| assertNotNull(response); |
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.
Let's have an assertion that the future is completed in all of these tests please.
| ListOffsetsPartitionResponse response = futures.get(tp).get(); | ||
| assertNotNull(response); | ||
| assertEquals(PARTITION, response.partitionIndex()); | ||
| assertEquals(Errors.UNKNOWN_TOPIC_OR_PARTITION.code(), response.errorCode()); |
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.
Let's check that the offset, timestamp and epoch are -1 in the responses for the error cases.
| new ListOffsetsPartitionResponse() | ||
| .setPartitionIndex(PARTITION) | ||
| .setErrorCode(Errors.UNKNOWN_TOPIC_OR_PARTITION.code()) | ||
| .setOffset(ListOffsetsResponse.UNKNOWN_OFFSET) |
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.
Because -1 are the default values here, I think you could not set the values in this constructor. In this way, you would be testing that error paths which uses this response and do not set values do actually pick up the defaults with no extra effort.
| /** | ||
| * Client interface for retrieving latest offsets for topic partitions. | ||
| */ | ||
| public interface PartitionMetadataClient extends AutoCloseable { |
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.
You shouldn't need this interface now as the other PR is merged.
| this.sendThread = new SendThread( | ||
| "NetworkPartitionMetadataClientSendThread", | ||
| networkClient, | ||
| Math.toIntExact(CommonClientConfigs.DEFAULT_SOCKET_CONNECTION_SETUP_TIMEOUT_MAX_MS), //30 seconds | ||
| this.time | ||
| ); | ||
| this.sendThread.start(); |
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.
Can we have the usgae of this class in the same PR?
apoorvmittal10
left a comment
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.
Thanks for the PR, some comments.
| NetworkUtils.buildNetworkClient( | ||
| "NetworkPartitionMetadataClient", | ||
| config, | ||
| metrics, | ||
| Time.SYSTEM, | ||
| new LogContext(s"[NetworkPartitionMetadataClient broker=${config.brokerId}]") | ||
| ), |
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 need to decide if we want the PartitionMetadataClient to be lazily loaded, if yes then we should pass a provider which can fetch the NetworkClient.
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.
I think this is a good model since it's not going to be used in every cluster.
|
|
||
| override def close(): Unit = {} | ||
| } | ||
| private def createNetworkPartitionMetadataClient(metadataCache: MetadataCache): PartitionMetadataClient = { |
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.
nit:
| private def createNetworkPartitionMetadataClient(metadataCache: MetadataCache): PartitionMetadataClient = { | |
| private def createPartitionMetadataClient(metadataCache: MetadataCache): PartitionMetadataClient = { |
| this.sendThread = new SendThread( | ||
| "NetworkPartitionMetadataClientSendThread", | ||
| networkClient, | ||
| Math.toIntExact(CommonClientConfigs.DEFAULT_SOCKET_CONNECTION_SETUP_TIMEOUT_MAX_MS), //30 seconds | ||
| this.time | ||
| ); | ||
| this.sendThread.start(); |
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.
Again if it needs to be lazily loaded then we might have to delay the start.
|
|
||
| // Map to store futures for each TopicPartition | ||
| Map<TopicPartition, CompletableFuture<OffsetResponse>> futures = new HashMap<>(); | ||
|
|
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.
nit: remove line break
|
|
||
| if (leaderNodeOpt.isEmpty() || leaderNodeOpt.get().isEmpty()) { | ||
| // No leader available - complete with error | ||
|
|
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.
remove line break
| return; | ||
| } | ||
|
|
||
| log.debug("ListOffsets response received - {}", clientResponse); |
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.
Why not this is the first line in the method?
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.
Writing the log only during success scenarios might help better while debugging future issues, rather than logging it everytime. I have changed the log statement to ListOffsets response received successfully, for better understanding
| } | ||
|
|
||
| log.debug("ListOffsets response received - {}", clientResponse); | ||
|
|
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.
nit: remove line break
|
|
||
| log.debug("ListOffsets response received - {}", clientResponse); | ||
|
|
||
| // Parse the response |
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.
Is this comment providing any meaningful insight?
| */ | ||
| private record PendingRequest( | ||
| Node node, | ||
| List<TopicPartition> partitions, |
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.
If these are only required to log in case of error then you can write pendingRequest.futures.keySet() and avoid passing List<TopicPartition> partitions altogether.
| for (ListOffsetsPartitionResponse partitionResponse : topicResponse.partitions()) { | ||
| TopicPartition tp = new TopicPartition(topicName, partitionResponse.partitionIndex()); | ||
| // Remove the corresponding future from the map and complete it. | ||
| CompletableFuture<OffsetResponse> future = pendingRequest.futures.remove(tp); |
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.
I do get why do you want to remove the topicPartition from the futures so can complete the pending ones later, but it's not a good idea to modify the futures list, as it can be passed as unmodifiable by some code in future. Hence, either keep a set of topic partitions which are completed and then while iterating pendingRequest.futures check if it's already contained in the set or a better one is to avoid set and just check if the future is still pending i.e.
pendingRequest.futures.forEach((tp, future) -> {
// If future is not completed yet hence topic-partition was not included in the response, complete with error
if (!future.isDone()) {
future.complete(new OffsetResponse(-1, Errors.UNKNOWN_TOPIC_OR_PARTITION));
}
});
apoorvmittal10
left a comment
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.
Thanks for the changes, looks good mostly. Some comments.
| return this; | ||
| } | ||
|
|
||
| public static NetworkPartitionMetadataClientBuilder bulider() { |
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.
| public static NetworkPartitionMetadataClientBuilder bulider() { | |
| static NetworkPartitionMetadataClientBuilder builder() { |
| return new NetworkPartitionMetadataClientBuilder(); | ||
| } | ||
|
|
||
| public NetworkPartitionMetadataClient build() { |
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.
| public NetworkPartitionMetadataClient build() { | |
| NetworkPartitionMetadataClient build() { |
| NetworkPartitionMetadataClientBuilder withTime(Time time) { | ||
| this.time = time; | ||
| return this; |
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.
Are these methods used ever? If not then remove them.
| import static org.mockito.Mockito.verify; | ||
| import static org.mockito.Mockito.when; | ||
|
|
||
| class NetworkPartitionMetadataClientTest { |
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.
Missing tests
- Failure handling of condition:
clientResponse.wasTimedOut()andclientResponse == null - Close method error condition -
catch (InterruptedException e)
|
|
||
| private final MetadataCache metadataCache; | ||
| private final Supplier<KafkaClient> networkClientSupplier; | ||
| private volatile SendThread sendThread; |
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.
nit: move after the final variable declaration
| this.networkClientSupplier = networkClientSupplier; | ||
| this.time = time; | ||
| this.listenerName = listenerName; | ||
| this.sendThread = null; |
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.
nit: not required
| if (sendThread == null) { | ||
| synchronized (initializationLock) { | ||
| if (sendThread == null) { |
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.
Better to use AtomicBoolean with compareAndSet.
| ); | ||
| thread.start(); | ||
| sendThread = thread; | ||
| log.debug("NetworkPartitionMetadataClient sendThread initialized and started"); |
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.
| log.debug("NetworkPartitionMetadataClient sendThread initialized and started"); | |
| log.info("NetworkPartitionMetadataClient sendThread initialized and started"); |
| } catch (CompletionException e) { | ||
| // If fetching latest offset for a partition failed, return the error in the response for that partition. | ||
| partitionResponses.add(new DescribeShareGroupOffsetsResponseData.DescribeShareGroupOffsetsResponsePartition() | ||
| .setPartitionIndex(partitionData.partition()) | ||
| .setErrorCode(Errors.forException(e.getCause()).code()) | ||
| .setErrorMessage(e.getCause().getMessage())); | ||
| } |
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.
Do we need catch block now?
This PR is part of
KIP-1226.
This PR introduces an implementation class
NetworkPartitionMetadataClient for PartitionMetadataClient, that uses a
NetworkClient to send ListOffsetsRequest to the destination node. The
destination node should be the leader broker for the partitions in the
request and is retrieved from MetadataCache.
This new imple class will later be used in GroupCoordinatorService to
find the partition end offsets while computing share partition lag for
DescribeShareGroupOffsets request.