adixitconfluent commented on code in PR #22780:
URL: https://github.com/apache/kafka/pull/22780#discussion_r3543294616


##########
core/src/main/java/kafka/server/share/DelayedShareFetch.java:
##########
@@ -332,43 +417,68 @@ private void 
resetFetchOffsetMetadataForRemoteFetchPartitions(
         });
     }
 
-    /**
-     * Try to complete the fetch operation if we can acquire records for any 
partition in the share fetch request.
-     */
     @Override
     public boolean tryComplete() {
+        // Check if there's a pending fetch from a previous tryComplete 
invocation.
+        // This happens when the async read didn't complete immediately and we 
stored the future.
+        // If the async read is still in progress, stay in purgatory. 
Otherwise, process the result.
+        if (pendingFetch != null) {
+            if (!pendingFetch.isDone()) {
+                // Async read still in flight - partition locks are still held.
+                return false;
+            }
+            // Async read completed - retrieve result and process
+            return maybeCompleteAsyncFetch(pendingFetch, partitionsAcquired);
+        }
+
         // Check to see if the remote fetch is in flight. If there is an in 
flight remote fetch we want to resolve it first.
+        // Remote fetch can only happen after async fetch succeeds.
         if (pendingRemoteFetchesOpt.isPresent()) {
             return maybeCompletePendingRemoteFetch();
         }
 
+        // Try to acquire partition locks for all partitions in the request
         LinkedHashMap<TopicIdPartition, Long> topicPartitionData = 
acquirablePartitions(sharePartitions);
         try {
             if (!topicPartitionData.isEmpty()) {
+                // Successfully acquired locks for one or more partitions
                 // Update the metric to record the time taken to acquire the 
locks for the share partitions.
                 updateAcquireElapsedTimeMetric();
-                // In case, fetch offset metadata doesn't exist for one or 
more topic partitions, we do a
-                // readFromLog to populate the offset metadata and update the 
fetch offset metadata for
-                // those topic partitions.
-                LinkedHashMap<TopicIdPartition, LogReadResult> readResponse = 
maybeReadFromLog(topicPartitionData);
-                // Store the remote fetch info for the topic partitions for 
which we need to perform remote fetch.
-                LinkedHashMap<TopicIdPartition, LogReadResult> 
remoteStorageFetchInfoMap = 
maybePrepareRemoteStorageFetchInfo(topicPartitionData, readResponse);
-
-                if (!remoteStorageFetchInfoMap.isEmpty()) {
-                    return maybeProcessRemoteFetch(topicPartitionData, 
remoteStorageFetchInfoMap);
-                }
-                maybeUpdateFetchOffsetMetadata(topicPartitionData, 
readResponse);
-                if (anyPartitionHasLogReadError(readResponse) || 
isMinBytesSatisfied(topicPartitionData, 
partitionMaxBytesStrategy.maxBytes(shareFetch.fetchParams().maxBytes, 
topicPartitionData.keySet(), topicPartitionData.size()))) {
-                    partitionsAcquired = topicPartitionData;
-                    localPartitionsAlreadyFetched = readResponse;
-                    return forceComplete();
+                // In case, fetch offset metadata doesn't exist for one or 
more topic partitions, we do an
+                // async read to populate the offset metadata and update the 
fetch offset metadata for
+                // those topic partitions. This uses readAsync with 
readRemote=false.
+                CompletableFuture<LinkedHashMap<TopicIdPartition, 
LogReadResult>> readFuture = maybeReadFromLog(topicPartitionData);
+                // Check if future completed synchronously or needs async 
handling
+                if (readFuture.isDone()) {
+                    // Fast path, async read completed synchronously.
+                    // Process the result immediately without storing the 
future or registering callbacks
+                    return maybeCompleteAsyncFetch(readFuture, 
topicPartitionData);
                 } else {
-                    log.debug("minBytes is not satisfied for the share fetch 
request for group {}, member {}, " +
-                            "topic partitions {}", shareFetch.groupId(), 
shareFetch.memberId(),
-                        sharePartitions.keySet());
-                    releasePartitionLocks(topicPartitionData.keySet());
+                    // Store the future and register a callback. Partition 
locks remain held during the async operation.
+                    // When the async read completes, the callback will 
trigger a purgatory retry which will
+                    // invoke tryComplete() again, where we'll process the 
result.
+                    partitionsAcquired = topicPartitionData;
+                    pendingFetch = readFuture;
+
+                    // Register callback to be invoked when async read 
completes
+                    // This callback executes on the thread that completes the 
future (IO thread pool)
+                    readFuture.whenComplete((result, throwable) -> {
+                        if (throwable != null) {
+                            log.error("Async read failed for group {}, member 
{}",
+                                shareFetch.groupId(), shareFetch.memberId(), 
throwable);
+                            // Error is handled in maybeCompleteAsyncFetch() 
via get() exception handling
+                        }
+                        // maybeCompleteAsyncFetch() handles: get(), remote 
fetch check, minBytes check
+                        // and calls forceComplete() internally when the 
request should complete
+                        maybeCompleteAsyncFetch(readFuture, 
topicPartitionData);
+                        // Note: return value is ignored since we're in 
callback context, not tryComplete()
+                        // forceComplete() is called internally by 
processAsyncReadResponse() when needed.
+                    });
+                    // Return false to keep request in purgatory - locks 
remain held
+                    return false;
                 }

Review Comment:
   The callback calls `maybeCompleteAsyncFetch` directly on the IO thread that 
completes the future, bypassing the purgatory lock. Meanwhile, purgatory can 
invoke safeTryComplete() → tryComplete() on a watcher thread under the lock. 
Both threads can concurrently enter `maybeCompleteAsyncFetch` and mutate shared 
state (pendingFetch, partitionsAcquired, localPartitionsAlreadyFetched, 
releasePartitionLocks).`forceComplete` prevents double completion, but all the 
state mutations before it are unprotected.
   
   Fix: The callback should go through purgatory's checkAndComplete instead of 
directly calling maybeCompleteAsyncFetch().



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to