This is an automated email from the ASF dual-hosted git repository.

apoorvmittal10 pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/kafka.git


The following commit(s) were added to refs/heads/trunk by this push:
     new ac0d7e245b2 KAFKA-20783: Completion handling and more tests (3/N) 
(#22804)
ac0d7e245b2 is described below

commit ac0d7e245b213c20ab66bcea211d1cbd5ed2c95a
Author: Apoorv Mittal <[email protected]>
AuthorDate: Fri Jul 10 17:24:39 2026 +0100

    KAFKA-20783: Completion handling and more tests (3/N) (#22804)
    
    Minor change and more tests. I ll continue adding more tests for async
    handling.
    
    Reviewers: Andrew Schofield <[email protected]>
---
 .../java/kafka/server/share/DelayedShareFetch.java |  16 +-
 .../kafka/server/share/DelayedShareFetchTest.java  | 219 ++++++++++++++++++++-
 2 files changed, 231 insertions(+), 4 deletions(-)

diff --git a/core/src/main/java/kafka/server/share/DelayedShareFetch.java 
b/core/src/main/java/kafka/server/share/DelayedShareFetch.java
index e60d3979b20..312c1d1c102 100644
--- a/core/src/main/java/kafka/server/share/DelayedShareFetch.java
+++ b/core/src/main/java/kafka/server/share/DelayedShareFetch.java
@@ -63,6 +63,7 @@ import java.util.Optional;
 import java.util.OptionalInt;
 import java.util.OptionalLong;
 import java.util.Set;
+import java.util.concurrent.CancellationException;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.Future;
@@ -476,6 +477,13 @@ public class DelayedShareFetch extends DelayedOperation {
                     TopicIdPartition topicIdPartition = 
topicPartitionData.keySet().iterator().next();
                     readFuture.whenComplete((result, throwable) -> {
                         if (throwable != null) {
+                            if (throwable instanceof CancellationException) {
+                                // The completion handler execution has been 
cancelled due to the operation
+                                // being completed in onComplete.
+                                log.trace("Async read was cancelled for group 
{}, member {}",
+                                    shareFetch.groupId(), 
shareFetch.memberId());
+                                return;
+                            }
                             log.error("Async read failed for group {}, member 
{}",
                                 shareFetch.groupId(), shareFetch.memberId(), 
throwable);
                             // Error is handled in maybeCompleteAsyncFetch() 
via get() exception handling
@@ -774,6 +782,8 @@ public class DelayedShareFetch extends DelayedOperation {
         try {
             log.warn("Completing async fetch for group {}, member {}, 
partitions {} with empty response",
                 shareFetch.groupId(), shareFetch.memberId(), 
topicPartitionData.keySet());
+            // Cancel the future invocation of pending fetch.
+            pendingFetch.cancel(true);
             pendingFetch = null;
             // Update fetch ratio metric and complete the request with empty 
response.
             recordTopicPartitionsFetchRatioMetric(topicPartitionData);
@@ -884,10 +894,10 @@ public class DelayedShareFetch extends DelayedOperation {
 
     private boolean maybeRegisterCallbackPendingRemoteFetch() {
         log.trace("Registering callback pending remote fetch");
-        PendingRemoteFetches pendingFetch = pendingRemoteFetchesOpt.get();
-        if (!pendingFetch.isDone() && shareFetch.fetchParams().maxWaitMs < 
remoteFetchMaxWaitMs) {
+        PendingRemoteFetches pendingRemoteFetches = 
pendingRemoteFetchesOpt.get();
+        if (!pendingRemoteFetches.isDone() && 
shareFetch.fetchParams().maxWaitMs < remoteFetchMaxWaitMs) {
             TimerTask timerTask = new PendingRemoteFetchTimerTask();
-            pendingFetch.invokeCallbackOnCompletion(((ignored, throwable) -> {
+            pendingRemoteFetches.invokeCallbackOnCompletion(((ignored, 
throwable) -> {
                 timerTask.cancel();
                 log.trace("Invoked remote storage fetch callback for group {}, 
member {}, "
                         + "topic partitions {}", shareFetch.groupId(), 
shareFetch.memberId(),
diff --git a/core/src/test/java/kafka/server/share/DelayedShareFetchTest.java 
b/core/src/test/java/kafka/server/share/DelayedShareFetchTest.java
index e04274f0751..d89594807f4 100644
--- a/core/src/test/java/kafka/server/share/DelayedShareFetchTest.java
+++ b/core/src/test/java/kafka/server/share/DelayedShareFetchTest.java
@@ -2378,8 +2378,9 @@ public class DelayedShareFetchTest {
         LinkedHashMap<TopicIdPartition, SharePartition> sharePartitions = new 
LinkedHashMap<>();
         sharePartitions.put(tp0, sp0);
 
+        CompletableFuture<Map<TopicIdPartition, 
ShareFetchResponseData.PartitionData>> future = new CompletableFuture<>();
         ShareFetch shareFetch = new ShareFetch(FETCH_PARAMS, groupId, 
Uuid.randomUuid().toString(),
-            new CompletableFuture<>(), List.of(tp0), BATCH_OPTIMIZED, 
BATCH_SIZE, MAX_FETCH_RECORDS, BROKER_TOPIC_STATS);
+            future, List.of(tp0), BATCH_OPTIMIZED, BATCH_SIZE, 
MAX_FETCH_RECORDS, BROKER_TOPIC_STATS);
 
         CompletableFuture<LinkedHashMap<TopicIdPartition, LogReadResult>> 
asyncReadFuture = new CompletableFuture<>();
         when(logReader.readAsync(any(), any(), any(), any(), 
anyBoolean())).thenReturn(asyncReadFuture);
@@ -2416,6 +2417,8 @@ public class DelayedShareFetchTest {
         // Invoke tryComplete again to simulate async read completion but 
should return false as future
         // is not yet completed.
         assertFalse(delayedShareFetch.tryComplete());
+        // The result future should still not be completed.
+        assertFalse(shareFetch.isCompleted());
         // Complete future to simulate async read completion. The registered 
callback should trigger a
         // purgatory check rather than completing the request directly on the 
completing thread.
         asyncReadFuture.complete(buildLogReadResultMap(List.of(tp0)));
@@ -2431,6 +2434,220 @@ public class DelayedShareFetchTest {
         // Verify readFromLog was called (which is used by async readAsync 
internally)
         Mockito.verify(logReader).readAsync(any(), any(), any(), any(), 
anyBoolean());
         Mockito.verify(sp0).releaseFetchLock(fetchId);
+        // The future of shareFetch completes.
+        assertTrue(shareFetch.isCompleted());
+        assertEquals(Set.of(tp0), future.join().keySet());
+    }
+
+    /**
+     * Tests that when async read encounters an exception, request completes 
with error.
+     */
+    @Test
+    public void testAsyncReadExceptionReleasesLocks() {
+        String groupId = "grp";
+        Uuid topicId = Uuid.randomUuid();
+        LogReader logReader = mock(LogReader.class);
+        PartitionMetadataProvider metadataProvider = 
mock(PartitionMetadataProvider.class);
+        ReplicaManager replicaManager = mock(ReplicaManager.class);
+        TopicIdPartition tp0 = new TopicIdPartition(topicId, new 
TopicPartition("foo", 0));
+
+        SharePartition sp0 = mock(SharePartition.class);
+        LinkedHashMap<TopicIdPartition, SharePartition> sharePartitions = new 
LinkedHashMap<>();
+        sharePartitions.put(tp0, sp0);
+
+        CompletableFuture<Map<TopicIdPartition, 
ShareFetchResponseData.PartitionData>> future = new CompletableFuture<>();
+        ShareFetch shareFetch = new ShareFetch(FETCH_PARAMS, groupId, 
Uuid.randomUuid().toString(),
+            future, List.of(tp0), BATCH_OPTIMIZED, BATCH_SIZE, 
MAX_FETCH_RECORDS, BROKER_TOPIC_STATS);
+
+        CompletableFuture<LinkedHashMap<TopicIdPartition, LogReadResult>> 
asyncReadFuture = new CompletableFuture<>();
+        when(logReader.readAsync(any(), any(), any(), any(), 
anyBoolean())).thenReturn(asyncReadFuture);
+
+        when(sp0.maybeAcquireFetchLock(any())).thenReturn(true);
+        when(sp0.canAcquireRecords()).thenReturn(true);
+        when(sp0.nextFetchOffset()).thenReturn(0L);
+        when(sp0.fetchOffsetMetadata(anyLong()))
+            .thenReturn(Optional.empty())
+            .thenReturn(Optional.of(new LogOffsetMetadata(0, 1, 0)));
+        when(metadataProvider.endOffsetMetadata(any(), any())).thenReturn(new 
LogOffsetMetadata(1, 1, 1));
+
+        PartitionMaxBytesStrategy partitionMaxBytesStrategy = 
mockPartitionMaxBytes(Set.of(tp0));
+
+        Uuid fetchId = Uuid.randomUuid();
+        DelayedShareFetch delayedShareFetch = 
DelayedShareFetchBuilder.builder()
+            .withShareFetchData(shareFetch)
+            .withReplicaManager(replicaManager)
+            .withLogReader(logReader)
+            .withMetadataProvider(metadataProvider)
+            .withPartitionMaxBytesStrategy(partitionMaxBytesStrategy)
+            .withSharePartitions(sharePartitions)
+            .withFetchId(fetchId)
+            .build();
+
+        // Initially no pending fetch
+        assertNull(delayedShareFetch.pendingFetch());
+        // First tryComplete triggers async read
+        assertFalse(delayedShareFetch.tryComplete());
+        assertNotNull(delayedShareFetch.pendingFetch());
+        // Complete with exception.
+        asyncReadFuture.completeExceptionally(new RuntimeException("Simulated 
read failure"));
+        Mockito.verify(replicaManager).completeDelayedShareFetchRequest(any());
+        // The request should not be completed yet as the purgatory check 
(mocked) is a no-op here.
+        assertFalse(delayedShareFetch.isCompleted());
+
+        // Simulate the purgatory re-invoking tryComplete. get() throws, so 
the request is force-completed
+        // with an error and the partition locks are released.
+        assertTrue(delayedShareFetch.tryComplete());
+        assertTrue(delayedShareFetch.isCompleted());
+        Map<TopicIdPartition, ShareFetchResponseData.PartitionData> response = 
future.join();
+        assertEquals(Set.of(tp0), response.keySet());
+        assertEquals(Errors.UNKNOWN_SERVER_ERROR.code(), 
response.get(tp0).errorCode());
+        assertTrue(response.get(tp0).errorMessage().contains("Simulated read 
failure"));
+        Mockito.verify(sp0).releaseFetchLock(fetchId);
+    }
+
+    /**
+     * Tests that onComplete handles the case where async fetch is still 
pending.
+     * This simulates timeout scenario where async read hasn't completed yet.
+     */
+    @Test
+    public void testOnCompleteWithPendingAsyncFetch() {
+        String groupId = "grp";
+        Uuid topicId = Uuid.randomUuid();
+        LogReader logReader = mock(LogReader.class);
+        ReplicaManager replicaManager = mock(ReplicaManager.class);
+        PartitionMetadataProvider metadataProvider = 
mock(PartitionMetadataProvider.class);
+        TopicIdPartition tp0 = new TopicIdPartition(topicId, new 
TopicPartition("foo", 0));
+
+        SharePartition sp0 = mock(SharePartition.class);
+        LinkedHashMap<TopicIdPartition, SharePartition> sharePartitions = new 
LinkedHashMap<>();
+        sharePartitions.put(tp0, sp0);
+
+        CompletableFuture<Map<TopicIdPartition, 
ShareFetchResponseData.PartitionData>> future = new CompletableFuture<>();
+        ShareFetch shareFetch = new ShareFetch(FETCH_PARAMS, groupId, 
Uuid.randomUuid().toString(),
+            future, List.of(tp0), BATCH_OPTIMIZED, BATCH_SIZE, 
MAX_FETCH_RECORDS, BROKER_TOPIC_STATS);
+
+        CompletableFuture<LinkedHashMap<TopicIdPartition, LogReadResult>> 
asyncReadFuture = new CompletableFuture<>();
+        when(logReader.readAsync(any(), any(), any(), any(), 
anyBoolean())).thenReturn(asyncReadFuture);
+
+        when(sp0.maybeAcquireFetchLock(any())).thenReturn(true);
+        when(sp0.canAcquireRecords()).thenReturn(true);
+        when(sp0.nextFetchOffset()).thenReturn(0L);
+        when(sp0.fetchOffsetMetadata(anyLong()))
+            .thenReturn(Optional.empty())
+            .thenReturn(Optional.of(new LogOffsetMetadata(0, 1, 0)));
+        when(metadataProvider.endOffsetMetadata(any(), any())).thenReturn(new 
LogOffsetMetadata(1, 1, 1));
+
+        PartitionMaxBytesStrategy partitionMaxBytesStrategy = 
mockPartitionMaxBytes(Set.of(tp0));
+
+        Uuid fetchId = Uuid.randomUuid();
+        DelayedShareFetch delayedShareFetch = 
DelayedShareFetchBuilder.builder()
+            .withShareFetchData(shareFetch)
+            .withLogReader(logReader)
+            .withMetadataProvider(metadataProvider)
+            .withPartitionMaxBytesStrategy(partitionMaxBytesStrategy)
+            .withSharePartitions(sharePartitions)
+            .withReplicaManager(replicaManager)
+            .withFetchId(fetchId)
+            .build();
+
+        // Initially no pending fetch
+        assertNull(delayedShareFetch.pendingFetch());
+        // First tryComplete triggers async read
+        assertFalse(delayedShareFetch.tryComplete());
+        assertNotNull(delayedShareFetch.pendingFetch());
+        // Simulate timeout: onComplete is invoked while the async read is 
still in-flight (not done).
+        delayedShareFetch.forceComplete();
+        // As the async read had not completed, the request is completed with 
an empty response, the
+        // pending fetch is cleared and the partition locks are released.
+        assertNull(delayedShareFetch.pendingFetch());
+        assertTrue(shareFetch.isCompleted());
+        assertTrue(future.isDone());
+        assertEquals(Set.of(), future.join().keySet());
+        Mockito.verify(sp0).releaseFetchLock(fetchId);
+        // Pending fetch is marked to null.
+        assertNull(delayedShareFetch.pendingFetch());
+
+        // Complete the pending fetch successfully to validate that it doesn't 
invoke the callback.
+        asyncReadFuture.complete(buildLogReadResultMap(List.of(tp0)));
+        // Verify the callback handler should not be executed on the request 
completion now.
+        Mockito.verify(replicaManager, 
times(0)).completeDelayedShareFetchRequest(any());
+    }
+
+    /**
+     * Tests that onComplete handles the case where async fetch is just 
completed, on timeout.
+     */
+    @SuppressWarnings("unchecked")
+    @Test
+    public void testOnCompleteWithCompletedAsyncFetch() throws Exception {
+        String groupId = "grp";
+        Uuid topicId = Uuid.randomUuid();
+        LogReader logReader = mock(LogReader.class);
+        ReplicaManager replicaManager = mock(ReplicaManager.class);
+        PartitionMetadataProvider metadataProvider = 
mock(PartitionMetadataProvider.class);
+        TopicIdPartition tp0 = new TopicIdPartition(topicId, new 
TopicPartition("foo", 0));
+
+        SharePartition sp0 = mock(SharePartition.class);
+        LinkedHashMap<TopicIdPartition, SharePartition> sharePartitions = new 
LinkedHashMap<>();
+        sharePartitions.put(tp0, sp0);
+
+        CompletableFuture<Map<TopicIdPartition, 
ShareFetchResponseData.PartitionData>> future = new CompletableFuture<>();
+        ShareFetch shareFetch = new ShareFetch(FETCH_PARAMS, groupId, 
Uuid.randomUuid().toString(),
+            future, List.of(tp0), BATCH_OPTIMIZED, BATCH_SIZE, 
MAX_FETCH_RECORDS, BROKER_TOPIC_STATS);
+
+        CompletableFuture<LinkedHashMap<TopicIdPartition, LogReadResult>> 
asyncReadFuture = mock(CompletableFuture.class);
+        when(logReader.readAsync(any(), any(), any(), any(), 
anyBoolean())).thenReturn(asyncReadFuture);
+
+        when(sp0.maybeAcquireFetchLock(any())).thenReturn(true);
+        when(sp0.canAcquireRecords()).thenReturn(true);
+        when(sp0.nextFetchOffset()).thenReturn(0L);
+        when(sp0.fetchOffsetMetadata(anyLong()))
+            .thenReturn(Optional.empty())
+            .thenReturn(Optional.of(new LogOffsetMetadata(0, 1, 0)));
+        when(metadataProvider.endOffsetMetadata(any(), any())).thenReturn(new 
LogOffsetMetadata(1, 1, 1));
+
+        PartitionMaxBytesStrategy partitionMaxBytesStrategy = 
mockPartitionMaxBytes(Set.of(tp0));
+
+        // Run false for tryComplete and true for onComplete.
+        when(asyncReadFuture.isDone()).thenReturn(false).thenReturn(true);
+        // Teurn successful response when fetched in onComplete.
+        
when(asyncReadFuture.get()).thenReturn(buildLogReadResultMap(List.of(tp0)));
+
+        Uuid fetchId = Uuid.randomUuid();
+        DelayedShareFetch delayedShareFetch = 
DelayedShareFetchBuilder.builder()
+            .withShareFetchData(shareFetch)
+            .withLogReader(logReader)
+            .withMetadataProvider(metadataProvider)
+            .withPartitionMaxBytesStrategy(partitionMaxBytesStrategy)
+            .withSharePartitions(sharePartitions)
+            .withReplicaManager(replicaManager)
+            .withFetchId(fetchId)
+            .build();
+
+        // Initially no pending fetch
+        assertNull(delayedShareFetch.pendingFetch());
+        // First tryComplete triggers async read
+        assertFalse(delayedShareFetch.tryComplete());
+        assertNotNull(delayedShareFetch.pendingFetch());
+        try (MockedStatic<ShareFetchUtils> mockedShareFetchUtils = 
Mockito.mockStatic(ShareFetchUtils.class, Mockito.CALLS_REAL_METHODS)) {
+            mockedShareFetchUtils.when(
+                    () -> ShareFetchUtils.processFetchResponse(any(), any(), 
any(), any(), any()))
+                .thenReturn(Map.of(tp0, 
mock(ShareFetchResponseData.PartitionData.class)));
+            // Simulate timeout: onComplete is invoked while the async read is 
still in-flight (not done).
+            delayedShareFetch.forceComplete();
+            // As the async read has completed now since second invocation to 
isDone returns true from
+            // mocked response, the request is completed with data.
+            assertNull(delayedShareFetch.pendingFetch());
+            assertTrue(shareFetch.isCompleted());
+            assertTrue(future.isDone());
+            Map<TopicIdPartition, ShareFetchResponseData.PartitionData> 
response = future.join();
+            assertEquals(Set.of(tp0), response.keySet());
+            // Request should be completed without any error. Do not validate 
the response as it's returned
+            // from mocked ShareFetchUtils.
+            assertEquals(Errors.NONE.code(), response.get(tp0).errorCode());
+            Mockito.verify(sp0).releaseFetchLock(fetchId);
+            // Pending fetch is marked to null.
+            assertNull(delayedShareFetch.pendingFetch());
+        }
     }
 
     static void mockTopicIdPartitionToReturnDataEqualToMinBytes(ReplicaManager 
replicaManager, TopicIdPartition topicIdPartition, int minBytes) {

Reply via email to