Skip to content

Commit 18a4368

Browse files
authored
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
1 parent 1dc894f commit 18a4368

2 files changed

Lines changed: 146 additions & 13 deletions

File tree

bookkeeper-server/src/main/java/org/apache/bookkeeper/client/BatchedReadOp.java

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,11 @@ public void readEntriesComplete(int rc, long ledgerId, long startEntryId, ByteBu
112112
heardFromHosts.add(rctx.to);
113113
heardFromHostsBitSet.set(rctx.bookieIndex, true);
114114

115+
/*
116+
* Retain the response while this read op handles it. complete() returns true only when it
117+
* transfers the buffers into request.entries. For digest failures, duplicate responses, or
118+
* other incomplete paths, complete() returns false and this retained reference is released here.
119+
*/
115120
bufList.retain();
116121
// if entry has completed don't handle twice
117122
if (entry.complete(rctx.bookieIndex, rctx.to, bufList)) {
@@ -160,32 +165,50 @@ boolean complete(int bookieIndex, BookieId host, final ByteBufList bufList) {
160165
if (isComplete()) {
161166
return false;
162167
}
163-
if (!complete.getAndSet(true)) {
164-
for (int i = 0; i < bufList.size(); i++) {
165-
ByteBuf buffer = bufList.getBuffer(i);
166-
ByteBuf content;
167-
try {
168-
content = lh.macManager.verifyDigestAndReturnData(eId + i, buffer);
169-
} catch (BKException.BKDigestMatchException e) {
170-
clientCtx.getClientStats().getReadOpDmCounter().inc();
168+
169+
/*
170+
* Verify entries in order. If the first entry has a digest mismatch, retry the read from
171+
* another replica. If a later entry fails, return the verified prefix; batch reads are allowed
172+
* to return fewer than maxCount entries.
173+
*/
174+
int verifiedEntries = 0;
175+
for (int i = 0; i < bufList.size(); i++) {
176+
ByteBuf buffer = bufList.getBuffer(i);
177+
try {
178+
lh.macManager.verifyDigestAndReturnData(eId + i, buffer);
179+
verifiedEntries++;
180+
} catch (BKException.BKDigestMatchException e) {
181+
clientCtx.getClientStats().getReadOpDmCounter().inc();
182+
if (verifiedEntries == 0) {
171183
logErrorAndReattemptRead(bookieIndex, host, "Mac mismatch",
172184
BKException.Code.DigestMatchException);
173185
return false;
174186
}
175-
rc = BKException.Code.OK;
187+
break;
188+
}
189+
}
190+
191+
if (complete.compareAndSet(false, true)) {
192+
rc = BKException.Code.OK;
193+
for (int i = 0; i < verifiedEntries; i++) {
194+
ByteBuf buffer = bufList.getBuffer(i);
176195
/*
177196
* The length is a long and it is the last field of the metadata of an entry.
178197
* Consequently, we have to subtract 8 from METADATA_LENGTH to get the length.
179198
*/
180-
LedgerEntryImpl entryImpl = LedgerEntryImpl.create(lh.ledgerId, startEntryId + i);
199+
LedgerEntryImpl entryImpl = LedgerEntryImpl.create(lh.ledgerId, startEntryId + i);
181200
entryImpl.setLength(buffer.getLong(DigestManager.METADATA_LENGTH - 8));
182-
entryImpl.setEntryBuf(content);
201+
entryImpl.setEntryBuf(buffer);
183202
entries.add(entryImpl);
184203
}
204+
// These buffers are not transferred to LedgerEntryImpl, so release them here.
205+
for (int i = verifiedEntries; i < bufList.size(); i++) {
206+
bufList.getBuffer(i).release();
207+
}
185208
writeSet.recycle();
186209
return true;
187210
} else {
188-
writeSet.recycle();
211+
// Another response completed the request first; readEntriesComplete() releases bufList.
189212
return false;
190213
}
191214
}
@@ -329,4 +352,4 @@ boolean complete(int bookieIndex, BookieId host, final ByteBufList bufList) {
329352
return completed;
330353
}
331354
}
332-
}
355+
}

bookkeeper-server/src/test/java/org/apache/bookkeeper/client/TestBatchedRead.java

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,29 @@
2121
package org.apache.bookkeeper.client;
2222

2323
import static org.apache.bookkeeper.common.concurrent.FutureUtils.result;
24+
import static org.junit.Assert.assertArrayEquals;
2425
import static org.junit.Assert.assertEquals;
2526
import static org.junit.Assert.assertFalse;
2627
import static org.junit.Assert.assertNotNull;
2728
import static org.junit.Assert.assertTrue;
2829
import static org.junit.Assert.fail;
2930

31+
import io.netty.buffer.ByteBuf;
32+
import java.io.IOException;
33+
import java.nio.charset.StandardCharsets;
3034
import java.util.Iterator;
3135
import java.util.List;
3236
import java.util.concurrent.CompletableFuture;
3337
import java.util.concurrent.CountDownLatch;
38+
import java.util.concurrent.TimeUnit;
39+
import org.apache.bookkeeper.bookie.Bookie;
40+
import org.apache.bookkeeper.bookie.BookieException;
41+
import org.apache.bookkeeper.bookie.TestBookieImpl;
3442
import org.apache.bookkeeper.client.BKException.Code;
3543
import org.apache.bookkeeper.client.BookKeeper.DigestType;
3644
import org.apache.bookkeeper.client.api.LedgerEntry;
3745
import org.apache.bookkeeper.conf.ClientConfiguration;
46+
import org.apache.bookkeeper.conf.ServerConfiguration;
3847
import org.apache.bookkeeper.net.BookieId;
3948
import org.apache.bookkeeper.test.BookKeeperClusterTestCase;
4049
import org.junit.Test;
@@ -289,4 +298,105 @@ public void testReadFailureWithFailedBookies() throws Exception {
289298
lh.close();
290299
newBk.close();
291300
}
301+
302+
@Test
303+
public void testDigestMismatchRetriesNextReplicaAndCompletes() throws Exception {
304+
ClientConfiguration conf = new ClientConfiguration(baseClientConf)
305+
.setUseV2WireProtocol(true)
306+
.setReorderReadSequenceEnabled(false)
307+
.setMetadataServiceUri(zkUtil.getMetadataServiceUri());
308+
309+
try (BookKeeperTestClient bk = new BookKeeperTestClient(conf)) {
310+
byte[] data = "batch-digest-data".getBytes(StandardCharsets.UTF_8);
311+
LedgerHandle writer = bk.createLedger(3, 3, 2, digestType, passwd);
312+
writer.addEntry(data);
313+
long ledgerId = writer.getId();
314+
BookieId corruptReplica = writer.getLedgerMetadata().getAllEnsembles().get(0L).get(0);
315+
writer.close();
316+
317+
ServerConfiguration corruptConf = killBookie(corruptReplica);
318+
startAndAddBookie(corruptConf, new CorruptReadBookie(corruptConf));
319+
320+
LedgerHandle reader = bk.openLedger(ledgerId, digestType, passwd);
321+
BatchedReadOp readOp = new BatchedReadOp(reader, bk.getClientCtx(), 0, 1, 1024, false);
322+
readOp.submit();
323+
324+
Iterator<LedgerEntry> entries = readOp.future().get().iterator();
325+
assertTrue(entries.hasNext());
326+
LedgerEntry entry = entries.next();
327+
assertArrayEquals(data, entry.getEntryBytes());
328+
entry.close();
329+
assertFalse(entries.hasNext());
330+
reader.close();
331+
}
332+
}
333+
334+
@Test
335+
public void testDigestMismatchAfterPartialVerificationReturnsVerifiedPrefix() throws Exception {
336+
ClientConfiguration conf = new ClientConfiguration(baseClientConf)
337+
.setUseV2WireProtocol(true)
338+
.setReorderReadSequenceEnabled(false)
339+
.setMetadataServiceUri(zkUtil.getMetadataServiceUri());
340+
341+
try (BookKeeperTestClient bk = new BookKeeperTestClient(conf)) {
342+
byte[] entry0 = "batch-digest-entry-0".getBytes(StandardCharsets.UTF_8);
343+
byte[] entry1 = "batch-digest-entry-1".getBytes(StandardCharsets.UTF_8);
344+
LedgerHandle writer = bk.createLedger(3, 3, 2, digestType, passwd);
345+
writer.addEntry(entry0);
346+
writer.addEntry(entry1);
347+
long ledgerId = writer.getId();
348+
List<BookieId> ensemble = writer.getLedgerMetadata().getAllEnsembles().get(0L);
349+
BookieId corruptReplica = ensemble.get(0);
350+
writer.close();
351+
352+
CountDownLatch corruptReadLatch = new CountDownLatch(1);
353+
ServerConfiguration corruptConf = killBookie(corruptReplica);
354+
startAndAddBookie(corruptConf, new CorruptReadBookie(corruptConf, 1L, corruptReadLatch));
355+
356+
LedgerHandle reader = bk.openLedger(ledgerId, digestType, passwd);
357+
BatchedReadOp readOp = new BatchedReadOp(reader, bk.getClientCtx(), 0, 2, 2048, false);
358+
readOp.submit();
359+
360+
assertTrue("corrupt replica did not read the corrupted entry",
361+
corruptReadLatch.await(10, TimeUnit.SECONDS));
362+
Iterator<LedgerEntry> entries = readOp.future().get(10, TimeUnit.SECONDS).iterator();
363+
assertTrue(entries.hasNext());
364+
LedgerEntry first = entries.next();
365+
assertArrayEquals(entry0, first.getEntryBytes());
366+
first.close();
367+
assertFalse(entries.hasNext());
368+
reader.close();
369+
}
370+
}
371+
372+
static class CorruptReadBookie extends TestBookieImpl {
373+
private final long corruptEntryId;
374+
private final CountDownLatch corruptReadLatch;
375+
376+
CorruptReadBookie(ServerConfiguration conf) throws Exception {
377+
this(conf, -1L, null);
378+
}
379+
380+
CorruptReadBookie(ServerConfiguration conf, long corruptEntryId, CountDownLatch corruptReadLatch)
381+
throws Exception {
382+
super(conf);
383+
this.corruptEntryId = corruptEntryId;
384+
this.corruptReadLatch = corruptReadLatch;
385+
}
386+
387+
@Override
388+
public ByteBuf readEntry(long ledgerId, long entryId)
389+
throws IOException, Bookie.NoLedgerException, BookieException {
390+
ByteBuf localBuf = super.readEntry(ledgerId, entryId);
391+
if (corruptEntryId < 0 || corruptEntryId == entryId) {
392+
for (int i = 0; i < localBuf.capacity(); i++) {
393+
localBuf.setByte(i, 0);
394+
}
395+
if (corruptReadLatch != null) {
396+
corruptReadLatch.countDown();
397+
}
398+
}
399+
return localBuf;
400+
}
401+
}
292402
}

0 commit comments

Comments
 (0)