3232import com .google .api .gax .rpc .OutOfRangeException ;
3333import com .google .cloud .storage .ITObjectReadSessionFakeTest .FakeStorage ;
3434import com .google .cloud .storage .ReadProjectionConfigs .BaseConfig ;
35+ import com .google .cloud .storage .Storage .BlobSourceOption ;
3536import com .google .cloud .storage .it .ChecksummedTestContent ;
37+ import com .google .cloud .storage .otel .TestExporter ;
3638import com .google .common .base .Throwables ;
3739import com .google .common .collect .ImmutableList ;
3840import com .google .common .collect .ImmutableMap ;
4850import io .grpc .Metadata ;
4951import io .grpc .Status ;
5052import 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 ;
5157import java .util .ArrayList ;
5258import java .util .Arrays ;
5359import 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.
0 commit comments