|
61 | 61 | import java.time.Duration; |
62 | 62 | import java.util.concurrent.CountDownLatch; |
63 | 63 | import java.util.concurrent.ExecutionException; |
| 64 | +import java.util.concurrent.ThreadPoolExecutor; |
64 | 65 | import java.util.concurrent.TimeUnit; |
65 | 66 | import java.util.concurrent.atomic.AtomicBoolean; |
66 | 67 | import java.util.concurrent.atomic.AtomicLong; |
|
71 | 72 | import static java.util.concurrent.TimeUnit.SECONDS; |
72 | 73 | import static org.asynchttpclient.Dsl.asyncHttpClient; |
73 | 74 | import static org.asynchttpclient.Dsl.config; |
| 75 | +import static org.asynchttpclient.OffloadedBodyReadTestSupport.BlockingInputStream; |
| 76 | +import static org.asynchttpclient.OffloadedBodyReadTestSupport.CloseProbeInputStream; |
| 77 | +import static org.asynchttpclient.OffloadedBodyReadTestSupport.awaitExecutorState; |
74 | 78 | import static org.junit.jupiter.api.Assertions.assertEquals; |
75 | 79 | import static org.junit.jupiter.api.Assertions.assertFalse; |
76 | 80 | import static org.junit.jupiter.api.Assertions.assertInstanceOf; |
@@ -362,6 +366,74 @@ public void inputStreamBodyReadsOffEventLoopOverHttp2() throws Exception { |
362 | 366 | assertFalse(is.readOnEventLoop.get(), "InputStream.read must not run on an event-loop thread"); |
363 | 367 | } |
364 | 368 |
|
| 369 | + @Test |
| 370 | + public void blockedInputStreamDoesNotStallSiblingStreamOverHttp2() throws Exception { |
| 371 | + startServer(-1); |
| 372 | + byte[] payload = deterministicPayload(SMALL_SIZE); |
| 373 | + BlockingInputStream bodyStream = new BlockingInputStream(payload); |
| 374 | + |
| 375 | + try (AsyncHttpClient client = http2Client()) { |
| 376 | + ListenableFuture<Response> blockedUpload = client.preparePost(httpsUrl("/blocked")) |
| 377 | + .setBody(new InputStreamBodyGenerator(bodyStream, payload.length)) |
| 378 | + .execute(); |
| 379 | + assertTrue(bodyStream.readStarted.await(5, SECONDS), "blocking body read should start"); |
| 380 | + |
| 381 | + Response sibling = client.prepareGet(httpsUrl("/sibling")) |
| 382 | + .execute() |
| 383 | + .get(5, SECONDS); |
| 384 | + assertEquals(200, sibling.getStatusCode(), |
| 385 | + "a sibling stream should complete while the upload read is blocked"); |
| 386 | + |
| 387 | + bodyStream.releaseRead.countDown(); |
| 388 | + assertEchoed(blockedUpload.get(30, SECONDS), payload); |
| 389 | + } finally { |
| 390 | + bodyStream.releaseRead.countDown(); |
| 391 | + } |
| 392 | + } |
| 393 | + |
| 394 | + @Test |
| 395 | + public void cancelledQueuedInputStreamIsNotReadOverHttp2() throws Exception { |
| 396 | + startServer(-1); |
| 397 | + byte[] payload = deterministicPayload(SMALL_SIZE); |
| 398 | + BlockingInputStream blockingStream = new BlockingInputStream(payload); |
| 399 | + CloseProbeInputStream queuedStream = new CloseProbeInputStream(); |
| 400 | + |
| 401 | + DefaultAsyncHttpClientConfig clientConfig = config() |
| 402 | + .setUseInsecureTrustManager(true) |
| 403 | + .setHttp2Enabled(true) |
| 404 | + .setMaxConnectionsPerHost(1) |
| 405 | + .setRequestBodyStreamReadOffloadEnabled(true) |
| 406 | + .setRequestBodyStreamReadThreadsCount(1) |
| 407 | + .setRequestBodyStreamReadQueueSize(1) |
| 408 | + .setRequestTimeout(Duration.ofSeconds(30)) |
| 409 | + .build(); |
| 410 | + |
| 411 | + try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(clientConfig)) { |
| 412 | + ThreadPoolExecutor executor = assertInstanceOf(ThreadPoolExecutor.class, |
| 413 | + client.blockingBodyReadExecutor()); |
| 414 | + ListenableFuture<Response> blockingUpload = client.preparePost(httpsUrl("/blocking")) |
| 415 | + .setBody(new InputStreamBodyGenerator(blockingStream, payload.length)) |
| 416 | + .execute(); |
| 417 | + assertTrue(blockingStream.readStarted.await(5, SECONDS), "first body read should occupy the worker"); |
| 418 | + |
| 419 | + ListenableFuture<Response> queuedUpload = client.preparePost(httpsUrl("/queued")) |
| 420 | + .setBody(new InputStreamBodyGenerator(queuedStream, 1)) |
| 421 | + .execute(); |
| 422 | + awaitExecutorState(executor, 1, 1); |
| 423 | + |
| 424 | + assertTrue(queuedUpload.cancel(true), "queued request should be cancelled"); |
| 425 | + assertTrue(queuedStream.closed.await(5, SECONDS), "cancellation should close the queued stream"); |
| 426 | + |
| 427 | + blockingStream.releaseRead.countDown(); |
| 428 | + assertEchoed(blockingUpload.get(30, SECONDS), payload); |
| 429 | + awaitExecutorState(executor, 0, 0); |
| 430 | + assertFalse(queuedStream.readAttempted.get(), |
| 431 | + "a queued read must not start after cancellation closed its stream"); |
| 432 | + } finally { |
| 433 | + blockingStream.releaseRead.countDown(); |
| 434 | + } |
| 435 | + } |
| 436 | + |
365 | 437 | @Test |
366 | 438 | public void inputStreamRuntimeFailureFailsRequestOverHttp2() throws Exception { |
367 | 439 | startServer(-1); |
@@ -587,6 +659,7 @@ private void recordReadThread() { |
587 | 659 | } |
588 | 660 | } |
589 | 661 |
|
| 662 | + |
590 | 663 | // Lifecycle (finding #2 from the cold audit): when the stream is closed while the body pump is parked on |
591 | 664 | // SUSPEND (a streaming/feedable body with no data yet), no in-flight write's failure would run cleanup — |
592 | 665 | // so the writer must close the body source from the stream's closeFuture, or the source (and any file |
|
0 commit comments