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

StevenLuMT pushed a commit to branch branch-4.17
in repository https://gitbox.apache.org/repos/asf/bookkeeper.git


The following commit(s) were added to refs/heads/branch-4.17 by this push:
     new d33047b127 Revert "Fix read thread blocking in sendResponseAndWait 
causing READ_ENTRY_REQUEST p99 latency spike (#4730)" (#4830)
d33047b127 is described below

commit d33047b12780f82372b9f4f792792def39dd1b37
Author: Hang Chen <[email protected]>
AuthorDate: Tue Jul 7 20:18:57 2026 +0800

    Revert "Fix read thread blocking in sendResponseAndWait causing 
READ_ENTRY_REQUEST p99 latency spike (#4730)" (#4830)
    
    This reverts commit 8664dd962935dc23d40218f1bf228661776580fd.
    
    (cherry picked from commit 15a961b67bf0fb030eac0647317c53f3a889ca72)
---
 .../bookkeeper/conf/ServerConfiguration.java       |   8 +-
 .../bookkeeper/proto/PacketProcessorBase.java      |  51 ++----
 .../bookkeeper/proto/ReadEntryProcessorTest.java   | 178 ---------------------
 3 files changed, 17 insertions(+), 220 deletions(-)

diff --git 
a/bookkeeper-server/src/main/java/org/apache/bookkeeper/conf/ServerConfiguration.java
 
b/bookkeeper-server/src/main/java/org/apache/bookkeeper/conf/ServerConfiguration.java
index 4bf0ded738..4d9eabb241 100644
--- 
a/bookkeeper-server/src/main/java/org/apache/bookkeeper/conf/ServerConfiguration.java
+++ 
b/bookkeeper-server/src/main/java/org/apache/bookkeeper/conf/ServerConfiguration.java
@@ -1075,16 +1075,10 @@ public class ServerConfiguration extends 
AbstractConfiguration<ServerConfigurati
     /**
      * Get max number of reads in progress. 0 == unlimited.
      *
-     * <p>This limit bounds the memory used by read responses that have been 
read from storage
-     * but not yet flushed to the network. Since read response writes are 
non-blocking,
-     * without this limit a slow consumer could cause unbounded memory growth.
-     * The default value of 10000 provides a reasonable balance between 
throughput and memory usage.
-     * Tune based on your average entry size: memoryBudget / avgEntrySize.
-     *
      * @return Max number of reads in progress.
      */
     public int getMaxReadsInProgressLimit() {
-        return this.getInt(MAX_READS_IN_PROGRESS_LIMIT, 10000);
+        return this.getInt(MAX_READS_IN_PROGRESS_LIMIT, 0);
     }
 
     /**
diff --git 
a/bookkeeper-server/src/main/java/org/apache/bookkeeper/proto/PacketProcessorBase.java
 
b/bookkeeper-server/src/main/java/org/apache/bookkeeper/proto/PacketProcessorBase.java
index 5ecda0355c..889d3790d2 100644
--- 
a/bookkeeper-server/src/main/java/org/apache/bookkeeper/proto/PacketProcessorBase.java
+++ 
b/bookkeeper-server/src/main/java/org/apache/bookkeeper/proto/PacketProcessorBase.java
@@ -19,8 +19,8 @@ package org.apache.bookkeeper.proto;
 
 import io.netty.channel.Channel;
 import io.netty.channel.ChannelFuture;
-import io.netty.channel.ChannelFutureListener;
 import io.netty.channel.ChannelPromise;
+import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeUnit;
 import org.apache.bookkeeper.proto.BookieProtocol.Request;
 import org.apache.bookkeeper.stats.OpStatsLogger;
@@ -74,12 +74,10 @@ abstract class PacketProcessorBase<T extends Request> 
implements Runnable {
     protected void sendReadReqResponse(int rc, Object response, OpStatsLogger 
statsLogger, boolean throttle) {
         if (throttle) {
             sendResponseAndWait(rc, response, statsLogger);
-            // onReadRequestFinish is called asynchronously in the 
ChannelFutureListener
-            // inside sendResponseAndWait to maintain throttling without 
blocking the thread.
         } else {
             sendResponse(rc, response, statsLogger);
-            requestProcessor.onReadRequestFinish();
         }
+        requestProcessor.onReadRequestFinish();
     }
 
     protected void sendResponse(int rc, Object response, OpStatsLogger 
statsLogger) {
@@ -152,44 +150,27 @@ abstract class PacketProcessorBase<T extends Request> 
implements Runnable {
     }
 
     /**
-     * Write on the channel and notify completion via a listener.
+     * Write on the channel and wait until the write is completed.
      *
-     * <p>This provides auto-throttling by holding the read semaphore until 
the write completes,
-     * without blocking the read thread pool thread. The read thread is freed 
immediately to
-     * process other requests, while the semaphore prevents unbounded read 
concurrency.
+     * <p>That will make the thread to get blocked until we're able to
+     * write everything on the TCP stack, providing auto-throttling
+     * and avoiding using too much memory when handling read-requests.
      */
     protected void sendResponseAndWait(int rc, Object response, OpStatsLogger 
statsLogger) {
-        // Capture fields before the processor may be recycled after this 
method returns.
-        final long capturedEnqueueNanos = this.enqueueNanos;
-        final BookieRequestProcessor processor = this.requestProcessor;
         try {
             Channel channel = requestHandler.ctx().channel();
             ChannelFuture future = channel.writeAndFlush(response);
-            future.addListener((ChannelFutureListener) f -> {
-                if (!f.isSuccess() && logger.isDebugEnabled()) {
-                    logger.debug("Netty channel write exception. ", f.cause());
-                }
-                if (BookieProtocol.EOK == rc) {
-                    statsLogger.registerSuccessfulEvent(
-                            MathUtils.elapsedNanos(capturedEnqueueNanos), 
TimeUnit.NANOSECONDS);
-                } else {
-                    statsLogger.registerFailedEvent(
-                            MathUtils.elapsedNanos(capturedEnqueueNanos), 
TimeUnit.NANOSECONDS);
-                }
-                processor.onReadRequestFinish();
-            });
-        } catch (Exception e) {
-            if (logger.isDebugEnabled()) {
-                logger.debug("Netty channel write exception. ", e);
+            if (!channel.eventLoop().inEventLoop()) {
+                future.get();
             }
-            if (BookieProtocol.EOK == rc) {
-                statsLogger.registerSuccessfulEvent(
-                        MathUtils.elapsedNanos(capturedEnqueueNanos), 
TimeUnit.NANOSECONDS);
-            } else {
-                statsLogger.registerFailedEvent(
-                        MathUtils.elapsedNanos(capturedEnqueueNanos), 
TimeUnit.NANOSECONDS);
-            }
-            processor.onReadRequestFinish();
+        } catch (ExecutionException | InterruptedException e) {
+            logger.debug("Netty channel write exception. ", e);
+            return;
+        }
+        if (BookieProtocol.EOK == rc) {
+            
statsLogger.registerSuccessfulEvent(MathUtils.elapsedNanos(enqueueNanos), 
TimeUnit.NANOSECONDS);
+        } else {
+            
statsLogger.registerFailedEvent(MathUtils.elapsedNanos(enqueueNanos), 
TimeUnit.NANOSECONDS);
         }
     }
 
diff --git 
a/bookkeeper-server/src/test/java/org/apache/bookkeeper/proto/ReadEntryProcessorTest.java
 
b/bookkeeper-server/src/test/java/org/apache/bookkeeper/proto/ReadEntryProcessorTest.java
index 2cdf744f24..251f900c09 100644
--- 
a/bookkeeper-server/src/test/java/org/apache/bookkeeper/proto/ReadEntryProcessorTest.java
+++ 
b/bookkeeper-server/src/test/java/org/apache/bookkeeper/proto/ReadEntryProcessorTest.java
@@ -19,13 +19,11 @@
 package org.apache.bookkeeper.proto;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -40,12 +38,10 @@ import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
-import java.util.concurrent.Semaphore;
 import java.util.concurrent.atomic.AtomicReference;
 import org.apache.bookkeeper.bookie.Bookie;
 import org.apache.bookkeeper.bookie.BookieException;
 import org.apache.bookkeeper.common.concurrent.FutureUtils;
-import org.apache.bookkeeper.conf.ServerConfiguration;
 import org.apache.bookkeeper.proto.BookieProtocol.ReadRequest;
 import org.apache.bookkeeper.proto.BookieProtocol.Response;
 import org.apache.bookkeeper.stats.NullStatsLogger;
@@ -199,178 +195,4 @@ public class ReadEntryProcessorTest {
         assertEquals(BookieProtocol.READENTRY, response.getOpCode());
         assertEquals(BookieProtocol.EOK, response.getErrorCode());
     }
-
-    /**
-     * Test that when throttleReadResponses=true and the caller is not in the 
Netty event loop,
-     * the read thread is not blocked by the write. onReadRequestFinish() 
should only be called
-     * after the write future completes, preserving throttling without 
blocking the thread.
-     */
-    @Test
-    public void testThrottledReadNonBlockingOnSuccess() throws Exception {
-        // Setup event loop to simulate read worker thread (not event loop 
thread)
-        EventLoop eventLoop = mock(EventLoop.class);
-        when(eventLoop.inEventLoop()).thenReturn(false);
-        doAnswer(inv -> {
-            ((Runnable) inv.getArgument(0)).run();
-            return null;
-        }).when(eventLoop).execute(any(Runnable.class));
-        when(channel.eventLoop()).thenReturn(eventLoop);
-
-        // Use a controllable promise so we can verify deferred behavior
-        DefaultChannelPromise writeFuture = new DefaultChannelPromise(channel);
-        doAnswer(inv -> 
writeFuture).when(channel).writeAndFlush(any(Response.class));
-
-        long ledgerId = System.currentTimeMillis();
-        ReadRequest request = ReadRequest.create(
-                BookieProtocol.CURRENT_PROTOCOL_VERSION, ledgerId, 1, (short) 
0, new byte[]{});
-        ReadEntryProcessor processor = ReadEntryProcessor.create(
-                request, requestHandler, requestProcessor, null, true /* 
throttle */);
-
-        // run() should return immediately without blocking on the write
-        processor.run();
-
-        // Write should have been issued
-        verify(channel, times(1)).writeAndFlush(any(Response.class));
-        // But onReadRequestFinish should NOT have been called yet — write not 
completed
-        verify(requestProcessor, never()).onReadRequestFinish();
-
-        // Complete the write
-        writeFuture.setSuccess();
-
-        // Now onReadRequestFinish should have been called
-        verify(requestProcessor, times(1)).onReadRequestFinish();
-    }
-
-    /**
-     * Test that onReadRequestFinish() is still called even when the write 
fails,
-     * so the read semaphore is always released.
-     */
-    @Test
-    public void testThrottledReadNonBlockingOnWriteFailure() throws Exception {
-        EventLoop eventLoop = mock(EventLoop.class);
-        when(eventLoop.inEventLoop()).thenReturn(false);
-        doAnswer(inv -> {
-            ((Runnable) inv.getArgument(0)).run();
-            return null;
-        }).when(eventLoop).execute(any(Runnable.class));
-        when(channel.eventLoop()).thenReturn(eventLoop);
-
-        DefaultChannelPromise writeFuture = new DefaultChannelPromise(channel);
-        doAnswer(inv -> 
writeFuture).when(channel).writeAndFlush(any(Response.class));
-
-        long ledgerId = System.currentTimeMillis();
-        ReadRequest request = ReadRequest.create(
-                BookieProtocol.CURRENT_PROTOCOL_VERSION, ledgerId, 1, (short) 
0, new byte[]{});
-        ReadEntryProcessor processor = ReadEntryProcessor.create(
-                request, requestHandler, requestProcessor, null, true /* 
throttle */);
-
-        processor.run();
-
-        verify(channel, times(1)).writeAndFlush(any(Response.class));
-        verify(requestProcessor, never()).onReadRequestFinish();
-
-        // Fail the write
-        writeFuture.setFailure(new IOException("channel write failed"));
-
-        // onReadRequestFinish must still be called to release the read 
semaphore
-        verify(requestProcessor, times(1)).onReadRequestFinish();
-    }
-
-    /**
-     * Test that when throttleReadResponses=false, onReadRequestFinish() is 
called
-     * synchronously before run() returns.
-     */
-    @Test
-    public void testNonThrottledReadCallsOnFinishSynchronously() throws 
Exception {
-        // sendResponse (non-throttle path) uses channel.isActive() and 
two-arg writeAndFlush
-        when(channel.isActive()).thenReturn(true);
-        when(channel.writeAndFlush(any(), 
any(ChannelPromise.class))).thenReturn(mock(ChannelPromise.class));
-
-        long ledgerId = System.currentTimeMillis();
-        ReadRequest request = ReadRequest.create(
-                BookieProtocol.CURRENT_PROTOCOL_VERSION, ledgerId, 1, (short) 
0, new byte[]{});
-        ReadEntryProcessor processor = ReadEntryProcessor.create(
-                request, requestHandler, requestProcessor, null, false /* no 
throttle */);
-
-        processor.run();
-
-        verify(channel, times(1)).writeAndFlush(any(), 
any(ChannelPromise.class));
-        // onReadRequestFinish should have been called synchronously
-        verify(requestProcessor, times(1)).onReadRequestFinish();
-    }
-
-    /**
-     * Verify that maxReadsInProgressLimit defaults to 10000 (enabled),
-     * ensuring non-blocking read response writes are bounded by default.
-     */
-    @Test
-    public void testDefaultMaxReadsInProgressLimitIsEnabled() {
-        ServerConfiguration conf = new ServerConfiguration();
-        assertEquals("maxReadsInProgressLimit should default to 10000",
-                10000, conf.getMaxReadsInProgressLimit());
-    }
-
-    /**
-     * Test that the read semaphore is held from request creation until the 
write future completes,
-     * not released when the read thread returns. This ensures that 
maxReadsInProgressLimit correctly
-     * bounds the number of read responses buffered in memory, even though the 
read thread is
-     * non-blocking.
-     */
-    @Test
-    public void testThrottledReadHoldsSemaphoreUntilWriteCompletes() throws 
Exception {
-        // Simulate maxReadsInProgressLimit=1 with a real semaphore
-        Semaphore readsSemaphore = new Semaphore(1);
-
-        doAnswer(inv -> {
-            readsSemaphore.acquireUninterruptibly();
-            return null;
-        }).when(requestProcessor).onReadRequestStart(any(Channel.class));
-        doAnswer(inv -> {
-            readsSemaphore.release();
-            return null;
-        }).when(requestProcessor).onReadRequestFinish();
-
-        // Setup non-event-loop thread
-        EventLoop eventLoop = mock(EventLoop.class);
-        when(eventLoop.inEventLoop()).thenReturn(false);
-        doAnswer(inv -> {
-            ((Runnable) inv.getArgument(0)).run();
-            return null;
-        }).when(eventLoop).execute(any(Runnable.class));
-        when(channel.eventLoop()).thenReturn(eventLoop);
-
-        // Controllable write future
-        DefaultChannelPromise writeFuture = new DefaultChannelPromise(channel);
-        doAnswer(inv -> 
writeFuture).when(channel).writeAndFlush(any(Response.class));
-
-        long ledgerId = System.currentTimeMillis();
-        ReadRequest request = ReadRequest.create(
-                BookieProtocol.CURRENT_PROTOCOL_VERSION, ledgerId, 1, (short) 
0, new byte[]{});
-
-        // create() calls onReadRequestStart → semaphore acquired
-        ReadEntryProcessor processor = ReadEntryProcessor.create(
-                request, requestHandler, requestProcessor, null, true /* 
throttle */);
-
-        // Semaphore should be acquired (1 permit used)
-        assertEquals("semaphore should have 0 permits after read started",
-                0, readsSemaphore.availablePermits());
-
-        // Run the processor — thread returns immediately (non-blocking)
-        processor.run();
-
-        // Semaphore should STILL be held (write not completed)
-        assertEquals("semaphore should still have 0 permits while write is in 
progress",
-                0, readsSemaphore.availablePermits());
-
-        // A second read would be unable to acquire the semaphore
-        assertFalse("second read should not be able to acquire semaphore",
-                readsSemaphore.tryAcquire());
-
-        // Complete the write
-        writeFuture.setSuccess();
-
-        // Now semaphore should be released — a new read can proceed
-        assertEquals("semaphore should have 1 permit after write completes",
-                1, readsSemaphore.availablePermits());
-    }
 }

Reply via email to