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

hangc0276 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/bookkeeper.git


The following commit(s) were added to refs/heads/master by this push:
     new 18a4368d2d Fix batch reads hanging after digest mismatch retries are 
ignored (#4789)
18a4368d2d is described below

commit 18a4368d2df74107e3a825395d93ec8b57703c75
Author: void-ptr974 <[email protected]>
AuthorDate: Wed May 27 02:51:00 2026 +0800

    Fix batch reads hanging after digest mismatch retries are ignored (#4789)
    
    * Fix batch read retry on digest mismatch
    
      Batch reads marked the request as complete before verifying the
      digest of each returned entry. If one entry in the response failed
      digest verification, the operation attempted to retry on another
      replica, but the request had already been completed, so the retry
      response could be ignored and the batch read could hang or fail.
    
      Verify all entries in the batch before completing the request. On
      digest mismatch, leave the request incomplete, discard the whole
      response, and retry the same batch on the next replica. Only create
      LedgerEntryImpl instances after the full batch has passed digest
      verification, so no partially verified entries are retained.
    
      Add tests for retrying after a corrupt batch response and for the
      case where an earlier entry verifies successfully but a later entry
      fails digest verification.
    
    * Return verified batch prefix on digest mismatch
---
 .../apache/bookkeeper/client/BatchedReadOp.java    |  49 ++++++---
 .../apache/bookkeeper/client/TestBatchedRead.java  | 110 +++++++++++++++++++++
 2 files changed, 146 insertions(+), 13 deletions(-)

diff --git 
a/bookkeeper-server/src/main/java/org/apache/bookkeeper/client/BatchedReadOp.java
 
b/bookkeeper-server/src/main/java/org/apache/bookkeeper/client/BatchedReadOp.java
index ebba79faea..89a976644e 100644
--- 
a/bookkeeper-server/src/main/java/org/apache/bookkeeper/client/BatchedReadOp.java
+++ 
b/bookkeeper-server/src/main/java/org/apache/bookkeeper/client/BatchedReadOp.java
@@ -112,6 +112,11 @@ public class BatchedReadOp extends ReadOpBase implements 
BatchedReadEntryCallbac
         heardFromHosts.add(rctx.to);
         heardFromHostsBitSet.set(rctx.bookieIndex, true);
 
+        /*
+         * Retain the response while this read op handles it. complete() 
returns true only when it
+         * transfers the buffers into request.entries. For digest failures, 
duplicate responses, or
+         * other incomplete paths, complete() returns false and this retained 
reference is released here.
+         */
         bufList.retain();
         // if entry has completed don't handle twice
         if (entry.complete(rctx.bookieIndex, rctx.to, bufList)) {
@@ -160,32 +165,50 @@ public class BatchedReadOp extends ReadOpBase implements 
BatchedReadEntryCallbac
             if (isComplete()) {
                 return false;
             }
-            if (!complete.getAndSet(true)) {
-                for (int i = 0; i < bufList.size(); i++) {
-                    ByteBuf buffer = bufList.getBuffer(i);
-                    ByteBuf content;
-                    try {
-                        content = lh.macManager.verifyDigestAndReturnData(eId 
+ i, buffer);
-                    } catch (BKException.BKDigestMatchException e) {
-                        clientCtx.getClientStats().getReadOpDmCounter().inc();
+
+            /*
+             * Verify entries in order. If the first entry has a digest 
mismatch, retry the read from
+             * another replica. If a later entry fails, return the verified 
prefix; batch reads are allowed
+             * to return fewer than maxCount entries.
+             */
+            int verifiedEntries = 0;
+            for (int i = 0; i < bufList.size(); i++) {
+                ByteBuf buffer = bufList.getBuffer(i);
+                try {
+                    lh.macManager.verifyDigestAndReturnData(eId + i, buffer);
+                    verifiedEntries++;
+                } catch (BKException.BKDigestMatchException e) {
+                    clientCtx.getClientStats().getReadOpDmCounter().inc();
+                    if (verifiedEntries == 0) {
                         logErrorAndReattemptRead(bookieIndex, host, "Mac 
mismatch",
                                 BKException.Code.DigestMatchException);
                         return false;
                     }
-                    rc = BKException.Code.OK;
+                    break;
+                }
+            }
+
+            if (complete.compareAndSet(false, true)) {
+                rc = BKException.Code.OK;
+                for (int i = 0; i < verifiedEntries; i++) {
+                    ByteBuf buffer = bufList.getBuffer(i);
                     /*
                      * The length is a long and it is the last field of the 
metadata of an entry.
                      * Consequently, we have to subtract 8 from 
METADATA_LENGTH to get the length.
                      */
-                    LedgerEntryImpl entryImpl =  
LedgerEntryImpl.create(lh.ledgerId, startEntryId + i);
+                    LedgerEntryImpl entryImpl = 
LedgerEntryImpl.create(lh.ledgerId, startEntryId + i);
                     
entryImpl.setLength(buffer.getLong(DigestManager.METADATA_LENGTH - 8));
-                    entryImpl.setEntryBuf(content);
+                    entryImpl.setEntryBuf(buffer);
                     entries.add(entryImpl);
                 }
+                // These buffers are not transferred to LedgerEntryImpl, so 
release them here.
+                for (int i = verifiedEntries; i < bufList.size(); i++) {
+                    bufList.getBuffer(i).release();
+                }
                 writeSet.recycle();
                 return true;
             } else {
-                writeSet.recycle();
+                // Another response completed the request first; 
readEntriesComplete() releases bufList.
                 return false;
             }
         }
@@ -329,4 +352,4 @@ public class BatchedReadOp extends ReadOpBase implements 
BatchedReadEntryCallbac
             return completed;
         }
     }
-}
\ No newline at end of file
+}
diff --git 
a/bookkeeper-server/src/test/java/org/apache/bookkeeper/client/TestBatchedRead.java
 
b/bookkeeper-server/src/test/java/org/apache/bookkeeper/client/TestBatchedRead.java
index 1bb95ed047..505c2a8936 100644
--- 
a/bookkeeper-server/src/test/java/org/apache/bookkeeper/client/TestBatchedRead.java
+++ 
b/bookkeeper-server/src/test/java/org/apache/bookkeeper/client/TestBatchedRead.java
@@ -21,20 +21,29 @@
 package org.apache.bookkeeper.client;
 
 import static org.apache.bookkeeper.common.concurrent.FutureUtils.result;
+import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import io.netty.buffer.ByteBuf;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
 import java.util.Iterator;
 import java.util.List;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import org.apache.bookkeeper.bookie.Bookie;
+import org.apache.bookkeeper.bookie.BookieException;
+import org.apache.bookkeeper.bookie.TestBookieImpl;
 import org.apache.bookkeeper.client.BKException.Code;
 import org.apache.bookkeeper.client.BookKeeper.DigestType;
 import org.apache.bookkeeper.client.api.LedgerEntry;
 import org.apache.bookkeeper.conf.ClientConfiguration;
+import org.apache.bookkeeper.conf.ServerConfiguration;
 import org.apache.bookkeeper.net.BookieId;
 import org.apache.bookkeeper.test.BookKeeperClusterTestCase;
 import org.junit.Test;
@@ -289,4 +298,105 @@ public class TestBatchedRead extends 
BookKeeperClusterTestCase {
         lh.close();
         newBk.close();
     }
+
+    @Test
+    public void testDigestMismatchRetriesNextReplicaAndCompletes() throws 
Exception {
+        ClientConfiguration conf = new ClientConfiguration(baseClientConf)
+                .setUseV2WireProtocol(true)
+                .setReorderReadSequenceEnabled(false)
+                .setMetadataServiceUri(zkUtil.getMetadataServiceUri());
+
+        try (BookKeeperTestClient bk = new BookKeeperTestClient(conf)) {
+            byte[] data = "batch-digest-data".getBytes(StandardCharsets.UTF_8);
+            LedgerHandle writer = bk.createLedger(3, 3, 2, digestType, passwd);
+            writer.addEntry(data);
+            long ledgerId = writer.getId();
+            BookieId corruptReplica = 
writer.getLedgerMetadata().getAllEnsembles().get(0L).get(0);
+            writer.close();
+
+            ServerConfiguration corruptConf = killBookie(corruptReplica);
+            startAndAddBookie(corruptConf, new CorruptReadBookie(corruptConf));
+
+            LedgerHandle reader = bk.openLedger(ledgerId, digestType, passwd);
+            BatchedReadOp readOp = new BatchedReadOp(reader, 
bk.getClientCtx(), 0, 1, 1024, false);
+            readOp.submit();
+
+            Iterator<LedgerEntry> entries = readOp.future().get().iterator();
+            assertTrue(entries.hasNext());
+            LedgerEntry entry = entries.next();
+            assertArrayEquals(data, entry.getEntryBytes());
+            entry.close();
+            assertFalse(entries.hasNext());
+            reader.close();
+        }
+    }
+
+    @Test
+    public void 
testDigestMismatchAfterPartialVerificationReturnsVerifiedPrefix() throws 
Exception {
+        ClientConfiguration conf = new ClientConfiguration(baseClientConf)
+                .setUseV2WireProtocol(true)
+                .setReorderReadSequenceEnabled(false)
+                .setMetadataServiceUri(zkUtil.getMetadataServiceUri());
+
+        try (BookKeeperTestClient bk = new BookKeeperTestClient(conf)) {
+            byte[] entry0 = 
"batch-digest-entry-0".getBytes(StandardCharsets.UTF_8);
+            byte[] entry1 = 
"batch-digest-entry-1".getBytes(StandardCharsets.UTF_8);
+            LedgerHandle writer = bk.createLedger(3, 3, 2, digestType, passwd);
+            writer.addEntry(entry0);
+            writer.addEntry(entry1);
+            long ledgerId = writer.getId();
+            List<BookieId> ensemble = 
writer.getLedgerMetadata().getAllEnsembles().get(0L);
+            BookieId corruptReplica = ensemble.get(0);
+            writer.close();
+
+            CountDownLatch corruptReadLatch = new CountDownLatch(1);
+            ServerConfiguration corruptConf = killBookie(corruptReplica);
+            startAndAddBookie(corruptConf, new CorruptReadBookie(corruptConf, 
1L, corruptReadLatch));
+
+            LedgerHandle reader = bk.openLedger(ledgerId, digestType, passwd);
+            BatchedReadOp readOp = new BatchedReadOp(reader, 
bk.getClientCtx(), 0, 2, 2048, false);
+            readOp.submit();
+
+            assertTrue("corrupt replica did not read the corrupted entry",
+                    corruptReadLatch.await(10, TimeUnit.SECONDS));
+            Iterator<LedgerEntry> entries = readOp.future().get(10, 
TimeUnit.SECONDS).iterator();
+            assertTrue(entries.hasNext());
+            LedgerEntry first = entries.next();
+            assertArrayEquals(entry0, first.getEntryBytes());
+            first.close();
+            assertFalse(entries.hasNext());
+            reader.close();
+        }
+    }
+
+    static class CorruptReadBookie extends TestBookieImpl {
+        private final long corruptEntryId;
+        private final CountDownLatch corruptReadLatch;
+
+        CorruptReadBookie(ServerConfiguration conf) throws Exception {
+            this(conf, -1L, null);
+        }
+
+        CorruptReadBookie(ServerConfiguration conf, long corruptEntryId, 
CountDownLatch corruptReadLatch)
+                throws Exception {
+            super(conf);
+            this.corruptEntryId = corruptEntryId;
+            this.corruptReadLatch = corruptReadLatch;
+        }
+
+        @Override
+        public ByteBuf readEntry(long ledgerId, long entryId)
+                throws IOException, Bookie.NoLedgerException, BookieException {
+            ByteBuf localBuf = super.readEntry(ledgerId, entryId);
+            if (corruptEntryId < 0 || corruptEntryId == entryId) {
+                for (int i = 0; i < localBuf.capacity(); i++) {
+                    localBuf.setByte(i, 0);
+                }
+                if (corruptReadLatch != null) {
+                    corruptReadLatch.countDown();
+                }
+            }
+            return localBuf;
+        }
+    }
 }

Reply via email to