Skip to content

Commit 15a961b

Browse files
authored
Revert "Fix read thread blocking in sendResponseAndWait causing READ_ENTRY_REQUEST p99 latency spike (#4730)" (#4830)
This reverts commit 8664dd9.
1 parent 82df77c commit 15a961b

3 files changed

Lines changed: 17 additions & 218 deletions

File tree

bookkeeper-server/src/main/java/org/apache/bookkeeper/conf/ServerConfiguration.java

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1078,16 +1078,10 @@ public ServerConfiguration setMaxAddsInProgressLimit(int value) {
10781078
/**
10791079
* Get max number of reads in progress. 0 == unlimited.
10801080
*
1081-
* <p>This limit bounds the memory used by read responses that have been read from storage
1082-
* but not yet flushed to the network. Since read response writes are non-blocking,
1083-
* without this limit a slow consumer could cause unbounded memory growth.
1084-
* The default value of 10000 provides a reasonable balance between throughput and memory usage.
1085-
* Tune based on your average entry size: memoryBudget / avgEntrySize.
1086-
*
10871081
* @return Max number of reads in progress.
10881082
*/
10891083
public int getMaxReadsInProgressLimit() {
1090-
return this.getInt(MAX_READS_IN_PROGRESS_LIMIT, 10000);
1084+
return this.getInt(MAX_READS_IN_PROGRESS_LIMIT, 0);
10911085
}
10921086

10931087
/**

bookkeeper-server/src/main/java/org/apache/bookkeeper/proto/PacketProcessorBase.java

Lines changed: 16 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@
1919

2020
import io.netty.channel.Channel;
2121
import io.netty.channel.ChannelFuture;
22-
import io.netty.channel.ChannelFutureListener;
2322
import io.netty.channel.ChannelPromise;
23+
import java.util.concurrent.ExecutionException;
2424
import java.util.concurrent.TimeUnit;
2525
import lombok.CustomLog;
2626
import org.apache.bookkeeper.common.util.MathUtils;
@@ -73,12 +73,10 @@ protected void sendWriteReqResponse(int rc, Object response, OpStatsLogger stats
7373
protected void sendReadReqResponse(int rc, Object response, OpStatsLogger statsLogger, boolean throttle) {
7474
if (throttle) {
7575
sendResponseAndWait(rc, response, statsLogger);
76-
// onReadRequestFinish is called asynchronously in the ChannelFutureListener
77-
// inside sendResponseAndWait to maintain throttling without blocking the thread.
7876
} else {
7977
sendResponse(rc, response, statsLogger);
80-
requestProcessor.onReadRequestFinish();
8178
}
79+
requestProcessor.onReadRequestFinish();
8280
}
8381

8482
protected void sendResponse(int rc, Object response, OpStatsLogger statsLogger) {
@@ -146,42 +144,27 @@ protected void sendResponse(int rc, Object response, OpStatsLogger statsLogger)
146144
}
147145

148146
/**
149-
* Write on the channel and notify completion via a listener.
147+
* Write on the channel and wait until the write is completed.
150148
*
151-
* <p>This provides auto-throttling by holding the read semaphore until the write completes,
152-
* without blocking the read thread pool thread. The read thread is freed immediately to
153-
* process other requests, while the semaphore prevents unbounded read concurrency.
149+
* <p>That will make the thread to get blocked until we're able to
150+
* write everything on the TCP stack, providing auto-throttling
151+
* and avoiding using too much memory when handling read-requests.
154152
*/
155153
protected void sendResponseAndWait(int rc, Object response, OpStatsLogger statsLogger) {
156-
// Capture fields before the processor may be recycled after this method returns.
157-
final long capturedEnqueueNanos = this.enqueueNanos;
158-
final BookieRequestProcessor processor = this.requestProcessor;
159154
try {
160155
Channel channel = requestHandler.ctx().channel();
161156
ChannelFuture future = channel.writeAndFlush(response);
162-
future.addListener((ChannelFutureListener) f -> {
163-
if (!f.isSuccess()) {
164-
log.debug().exception(f.cause()).log("Netty channel write exception");
165-
}
166-
if (BookieProtocol.EOK == rc) {
167-
statsLogger.registerSuccessfulEvent(
168-
MathUtils.elapsedNanos(capturedEnqueueNanos), TimeUnit.NANOSECONDS);
169-
} else {
170-
statsLogger.registerFailedEvent(
171-
MathUtils.elapsedNanos(capturedEnqueueNanos), TimeUnit.NANOSECONDS);
172-
}
173-
processor.onReadRequestFinish();
174-
});
175-
} catch (Exception e) {
176-
log.debug().exception(e).log("Netty channel write exception");
177-
if (BookieProtocol.EOK == rc) {
178-
statsLogger.registerSuccessfulEvent(
179-
MathUtils.elapsedNanos(capturedEnqueueNanos), TimeUnit.NANOSECONDS);
180-
} else {
181-
statsLogger.registerFailedEvent(
182-
MathUtils.elapsedNanos(capturedEnqueueNanos), TimeUnit.NANOSECONDS);
157+
if (!channel.eventLoop().inEventLoop()) {
158+
future.get();
183159
}
184-
processor.onReadRequestFinish();
160+
} catch (ExecutionException | InterruptedException e) {
161+
log.debug().exception(e).log("Netty channel write exception");
162+
return;
163+
}
164+
if (BookieProtocol.EOK == rc) {
165+
statsLogger.registerSuccessfulEvent(MathUtils.elapsedNanos(enqueueNanos), TimeUnit.NANOSECONDS);
166+
} else {
167+
statsLogger.registerFailedEvent(MathUtils.elapsedNanos(enqueueNanos), TimeUnit.NANOSECONDS);
185168
}
186169
}
187170

bookkeeper-server/src/test/java/org/apache/bookkeeper/proto/ReadEntryProcessorTest.java

Lines changed: 0 additions & 178 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,12 @@
1919
package org.apache.bookkeeper.proto;
2020

2121
import static org.junit.Assert.assertEquals;
22-
import static org.junit.Assert.assertFalse;
2322
import static org.junit.Assert.assertTrue;
2423
import static org.mockito.ArgumentMatchers.any;
2524
import static org.mockito.ArgumentMatchers.anyLong;
2625
import static org.mockito.Mockito.RETURNS_SELF;
2726
import static org.mockito.Mockito.doAnswer;
2827
import static org.mockito.Mockito.mock;
29-
import static org.mockito.Mockito.never;
3028
import static org.mockito.Mockito.times;
3129
import static org.mockito.Mockito.verify;
3230
import static org.mockito.Mockito.when;
@@ -41,12 +39,10 @@
4139
import java.util.concurrent.CountDownLatch;
4240
import java.util.concurrent.ExecutorService;
4341
import java.util.concurrent.Executors;
44-
import java.util.concurrent.Semaphore;
4542
import java.util.concurrent.atomic.AtomicReference;
4643
import org.apache.bookkeeper.bookie.Bookie;
4744
import org.apache.bookkeeper.bookie.BookieException;
4845
import org.apache.bookkeeper.common.concurrent.FutureUtils;
49-
import org.apache.bookkeeper.conf.ServerConfiguration;
5046
import org.apache.bookkeeper.proto.BookieProtocol.ReadRequest;
5147
import org.apache.bookkeeper.proto.BookieProtocol.Response;
5248
import org.apache.bookkeeper.stats.NullStatsLogger;
@@ -201,178 +197,4 @@ public void testNonFenceRequest() throws Exception {
201197
assertEquals(BookieProtocol.READENTRY, response.getOpCode());
202198
assertEquals(BookieProtocol.EOK, response.getErrorCode());
203199
}
204-
205-
/**
206-
* Test that when throttleReadResponses=true and the caller is not in the Netty event loop,
207-
* the read thread is not blocked by the write. onReadRequestFinish() should only be called
208-
* after the write future completes, preserving throttling without blocking the thread.
209-
*/
210-
@Test
211-
public void testThrottledReadNonBlockingOnSuccess() throws Exception {
212-
// Setup event loop to simulate read worker thread (not event loop thread)
213-
EventLoop eventLoop = mock(EventLoop.class);
214-
when(eventLoop.inEventLoop()).thenReturn(false);
215-
doAnswer(inv -> {
216-
((Runnable) inv.getArgument(0)).run();
217-
return null;
218-
}).when(eventLoop).execute(any(Runnable.class));
219-
when(channel.eventLoop()).thenReturn(eventLoop);
220-
221-
// Use a controllable promise so we can verify deferred behavior
222-
DefaultChannelPromise writeFuture = new DefaultChannelPromise(channel);
223-
doAnswer(inv -> writeFuture).when(channel).writeAndFlush(any(Response.class));
224-
225-
long ledgerId = System.currentTimeMillis();
226-
ReadRequest request = ReadRequest.create(
227-
BookieProtocol.CURRENT_PROTOCOL_VERSION, ledgerId, 1, (short) 0, new byte[]{});
228-
ReadEntryProcessor processor = ReadEntryProcessor.create(
229-
request, requestHandler, requestProcessor, null, true /* throttle */);
230-
231-
// run() should return immediately without blocking on the write
232-
processor.run();
233-
234-
// Write should have been issued
235-
verify(channel, times(1)).writeAndFlush(any(Response.class));
236-
// But onReadRequestFinish should NOT have been called yet — write not completed
237-
verify(requestProcessor, never()).onReadRequestFinish();
238-
239-
// Complete the write
240-
writeFuture.setSuccess();
241-
242-
// Now onReadRequestFinish should have been called
243-
verify(requestProcessor, times(1)).onReadRequestFinish();
244-
}
245-
246-
/**
247-
* Test that onReadRequestFinish() is still called even when the write fails,
248-
* so the read semaphore is always released.
249-
*/
250-
@Test
251-
public void testThrottledReadNonBlockingOnWriteFailure() throws Exception {
252-
EventLoop eventLoop = mock(EventLoop.class);
253-
when(eventLoop.inEventLoop()).thenReturn(false);
254-
doAnswer(inv -> {
255-
((Runnable) inv.getArgument(0)).run();
256-
return null;
257-
}).when(eventLoop).execute(any(Runnable.class));
258-
when(channel.eventLoop()).thenReturn(eventLoop);
259-
260-
DefaultChannelPromise writeFuture = new DefaultChannelPromise(channel);
261-
doAnswer(inv -> writeFuture).when(channel).writeAndFlush(any(Response.class));
262-
263-
long ledgerId = System.currentTimeMillis();
264-
ReadRequest request = ReadRequest.create(
265-
BookieProtocol.CURRENT_PROTOCOL_VERSION, ledgerId, 1, (short) 0, new byte[]{});
266-
ReadEntryProcessor processor = ReadEntryProcessor.create(
267-
request, requestHandler, requestProcessor, null, true /* throttle */);
268-
269-
processor.run();
270-
271-
verify(channel, times(1)).writeAndFlush(any(Response.class));
272-
verify(requestProcessor, never()).onReadRequestFinish();
273-
274-
// Fail the write
275-
writeFuture.setFailure(new IOException("channel write failed"));
276-
277-
// onReadRequestFinish must still be called to release the read semaphore
278-
verify(requestProcessor, times(1)).onReadRequestFinish();
279-
}
280-
281-
/**
282-
* Test that when throttleReadResponses=false, onReadRequestFinish() is called
283-
* synchronously before run() returns.
284-
*/
285-
@Test
286-
public void testNonThrottledReadCallsOnFinishSynchronously() throws Exception {
287-
// sendResponse (non-throttle path) uses channel.isActive() and two-arg writeAndFlush
288-
when(channel.isActive()).thenReturn(true);
289-
when(channel.writeAndFlush(any(), any(ChannelPromise.class))).thenReturn(mock(ChannelPromise.class));
290-
291-
long ledgerId = System.currentTimeMillis();
292-
ReadRequest request = ReadRequest.create(
293-
BookieProtocol.CURRENT_PROTOCOL_VERSION, ledgerId, 1, (short) 0, new byte[]{});
294-
ReadEntryProcessor processor = ReadEntryProcessor.create(
295-
request, requestHandler, requestProcessor, null, false /* no throttle */);
296-
297-
processor.run();
298-
299-
verify(channel, times(1)).writeAndFlush(any(), any(ChannelPromise.class));
300-
// onReadRequestFinish should have been called synchronously
301-
verify(requestProcessor, times(1)).onReadRequestFinish();
302-
}
303-
304-
/**
305-
* Verify that maxReadsInProgressLimit defaults to 10000 (enabled),
306-
* ensuring non-blocking read response writes are bounded by default.
307-
*/
308-
@Test
309-
public void testDefaultMaxReadsInProgressLimitIsEnabled() {
310-
ServerConfiguration conf = new ServerConfiguration();
311-
assertEquals("maxReadsInProgressLimit should default to 10000",
312-
10000, conf.getMaxReadsInProgressLimit());
313-
}
314-
315-
/**
316-
* Test that the read semaphore is held from request creation until the write future completes,
317-
* not released when the read thread returns. This ensures that maxReadsInProgressLimit correctly
318-
* bounds the number of read responses buffered in memory, even though the read thread is
319-
* non-blocking.
320-
*/
321-
@Test
322-
public void testThrottledReadHoldsSemaphoreUntilWriteCompletes() throws Exception {
323-
// Simulate maxReadsInProgressLimit=1 with a real semaphore
324-
Semaphore readsSemaphore = new Semaphore(1);
325-
326-
doAnswer(inv -> {
327-
readsSemaphore.acquireUninterruptibly();
328-
return null;
329-
}).when(requestProcessor).onReadRequestStart(any(Channel.class));
330-
doAnswer(inv -> {
331-
readsSemaphore.release();
332-
return null;
333-
}).when(requestProcessor).onReadRequestFinish();
334-
335-
// Setup non-event-loop thread
336-
EventLoop eventLoop = mock(EventLoop.class);
337-
when(eventLoop.inEventLoop()).thenReturn(false);
338-
doAnswer(inv -> {
339-
((Runnable) inv.getArgument(0)).run();
340-
return null;
341-
}).when(eventLoop).execute(any(Runnable.class));
342-
when(channel.eventLoop()).thenReturn(eventLoop);
343-
344-
// Controllable write future
345-
DefaultChannelPromise writeFuture = new DefaultChannelPromise(channel);
346-
doAnswer(inv -> writeFuture).when(channel).writeAndFlush(any(Response.class));
347-
348-
long ledgerId = System.currentTimeMillis();
349-
ReadRequest request = ReadRequest.create(
350-
BookieProtocol.CURRENT_PROTOCOL_VERSION, ledgerId, 1, (short) 0, new byte[]{});
351-
352-
// create() calls onReadRequestStart → semaphore acquired
353-
ReadEntryProcessor processor = ReadEntryProcessor.create(
354-
request, requestHandler, requestProcessor, null, true /* throttle */);
355-
356-
// Semaphore should be acquired (1 permit used)
357-
assertEquals("semaphore should have 0 permits after read started",
358-
0, readsSemaphore.availablePermits());
359-
360-
// Run the processor — thread returns immediately (non-blocking)
361-
processor.run();
362-
363-
// Semaphore should STILL be held (write not completed)
364-
assertEquals("semaphore should still have 0 permits while write is in progress",
365-
0, readsSemaphore.availablePermits());
366-
367-
// A second read would be unable to acquire the semaphore
368-
assertFalse("second read should not be able to acquire semaphore",
369-
readsSemaphore.tryAcquire());
370-
371-
// Complete the write
372-
writeFuture.setSuccess();
373-
374-
// Now semaphore should be released — a new read can proceed
375-
assertEquals("semaphore should have 1 permit after write completes",
376-
1, readsSemaphore.availablePermits());
377-
}
378200
}

0 commit comments

Comments
 (0)