|
19 | 19 | package org.apache.bookkeeper.proto; |
20 | 20 |
|
21 | 21 | import static org.junit.Assert.assertEquals; |
22 | | -import static org.junit.Assert.assertFalse; |
23 | 22 | import static org.junit.Assert.assertTrue; |
24 | 23 | import static org.mockito.ArgumentMatchers.any; |
25 | 24 | import static org.mockito.ArgumentMatchers.anyLong; |
26 | 25 | import static org.mockito.Mockito.RETURNS_SELF; |
27 | 26 | import static org.mockito.Mockito.doAnswer; |
28 | 27 | import static org.mockito.Mockito.mock; |
29 | | -import static org.mockito.Mockito.never; |
30 | 28 | import static org.mockito.Mockito.times; |
31 | 29 | import static org.mockito.Mockito.verify; |
32 | 30 | import static org.mockito.Mockito.when; |
|
41 | 39 | import java.util.concurrent.CountDownLatch; |
42 | 40 | import java.util.concurrent.ExecutorService; |
43 | 41 | import java.util.concurrent.Executors; |
44 | | -import java.util.concurrent.Semaphore; |
45 | 42 | import java.util.concurrent.atomic.AtomicReference; |
46 | 43 | import org.apache.bookkeeper.bookie.Bookie; |
47 | 44 | import org.apache.bookkeeper.bookie.BookieException; |
48 | 45 | import org.apache.bookkeeper.common.concurrent.FutureUtils; |
49 | | -import org.apache.bookkeeper.conf.ServerConfiguration; |
50 | 46 | import org.apache.bookkeeper.proto.BookieProtocol.ReadRequest; |
51 | 47 | import org.apache.bookkeeper.proto.BookieProtocol.Response; |
52 | 48 | import org.apache.bookkeeper.stats.NullStatsLogger; |
@@ -201,178 +197,4 @@ public void testNonFenceRequest() throws Exception { |
201 | 197 | assertEquals(BookieProtocol.READENTRY, response.getOpCode()); |
202 | 198 | assertEquals(BookieProtocol.EOK, response.getErrorCode()); |
203 | 199 | } |
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 | | - } |
378 | 200 | } |
0 commit comments