Skip to content

Commit 7c4262e

Browse files
author
Dhriti Chopra
committed
feat(storage): add blobReadSession overload that takes a fast open read
Seventh step of fast open BiDi reads (go/fast-open-bidi-read, b/552263062). Storage.blobReadSession(BlobId, ReadProjectionConfig, BlobSourceOption...) sends the read described by the config in the same request that opens the session. The first readAs with an equal config is given that read, saving the round trip a separate first read would take. Only asFutureBytes, asFutureByteString and asChannel configs are accepted, and a null config only opens the session. This is additive: blobReadSession(BlobId, BlobSourceOption...) is unchanged. OtelStorageDecorator passes the config through, so fast open also works with OpenTelemetry tracing enabled.
1 parent 3108ca7 commit 7c4262e

5 files changed

Lines changed: 213 additions & 2 deletions

File tree

‎java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java‎

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import static com.google.cloud.storage.Utils.ifNonNull;
2828
import static com.google.cloud.storage.Utils.projectNameCodec;
2929
import static com.google.common.base.MoreObjects.firstNonNull;
30+
import static com.google.common.base.Preconditions.checkArgument;
3031
import static java.util.Objects.requireNonNull;
3132

3233
import com.google.api.core.ApiFuture;
@@ -55,6 +56,7 @@
5556
import com.google.cloud.storage.HmacKey.HmacKeyState;
5657
import com.google.cloud.storage.PostPolicyV4.PostConditionsV4;
5758
import com.google.cloud.storage.PostPolicyV4.PostFieldsV4;
59+
import com.google.cloud.storage.ReadProjectionConfig.ProjectionType;
5860
import com.google.cloud.storage.Retrying.Retrier;
5961
import com.google.cloud.storage.Retrying.RetrierWithAlg;
6062
import com.google.cloud.storage.Storage.ComposeRequest.SourceBlob;
@@ -1468,6 +1470,26 @@ public Blob moveBlob(MoveBlobRequest request) {
14681470

14691471
@Override
14701472
public ApiFuture<BlobReadSession> blobReadSession(BlobId id, BlobSourceOption... options) {
1473+
return blobReadSession(id, ImmutableList.of(), options);
1474+
}
1475+
1476+
@Override
1477+
public <Projection> ApiFuture<BlobReadSession> blobReadSession(
1478+
BlobId id,
1479+
@Nullable ReadProjectionConfig<Projection> fastOpenConfig,
1480+
BlobSourceOption... options) {
1481+
if (fastOpenConfig == null) {
1482+
return blobReadSession(id, ImmutableList.of(), options);
1483+
}
1484+
checkArgument(
1485+
fastOpenConfig.getType() == ProjectionType.STREAM_READ,
1486+
"fastOpenConfig must be asFutureBytes(), asFutureByteString() or asChannel(), but was %s",
1487+
fastOpenConfig);
1488+
return blobReadSession(id, ImmutableList.of(fastOpenConfig), options);
1489+
}
1490+
1491+
private ApiFuture<BlobReadSession> blobReadSession(
1492+
BlobId id, List<ReadProjectionConfig<?>> fastOpenConfigs, BlobSourceOption... options) {
14711493
Opts<ObjectSourceOpt> opts = Opts.unwrap(options);
14721494
Object object = codecs.blobId().encode(id);
14731495

@@ -1484,7 +1506,8 @@ public ApiFuture<BlobReadSession> blobReadSession(BlobId id, BlobSourceOption...
14841506
BidiReadObjectRequest req = b.build();
14851507

14861508
GrpcCallContext context = opts.grpcMetadataMapper().apply(GrpcCallContext.createDefault());
1487-
ApiFuture<ObjectReadSession> session = storageDataClient.readSession(req, context);
1509+
ApiFuture<ObjectReadSession> session =
1510+
storageDataClient.readSession(req, context, fastOpenConfigs);
14881511

14891512
return BlobReadSessionAdapter.wrap(session);
14901513
}

‎java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/OtelStorageDecorator.java‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1475,6 +1475,14 @@ public Blob moveBlob(MoveBlobRequest request) {
14751475

14761476
@Override
14771477
public ApiFuture<BlobReadSession> blobReadSession(BlobId id, BlobSourceOption... options) {
1478+
return blobReadSession(id, null, options);
1479+
}
1480+
1481+
@Override
1482+
public <Projection> ApiFuture<BlobReadSession> blobReadSession(
1483+
BlobId id,
1484+
@Nullable ReadProjectionConfig<Projection> fastOpenConfig,
1485+
BlobSourceOption... options) {
14781486
Span blobReadSessionSpan =
14791487
tracer
14801488
.spanBuilder(BLOB_READ_SESSION)
@@ -1483,7 +1491,8 @@ public ApiFuture<BlobReadSession> blobReadSession(BlobId id, BlobSourceOption...
14831491
try (Scope ignore1 = blobReadSessionSpan.makeCurrent()) {
14841492
Context blobReadSessionContext = Context.current();
14851493
Span ready = tracer.spanBuilder(BLOB_READ_SESSION + "/ready").startSpan();
1486-
ApiFuture<BlobReadSession> blobReadSessionApiFuture = delegate.blobReadSession(id, options);
1494+
ApiFuture<BlobReadSession> blobReadSessionApiFuture =
1495+
delegate.blobReadSession(id, fastOpenConfig, options);
14871496
ApiFuture<BlobReadSession> futureDecorated =
14881497
ApiFutures.transform(
14891498
blobReadSessionApiFuture,

‎java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/Storage.java‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5955,6 +5955,44 @@ default ApiFuture<BlobReadSession> blobReadSession(BlobId id, BlobSourceOption..
59555955
return throwGrpcOnly(fmtMethodName("blobReadSession", BlobId.class, BlobSourceOption.class));
59565956
}
59575957

5958+
/**
5959+
* Asynchronously set up a new {@link BlobReadSession} for the specified {@link BlobId} and {@code
5960+
* options}, sending the read described by {@code fastOpenConfig} in the same request that opens
5961+
* the session ("fast open"). This saves the round trip a separate first read would take.
5962+
*
5963+
* <p>The first {@link BlobReadSession#readAs(ReadProjectionConfig)} with a config equal to {@code
5964+
* fastOpenConfig} is given that read; any other {@code readAs} sends a new read. If the fast open
5965+
* read fails, the error is reported by the returned future or by that first {@code readAs}.
5966+
*
5967+
* <h4>Example of reading the first 1 KiB of an object with fast open:</h4>
5968+
*
5969+
* <pre>{@code
5970+
* ReadAsFutureBytes first1KiB =
5971+
* ReadProjectionConfigs.asFutureBytes().withRangeSpec(RangeSpec.of(0, 1024));
5972+
* try (BlobReadSession session =
5973+
* storage.blobReadSession(blobId, first1KiB).get(10, TimeUnit.SECONDS)) {
5974+
* byte[] bytes = session.readAs(first1KiB).get(10, TimeUnit.SECONDS);
5975+
* }
5976+
* }</pre>
5977+
*
5978+
* @param id the blob to read from
5979+
* @param fastOpenConfig the read to send with the open, or {@code null} to only open the session.
5980+
* Must be {@link ReadProjectionConfigs#asFutureBytes()}, {@link
5981+
* ReadProjectionConfigs#asFutureByteString()} or {@link ReadProjectionConfigs#asChannel()},
5982+
* optionally with a range.
5983+
* @since 2.77.0 This new api is in preview and is subject to breaking changes.
5984+
*/
5985+
@BetaApi
5986+
@TransportCompatibility({Transport.GRPC})
5987+
default <Projection> ApiFuture<BlobReadSession> blobReadSession(
5988+
BlobId id,
5989+
@Nullable ReadProjectionConfig<Projection> fastOpenConfig,
5990+
BlobSourceOption... options) {
5991+
return throwGrpcOnly(
5992+
fmtMethodName(
5993+
"blobReadSession", BlobId.class, ReadProjectionConfig.class, BlobSourceOption.class));
5994+
}
5995+
59585996
/**
59595997
* Create a new {@link BlobAppendableUpload} for the specified {@code blobInfo} and {@code
59605998
* options}.

‎java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/ITFastOpenFakeTest.java‎

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@
3232
import com.google.api.gax.rpc.OutOfRangeException;
3333
import com.google.cloud.storage.ITObjectReadSessionFakeTest.FakeStorage;
3434
import com.google.cloud.storage.ReadProjectionConfigs.BaseConfig;
35+
import com.google.cloud.storage.Storage.BlobSourceOption;
3536
import com.google.cloud.storage.it.ChecksummedTestContent;
37+
import com.google.cloud.storage.otel.TestExporter;
3638
import com.google.common.base.Throwables;
3739
import com.google.common.collect.ImmutableList;
3840
import com.google.common.collect.ImmutableMap;
@@ -48,6 +50,10 @@
4850
import io.grpc.Metadata;
4951
import io.grpc.Status;
5052
import io.grpc.stub.StreamObserver;
53+
import io.opentelemetry.sdk.OpenTelemetrySdk;
54+
import io.opentelemetry.sdk.trace.SdkTracerProvider;
55+
import io.opentelemetry.sdk.trace.data.SpanData;
56+
import io.opentelemetry.sdk.trace.export.SimpleSpanProcessor;
5157
import java.util.ArrayList;
5258
import java.util.Arrays;
5359
import java.util.Collections;
@@ -69,6 +75,7 @@ public final class ITFastOpenFakeTest {
6975
.setSize(_2MiB)
7076
.build();
7177
private static final GrpcCallContext CTX = GrpcCallContext.createDefault();
78+
private static final BlobId BLOB_ID = BlobId.of("b", "o");
7279

7380
private static final BidiReadObjectRequest OPEN =
7481
BidiReadObjectRequest.newBuilder()
@@ -442,6 +449,133 @@ public void open_whenTheConnectionDropsHalfway_fetchesOnlyTheRestOfTheRange() th
442449
.inOrder();
443450
}
444451

452+
@Test
453+
public void blobReadSession_withAFastOpenConfig_doesNotSendAnotherRequest() throws Exception {
454+
FakeStorage server =
455+
fakeServer(ImmutableMap.of(OPEN_WITH_READ_OF_10_TO_20, metadataAndBytes(1, 10, 10)));
456+
457+
try (FakeServer fakeServer = FakeServer.of(server);
458+
Storage storage = fakeServer.getGrpcStorageOptions().getService()) {
459+
try (BlobReadSession session =
460+
storage.blobReadSession(BLOB_ID, READ_10_TO_20).get(5, SECONDS)) {
461+
byte[] actual = session.readAs(READ_10_TO_20).get(2, SECONDS);
462+
assertThat(xxd(actual)).isEqualTo(xxd(slice(10, 10)));
463+
}
464+
}
465+
466+
assertThat(requestsReceived).containsExactly(OPEN_WITH_READ_OF_10_TO_20);
467+
}
468+
469+
@Test
470+
public void blobReadSession_withTracing_doesNotSendAnotherRequest_andEndsTheSpans()
471+
throws Exception {
472+
FakeStorage server =
473+
fakeServer(ImmutableMap.of(OPEN_WITH_READ_OF_10_TO_20, metadataAndBytes(1, 10, 10)));
474+
TestExporter exporter = new TestExporter();
475+
OpenTelemetrySdk otel =
476+
OpenTelemetrySdk.builder()
477+
.setTracerProvider(
478+
SdkTracerProvider.builder()
479+
.addSpanProcessor(SimpleSpanProcessor.create(exporter))
480+
.build())
481+
.build();
482+
483+
try (FakeServer fakeServer = FakeServer.of(server);
484+
Storage storage =
485+
fakeServer.getGrpcStorageOptions().toBuilder()
486+
.setOpenTelemetry(otel)
487+
.build()
488+
.getService()) {
489+
try (BlobReadSession session =
490+
storage.blobReadSession(BLOB_ID, READ_10_TO_20).get(5, SECONDS)) {
491+
byte[] actual = session.readAs(READ_10_TO_20).get(2, SECONDS);
492+
assertThat(xxd(actual)).isEqualTo(xxd(slice(10, 10)));
493+
}
494+
}
495+
496+
assertThat(requestsReceived).containsExactly(OPEN_WITH_READ_OF_10_TO_20);
497+
List<String> endedSpans =
498+
exporter.getExportedSpans().stream().map(SpanData::getName).collect(toList());
499+
assertThat(endedSpans)
500+
.containsAtLeast(
501+
"com.google.cloud.storage.Storage/blobReadSession",
502+
"com.google.cloud.storage.Storage/blobReadSession/readAs");
503+
}
504+
505+
@Test
506+
public void blobReadSession_withANullFastOpenConfig_isAPlainOpen() throws Exception {
507+
FakeStorage server = fakeServer(ImmutableMap.of(OPEN, metadataOnly()));
508+
509+
try (FakeServer fakeServer = FakeServer.of(server);
510+
Storage storage = fakeServer.getGrpcStorageOptions().getService()) {
511+
BlobReadSession session =
512+
storage.blobReadSession(BLOB_ID, (ReadAsFutureBytes) null).get(5, SECONDS);
513+
session.close();
514+
}
515+
516+
assertThat(requestsReceived).containsExactly(OPEN);
517+
}
518+
519+
@Test
520+
public void blobReadSession_withASeekableChannelConfig_isRejected() throws Exception {
521+
FakeStorage server = fakeServer(ImmutableMap.of());
522+
523+
try (FakeServer fakeServer = FakeServer.of(server);
524+
Storage storage = fakeServer.getGrpcStorageOptions().getService()) {
525+
IllegalArgumentException e =
526+
assertThrows(
527+
IllegalArgumentException.class,
528+
() -> storage.blobReadSession(BLOB_ID, ReadProjectionConfigs.asSeekableChannel()));
529+
assertThat(e).hasMessageThat().startsWith("fastOpenConfig must be");
530+
}
531+
532+
assertThat(requestsReceived).isEmpty();
533+
}
534+
535+
@Test
536+
public void blobReadSession_whenTheFastOpenReadIsOutOfRange_failsTheOpen() throws Exception {
537+
FakeStorage server =
538+
FakeStorage.of(
539+
ImmutableMap.of(
540+
OPEN_WITH_READ_OF_10_TO_20,
541+
client -> {
542+
requestsReceived.add(OPEN_WITH_READ_OF_10_TO_20);
543+
client.onError(Status.OUT_OF_RANGE.asRuntimeException());
544+
}));
545+
546+
try (FakeServer fakeServer = FakeServer.of(server);
547+
Storage storage = fakeServer.getGrpcStorageOptions().getService()) {
548+
ApiFuture<BlobReadSession> open = storage.blobReadSession(BLOB_ID, READ_10_TO_20);
549+
550+
ExecutionException e = assertThrows(ExecutionException.class, () -> open.get(5, SECONDS));
551+
assertThat(e).hasCauseThat().isInstanceOf(StorageException.class);
552+
assertThat(((StorageException) e.getCause()).getCode()).isEqualTo(400);
553+
}
554+
555+
assertThat(requestsReceived).containsExactly(OPEN_WITH_READ_OF_10_TO_20);
556+
}
557+
558+
@Test
559+
public void blobReadSession_withAFastOpenConfig_stillAppliesBlobSourceOptions() throws Exception {
560+
BidiReadObjectRequest openWithGenerationMatchAndRead =
561+
OPEN_WITH_READ_OF_10_TO_20.toBuilder()
562+
.setReadObjectSpec(OPEN.getReadObjectSpec().toBuilder().setIfGenerationMatch(1))
563+
.build();
564+
FakeStorage server =
565+
fakeServer(ImmutableMap.of(openWithGenerationMatchAndRead, metadataAndBytes(1, 10, 10)));
566+
567+
try (FakeServer fakeServer = FakeServer.of(server);
568+
Storage storage = fakeServer.getGrpcStorageOptions().getService()) {
569+
BlobReadSession session =
570+
storage
571+
.blobReadSession(BLOB_ID, READ_10_TO_20, BlobSourceOption.generationMatch(1))
572+
.get(5, SECONDS);
573+
session.close();
574+
}
575+
576+
assertThat(requestsReceived).containsExactly(openWithGenerationMatchAndRead);
577+
}
578+
445579
/**
446580
* A fake server that answers each request in {@code responses} and adds it to {@link
447581
* #requestsReceived}. Any other request is answered with an error.

‎java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/it/runner/registry/AbstractStorageProxy.java‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import com.google.cloud.storage.PostPolicyV4;
4242
import com.google.cloud.storage.PostPolicyV4.PostConditionsV4;
4343
import com.google.cloud.storage.PostPolicyV4.PostFieldsV4;
44+
import com.google.cloud.storage.ReadProjectionConfig;
4445
import com.google.cloud.storage.ServiceAccount;
4546
import com.google.cloud.storage.Storage;
4647
import com.google.cloud.storage.StorageBatch;
@@ -498,6 +499,12 @@ public ApiFuture<BlobReadSession> blobReadSession(BlobId id, BlobSourceOption...
498499
return delegate.blobReadSession(id, options);
499500
}
500501

502+
@Override
503+
public <Projection> ApiFuture<BlobReadSession> blobReadSession(
504+
BlobId id, ReadProjectionConfig<Projection> fastOpenConfig, BlobSourceOption... options) {
505+
return delegate.blobReadSession(id, fastOpenConfig, options);
506+
}
507+
501508
@Override
502509
public BlobAppendableUpload blobAppendableUpload(
503510
BlobInfo blobInfo, BlobAppendableUploadConfig uploadConfig, BlobWriteOption... options) {

0 commit comments

Comments
 (0)