|
19 | 19 | import static com.google.common.truth.Truth.assertThat; |
20 | 20 | import static io.grpc.internal.GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE; |
21 | 21 | import static org.junit.Assert.assertEquals; |
| 22 | +import static org.junit.Assert.assertFalse; |
| 23 | +import static org.junit.Assert.assertNotNull; |
22 | 24 | import static org.junit.Assert.assertNull; |
23 | 25 | import static org.junit.Assert.assertThrows; |
24 | 26 | import static org.junit.Assert.assertTrue; |
|
35 | 37 | import com.google.common.io.ByteStreams; |
36 | 38 | import com.google.common.primitives.Bytes; |
37 | 39 | import io.grpc.Codec; |
| 40 | +import io.grpc.Decompressor; |
38 | 41 | import io.grpc.InternalChannelz.TransportStats; |
39 | 42 | import io.grpc.StatusRuntimeException; |
40 | 43 | import io.grpc.StreamTracer; |
| 44 | +import io.grpc.internal.MessageDeframer.LazyDecompressingInputStream; |
41 | 45 | import io.grpc.internal.MessageDeframer.Listener; |
42 | 46 | import io.grpc.internal.MessageDeframer.SizeEnforcingInputStream; |
43 | 47 | import io.grpc.internal.testing.TestStreamTracer.TestBaseStreamTracer; |
|
52 | 56 | import java.util.List; |
53 | 57 | import java.util.Locale; |
54 | 58 | import java.util.concurrent.TimeUnit; |
| 59 | +import java.util.concurrent.atomic.AtomicBoolean; |
55 | 60 | import java.util.zip.GZIPOutputStream; |
56 | 61 | import org.junit.Before; |
57 | 62 | import org.junit.Test; |
@@ -313,6 +318,75 @@ public void compressed() { |
313 | 318 | verifyNoMoreInteractions(listener); |
314 | 319 | } |
315 | 320 |
|
| 321 | + @Test |
| 322 | + public void compressed_lazyDecompression() throws IOException { |
| 323 | + final AtomicBoolean decompressCalled = new AtomicBoolean(false); |
| 324 | + Decompressor countingDecompressor = new Decompressor() { |
| 325 | + @Override |
| 326 | + public String getMessageEncoding() { |
| 327 | + return "gzip"; |
| 328 | + } |
| 329 | + |
| 330 | + @Override |
| 331 | + public InputStream decompress(InputStream is) throws IOException { |
| 332 | + decompressCalled.set(true); |
| 333 | + return new Codec.Gzip().decompress(is); |
| 334 | + } |
| 335 | + }; |
| 336 | + |
| 337 | + deframer = new MessageDeframer(listener, countingDecompressor, DEFAULT_MAX_MESSAGE_SIZE, |
| 338 | + statsTraceCtx, transportTracer); |
| 339 | + deframer.request(1); |
| 340 | + |
| 341 | + byte[] payload = compress(new byte[1000]); |
| 342 | + byte[] header = new byte[]{1, 0, 0, 0, (byte) payload.length}; |
| 343 | + deframer.deframe(buffer(Bytes.concat(header, payload))); |
| 344 | + |
| 345 | + verify(listener).messagesAvailable(producer.capture()); |
| 346 | + InputStream stream = producer.getValue().next(); |
| 347 | + assertNotNull(stream); |
| 348 | + |
| 349 | + // Decompressor should not be invoked before bytes are read |
| 350 | + assertFalse(decompressCalled.get()); |
| 351 | + |
| 352 | + // Reading a byte triggers decompression |
| 353 | + assertEquals(0, stream.read()); |
| 354 | + assertTrue(decompressCalled.get()); |
| 355 | + } |
| 356 | + |
| 357 | + @Test |
| 358 | + public void compressed_closeWithoutReading_noDecompression() throws IOException { |
| 359 | + final AtomicBoolean decompressCalled = new AtomicBoolean(false); |
| 360 | + Decompressor countingDecompressor = new Decompressor() { |
| 361 | + @Override |
| 362 | + public String getMessageEncoding() { |
| 363 | + return "gzip"; |
| 364 | + } |
| 365 | + |
| 366 | + @Override |
| 367 | + public InputStream decompress(InputStream is) throws IOException { |
| 368 | + decompressCalled.set(true); |
| 369 | + return new Codec.Gzip().decompress(is); |
| 370 | + } |
| 371 | + }; |
| 372 | + |
| 373 | + deframer = new MessageDeframer(listener, countingDecompressor, DEFAULT_MAX_MESSAGE_SIZE, |
| 374 | + statsTraceCtx, transportTracer); |
| 375 | + deframer.request(1); |
| 376 | + |
| 377 | + byte[] payload = compress(new byte[1000]); |
| 378 | + byte[] header = new byte[]{1, 0, 0, 0, (byte) payload.length}; |
| 379 | + deframer.deframe(buffer(Bytes.concat(header, payload))); |
| 380 | + |
| 381 | + verify(listener).messagesAvailable(producer.capture()); |
| 382 | + InputStream stream = producer.getValue().next(); |
| 383 | + assertNotNull(stream); |
| 384 | + |
| 385 | + // Closing without reading should not decompress |
| 386 | + stream.close(); |
| 387 | + assertFalse(decompressCalled.get()); |
| 388 | + } |
| 389 | + |
316 | 390 | @Test |
317 | 391 | public void deliverIsReentrantSafe() { |
318 | 392 | doAnswer( |
@@ -493,6 +567,79 @@ public void sizeEnforcingInputStream_markReset() throws IOException { |
493 | 567 | } |
494 | 568 | } |
495 | 569 |
|
| 570 | + @RunWith(JUnit4.class) |
| 571 | + public static class LazyDecompressingInputStreamTests { |
| 572 | + private TestBaseStreamTracer tracer = new TestBaseStreamTracer(); |
| 573 | + private StatsTraceContext statsTraceCtx = new StatsTraceContext(new StreamTracer[]{tracer}); |
| 574 | + |
| 575 | + @Test |
| 576 | + public void lazyDecompressingInputStream_doesNotInitializeUntilRead() throws IOException { |
| 577 | + final AtomicBoolean decompressCalled = new AtomicBoolean(false); |
| 578 | + Decompressor countingDecompressor = new Decompressor() { |
| 579 | + @Override |
| 580 | + public String getMessageEncoding() { |
| 581 | + return "gzip"; |
| 582 | + } |
| 583 | + |
| 584 | + @Override |
| 585 | + public InputStream decompress(InputStream is) throws IOException { |
| 586 | + decompressCalled.set(true); |
| 587 | + return new Codec.Gzip().decompress(is); |
| 588 | + } |
| 589 | + }; |
| 590 | + |
| 591 | + ByteArrayInputStream in = |
| 592 | + new ByteArrayInputStream(compress("hello".getBytes(StandardCharsets.UTF_8))); |
| 593 | + LazyDecompressingInputStream stream = new LazyDecompressingInputStream( |
| 594 | + in, 100, statsTraceCtx, countingDecompressor); |
| 595 | + |
| 596 | + assertFalse(decompressCalled.get()); |
| 597 | + byte[] buf = new byte[5]; |
| 598 | + int read = stream.read(buf); |
| 599 | + assertEquals(5, read); |
| 600 | + assertEquals("hello", new String(buf, StandardCharsets.UTF_8)); |
| 601 | + assertTrue(decompressCalled.get()); |
| 602 | + stream.close(); |
| 603 | + } |
| 604 | + |
| 605 | + @Test |
| 606 | + public void lazyDecompressingInputStream_closeWithoutRead() throws IOException { |
| 607 | + final AtomicBoolean decompressCalled = new AtomicBoolean(false); |
| 608 | + final AtomicBoolean inClosed = new AtomicBoolean(false); |
| 609 | + Decompressor countingDecompressor = new Decompressor() { |
| 610 | + @Override |
| 611 | + public String getMessageEncoding() { |
| 612 | + return "gzip"; |
| 613 | + } |
| 614 | + |
| 615 | + @Override |
| 616 | + public InputStream decompress(InputStream is) throws IOException { |
| 617 | + decompressCalled.set(true); |
| 618 | + return new Codec.Gzip().decompress(is); |
| 619 | + } |
| 620 | + }; |
| 621 | + |
| 622 | + ByteArrayInputStream in = |
| 623 | + new ByteArrayInputStream(compress("hello".getBytes(StandardCharsets.UTF_8))) { |
| 624 | + @Override |
| 625 | + public void close() throws IOException { |
| 626 | + inClosed.set(true); |
| 627 | + super.close(); |
| 628 | + } |
| 629 | + }; |
| 630 | + LazyDecompressingInputStream stream = new LazyDecompressingInputStream( |
| 631 | + in, 100, statsTraceCtx, countingDecompressor); |
| 632 | + |
| 633 | + assertFalse(decompressCalled.get()); |
| 634 | + stream.close(); |
| 635 | + assertTrue(inClosed.get()); |
| 636 | + assertFalse(decompressCalled.get()); |
| 637 | + |
| 638 | + // Reading after close should throw IOException |
| 639 | + assertThrows(IOException.class, () -> stream.read()); |
| 640 | + } |
| 641 | + } |
| 642 | + |
496 | 643 | /** |
497 | 644 | * Verify stats were published through the tracer. |
498 | 645 | * |
|
0 commit comments