|
53 | 53 | import com.google.common.io.BaseEncoding; |
54 | 54 | import com.google.common.io.ByteStreams; |
55 | 55 | import com.google.common.truth.Correspondence; |
| 56 | +import com.google.common.util.concurrent.ThreadFactoryBuilder; |
56 | 57 | import com.google.protobuf.Any; |
57 | 58 | import com.google.protobuf.ByteString; |
58 | 59 | import com.google.protobuf.TextFormat; |
| 60 | +import com.google.rpc.DebugInfo; |
59 | 61 | import com.google.storage.v2.BidiReadHandle; |
60 | 62 | import com.google.storage.v2.BidiReadObjectError; |
61 | 63 | import com.google.storage.v2.BidiReadObjectRedirectedError; |
|
77 | 79 | import io.grpc.protobuf.ProtoUtils; |
78 | 80 | import io.grpc.stub.StreamObserver; |
79 | 81 | import java.io.ByteArrayOutputStream; |
| 82 | +import java.io.IOException; |
80 | 83 | import java.nio.ByteBuffer; |
81 | 84 | import java.nio.channels.Channels; |
82 | 85 | import java.nio.channels.ScatteringByteChannel; |
|
86 | 89 | import java.util.Collections; |
87 | 90 | import java.util.HashSet; |
88 | 91 | import java.util.List; |
| 92 | +import java.util.Locale; |
89 | 93 | import java.util.Map; |
| 94 | +import java.util.Optional; |
90 | 95 | import java.util.Set; |
91 | 96 | import java.util.UUID; |
92 | 97 | import java.util.concurrent.CountDownLatch; |
93 | 98 | import java.util.concurrent.ExecutionException; |
| 99 | +import java.util.concurrent.ExecutorService; |
| 100 | +import java.util.concurrent.Executors; |
| 101 | +import java.util.concurrent.Future; |
| 102 | +import java.util.concurrent.Semaphore; |
94 | 103 | import java.util.concurrent.TimeUnit; |
95 | 104 | import java.util.concurrent.atomic.AtomicInteger; |
| 105 | +import java.util.concurrent.atomic.AtomicReference; |
96 | 106 | import java.util.function.Consumer; |
97 | 107 | import java.util.stream.Collectors; |
98 | 108 | import java.util.stream.Stream; |
@@ -1376,9 +1386,6 @@ public void gettingSessionFromFastOpenKeepsTheSessionOpenUntilClosed() throws Ex |
1376 | 1386 | .build()) |
1377 | 1387 | .build(); |
1378 | 1388 |
|
1379 | | - System.out.println("req1 = " + TextFormat.printer().shortDebugString(req1)); |
1380 | | - System.out.println("req2 = " + TextFormat.printer().shortDebugString(req2)); |
1381 | | - System.out.println("req3 = " + TextFormat.printer().shortDebugString(req3)); |
1382 | 1389 | ImmutableMap<BidiReadObjectRequest, BidiReadObjectResponse> db = |
1383 | 1390 | ImmutableMap.<BidiReadObjectRequest, BidiReadObjectResponse>builder() |
1384 | 1391 | .put(req1, res1) |
@@ -1538,6 +1545,196 @@ public void expectRetryForRangeWithFailedChecksumValidation_channel() throws Exc |
1538 | 1545 | } |
1539 | 1546 | } |
1540 | 1547 |
|
| 1548 | + /** |
| 1549 | + * Define a test where multiple reads for the same session will be performed, and some of those |
| 1550 | + * reads cause OUT_OF_RANGE errors. |
| 1551 | + * |
| 1552 | + * <p>An OUT_OF_RANGE error is delivered as a stream level status, which means any reads which |
| 1553 | + * share a stream must be restarted while the read that caused the OUT_OF_RANGE should be failed. |
| 1554 | + * |
| 1555 | + * <p>Verify this behavior for both channel based and future byte[] based. |
| 1556 | + */ |
| 1557 | + @Test |
| 1558 | + public void serverOutOfRangeIsNotRetried() throws Exception { |
| 1559 | + ChecksummedTestContent expected = ChecksummedTestContent.of(ALL_OBJECT_BYTES, 10, 20); |
| 1560 | + Semaphore sem = new Semaphore(1); |
| 1561 | + |
| 1562 | + BidiReadObjectResponse dataResp = |
| 1563 | + BidiReadObjectResponse.newBuilder() |
| 1564 | + .addObjectDataRanges( |
| 1565 | + ObjectRangeData.newBuilder() |
| 1566 | + .setChecksummedData(expected.asChecksummedData()) |
| 1567 | + .setReadRange(getReadRange(0, 10, 20)) |
| 1568 | + .setRangeEnd(true) |
| 1569 | + .build()) |
| 1570 | + .build(); |
| 1571 | + |
| 1572 | + AtomicInteger bidiReadObjectCount = new AtomicInteger(); |
| 1573 | + ExecutorService exec = |
| 1574 | + Executors.newCachedThreadPool( |
| 1575 | + new ThreadFactoryBuilder().setDaemon(true).setNameFormat("test-server-%d").build()); |
| 1576 | + |
| 1577 | + StorageImplBase fakeStorage = |
| 1578 | + new StorageImplBase() { |
| 1579 | + @Override |
| 1580 | + public StreamObserver<BidiReadObjectRequest> bidiReadObject( |
| 1581 | + StreamObserver<BidiReadObjectResponse> respond) { |
| 1582 | + bidiReadObjectCount.getAndIncrement(); |
| 1583 | + return new StreamObserver<BidiReadObjectRequest>() { |
| 1584 | + @Override |
| 1585 | + public void onNext(BidiReadObjectRequest request) { |
| 1586 | + if (request.equals(REQ_OPEN)) { |
| 1587 | + respond.onNext(RES_OPEN); |
| 1588 | + } else if (request.getReadRangesList().get(0).getReadOffset() == 10) { |
| 1589 | + exec.submit( |
| 1590 | + () -> { |
| 1591 | + try { |
| 1592 | + sem.acquire(); |
| 1593 | + BidiReadObjectResponse.Builder b = dataResp.toBuilder(); |
| 1594 | + ReadRange readRange = request.getReadRangesList().get(0); |
| 1595 | + ObjectRangeData.Builder bb = dataResp.getObjectDataRanges(0).toBuilder(); |
| 1596 | + bb.getReadRangeBuilder().setReadId(readRange.getReadId()); |
| 1597 | + b.setObjectDataRanges(0, bb.build()); |
| 1598 | + respond.onNext(b.build()); |
| 1599 | + } catch (InterruptedException e) { |
| 1600 | + respond.onError( |
| 1601 | + TestUtils.apiException(Code.UNIMPLEMENTED, e.getMessage())); |
| 1602 | + } |
| 1603 | + }); |
| 1604 | + } else if (bidiReadObjectCount.getAndIncrement() >= 1) { |
| 1605 | + Optional<ReadRange> readRange = request.getReadRangesList().stream().findFirst(); |
| 1606 | + String message = |
| 1607 | + String.format( |
| 1608 | + Locale.US, |
| 1609 | + "OUT_OF_RANGE read_offset = %d", |
| 1610 | + readRange.map(ReadRange::getReadOffset).orElse(0L)); |
| 1611 | + long readId = readRange.map(ReadRange::getReadId).orElse(0L); |
| 1612 | + |
| 1613 | + BidiReadObjectError err2 = |
| 1614 | + BidiReadObjectError.newBuilder() |
| 1615 | + .addReadRangeErrors( |
| 1616 | + ReadRangeError.newBuilder() |
| 1617 | + .setReadId(readId) |
| 1618 | + .setStatus( |
| 1619 | + com.google.rpc.Status.newBuilder() |
| 1620 | + .setCode(com.google.rpc.Code.OUT_OF_RANGE_VALUE) |
| 1621 | + .build()) |
| 1622 | + .build()) |
| 1623 | + .build(); |
| 1624 | + |
| 1625 | + com.google.rpc.Status grpcStatusDetails = |
| 1626 | + com.google.rpc.Status.newBuilder() |
| 1627 | + .setCode(com.google.rpc.Code.UNAVAILABLE_VALUE) |
| 1628 | + .setMessage("fail read_id: " + readId) |
| 1629 | + .addDetails(Any.pack(err2)) |
| 1630 | + .addDetails( |
| 1631 | + Any.pack( |
| 1632 | + DebugInfo.newBuilder() |
| 1633 | + .setDetail(message) |
| 1634 | + .addStackEntries( |
| 1635 | + TextFormat.printer().shortDebugString(request)) |
| 1636 | + .build())) |
| 1637 | + .build(); |
| 1638 | + |
| 1639 | + Metadata trailers = new Metadata(); |
| 1640 | + trailers.put(GRPC_STATUS_DETAILS_KEY, grpcStatusDetails); |
| 1641 | + StatusRuntimeException statusRuntimeException = |
| 1642 | + Status.OUT_OF_RANGE.withDescription(message).asRuntimeException(trailers); |
| 1643 | + respond.onError(statusRuntimeException); |
| 1644 | + } else { |
| 1645 | + respond.onError( |
| 1646 | + apiException( |
| 1647 | + Code.UNIMPLEMENTED, |
| 1648 | + "Unexpected request { " |
| 1649 | + + TextFormat.printer().shortDebugString(request) |
| 1650 | + + " }")); |
| 1651 | + } |
| 1652 | + } |
| 1653 | + |
| 1654 | + @Override |
| 1655 | + public void onError(Throwable t) { |
| 1656 | + respond.onError(t); |
| 1657 | + } |
| 1658 | + |
| 1659 | + @Override |
| 1660 | + public void onCompleted() { |
| 1661 | + respond.onCompleted(); |
| 1662 | + } |
| 1663 | + }; |
| 1664 | + } |
| 1665 | + }; |
| 1666 | + try (FakeServer fakeServer = FakeServer.of(fakeStorage); |
| 1667 | + Storage storage = fakeServer.getGrpcStorageOptions().getService()) { |
| 1668 | + |
| 1669 | + BlobId id = BlobId.of("b", "o"); |
| 1670 | + |
| 1671 | + try (BlobReadSession session = storage.blobReadSession(id).get(5, TimeUnit.SECONDS)) { |
| 1672 | + |
| 1673 | + ApiFuture<byte[]> shouldSucceedFuture = |
| 1674 | + session.readAs( |
| 1675 | + ReadProjectionConfigs.asFutureBytes().withRangeSpec(RangeSpec.of(10, 20))); |
| 1676 | + |
| 1677 | + ApiFuture<byte[]> shouldFailFuture = |
| 1678 | + session.readAs( |
| 1679 | + ReadProjectionConfigs.asFutureBytes().withRangeSpec(RangeSpec.beginAt(37))); |
| 1680 | + ExecutionException exceptionFromFuture = |
| 1681 | + assertThrows( |
| 1682 | + ExecutionException.class, () -> shouldFailFuture.get(30, TimeUnit.SECONDS)); |
| 1683 | + sem.release(); |
| 1684 | + |
| 1685 | + Exception exceptionFromChannel; |
| 1686 | + byte[] bytesFromFuture = shouldSucceedFuture.get(30, TimeUnit.SECONDS); |
| 1687 | + |
| 1688 | + AtomicReference<byte[]> bytesFromChannel = new AtomicReference<>(); |
| 1689 | + Future<Long> asyncShouldSucceedChannel = |
| 1690 | + exec.submit( |
| 1691 | + () -> { |
| 1692 | + try (ScatteringByteChannel succeed = |
| 1693 | + session.readAs( |
| 1694 | + ReadProjectionConfigs.asChannel().withRangeSpec(RangeSpec.of(10, 20)))) { |
| 1695 | + sem.acquire(); |
| 1696 | + ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
| 1697 | + long copy = ByteStreams.copy(succeed, Channels.newChannel(baos)); |
| 1698 | + bytesFromChannel.set(baos.toByteArray()); |
| 1699 | + return copy; |
| 1700 | + } catch (IOException e) { |
| 1701 | + throw new RuntimeException(e); |
| 1702 | + } |
| 1703 | + }); |
| 1704 | + |
| 1705 | + try (ScatteringByteChannel fail = |
| 1706 | + session.readAs( |
| 1707 | + ReadProjectionConfigs.asChannel().withRangeSpec(RangeSpec.beginAt(39)))) { |
| 1708 | + exceptionFromChannel = |
| 1709 | + assertThrows( |
| 1710 | + IOException.class, |
| 1711 | + () -> { |
| 1712 | + int read = 0; |
| 1713 | + do { |
| 1714 | + read = fail.read(ByteBuffer.allocate(1)); |
| 1715 | + } while (read == 0); |
| 1716 | + }); |
| 1717 | + sem.release(); |
| 1718 | + } |
| 1719 | + asyncShouldSucceedChannel.get(30, TimeUnit.SECONDS); |
| 1720 | + Exception finalExceptionFromChannel = exceptionFromChannel; |
| 1721 | + assertAll( |
| 1722 | + () -> |
| 1723 | + assertThat(exceptionFromFuture) |
| 1724 | + .hasCauseThat() |
| 1725 | + .hasCauseThat() |
| 1726 | + .isInstanceOf(OutOfRangeException.class), |
| 1727 | + () -> |
| 1728 | + assertThat(finalExceptionFromChannel) |
| 1729 | + .hasCauseThat() |
| 1730 | + .hasCauseThat() |
| 1731 | + .isInstanceOf(OutOfRangeException.class), |
| 1732 | + () -> assertThat(xxd(bytesFromFuture)).isEqualTo(xxd(expected.getBytes())), |
| 1733 | + () -> assertThat(xxd(bytesFromChannel.get())).isEqualTo(xxd(expected.getBytes()))); |
| 1734 | + } |
| 1735 | + } |
| 1736 | + } |
| 1737 | + |
1541 | 1738 | private static void runTestAgainstFakeServer( |
1542 | 1739 | FakeStorage fakeStorage, RangeSpec range, ChecksummedTestContent expected) throws Exception { |
1543 | 1740 |
|
|
0 commit comments