@@ -391,6 +391,117 @@ public void readOnlyTransactionRoutesEachReadIndependently() throws Exception {
391391 assertThat (harness .defaultManagedChannel .callCount ()).isEqualTo (2 );
392392 }
393393
394+ @ Test
395+ public void readOnlyInlinedBeginExecuteSqlRoutesSubsequentRequestsIndependently ()
396+ throws Exception {
397+ TestHarness harness = createHarness ();
398+ ByteString transactionId = ByteString .copyFromUtf8 ("ro-inline-sql" );
399+
400+ seedCache (harness , createTwoRangeCacheUpdate ());
401+
402+ // First query begins a read-only transaction inline and routes to server-a.
403+ ClientCall <ExecuteSqlRequest , ResultSet > firstCall =
404+ harness .channel .newCall (SpannerGrpc .getExecuteSqlMethod (), CallOptions .DEFAULT );
405+ firstCall .start (new CapturingListener <ResultSet >(), new Metadata ());
406+ firstCall .sendMessage (
407+ ExecuteSqlRequest .newBuilder ()
408+ .setSession (SESSION )
409+ .setTransaction (
410+ TransactionSelector .newBuilder ()
411+ .setBegin (
412+ TransactionOptions .newBuilder ()
413+ .setReadOnly (
414+ TransactionOptions .ReadOnly .newBuilder ()
415+ .setReturnReadTimestamp (true )
416+ .build ())
417+ .build ()))
418+ .setRoutingHint (RoutingHint .newBuilder ().setKey (bytes ("b" )).build ())
419+ .build ());
420+
421+ assertThat (harness .endpointCache .callCountForAddress ("server-a:1234" )).isEqualTo (1 );
422+
423+ @ SuppressWarnings ("unchecked" )
424+ RecordingClientCall <ExecuteSqlRequest , ResultSet > firstDelegate =
425+ (RecordingClientCall <ExecuteSqlRequest , ResultSet >)
426+ harness .endpointCache .latestCallForAddress ("server-a:1234" );
427+ firstDelegate .emitOnMessage (
428+ ResultSet .newBuilder ()
429+ .setMetadata (
430+ ResultSetMetadata .newBuilder ()
431+ .setTransaction (Transaction .newBuilder ().setId (transactionId )))
432+ .build ());
433+
434+ // Second query in same txn should route by key to server-b, not affinity-pin to server-a.
435+ ClientCall <ExecuteSqlRequest , ResultSet > secondCall =
436+ harness .channel .newCall (SpannerGrpc .getExecuteSqlMethod (), CallOptions .DEFAULT );
437+ secondCall .start (new CapturingListener <ResultSet >(), new Metadata ());
438+ secondCall .sendMessage (
439+ ExecuteSqlRequest .newBuilder ()
440+ .setSession (SESSION )
441+ .setTransaction (TransactionSelector .newBuilder ().setId (transactionId ))
442+ .setRoutingHint (RoutingHint .newBuilder ().setKey (bytes ("n" )).build ())
443+ .build ());
444+
445+ assertThat (harness .endpointCache .callCountForAddress ("server-a:1234" )).isEqualTo (1 );
446+ assertThat (harness .endpointCache .callCountForAddress ("server-b:1234" )).isEqualTo (1 );
447+ assertThat (harness .defaultManagedChannel .callCount ()).isEqualTo (1 );
448+ }
449+
450+ @ Test
451+ public void readOnlyInlinedBeginReadRoutesSubsequentRequestsIndependently () throws Exception {
452+ TestHarness harness = createHarness ();
453+ ByteString transactionId = ByteString .copyFromUtf8 ("ro-inline-read" );
454+
455+ seedCache (harness , createTwoRangeCacheUpdate ());
456+
457+ // First read begins a read-only transaction inline and routes to server-a.
458+ ClientCall <ReadRequest , PartialResultSet > firstCall =
459+ harness .channel .newCall (SpannerGrpc .getStreamingReadMethod (), CallOptions .DEFAULT );
460+ firstCall .start (new CapturingListener <PartialResultSet >(), new Metadata ());
461+ firstCall .sendMessage (
462+ ReadRequest .newBuilder ()
463+ .setSession (SESSION )
464+ .setTransaction (
465+ TransactionSelector .newBuilder ()
466+ .setBegin (
467+ TransactionOptions .newBuilder ()
468+ .setReadOnly (
469+ TransactionOptions .ReadOnly .newBuilder ()
470+ .setReturnReadTimestamp (true )
471+ .build ())
472+ .build ()))
473+ .setRoutingHint (RoutingHint .newBuilder ().setKey (bytes ("b" )).build ())
474+ .build ());
475+
476+ assertThat (harness .endpointCache .callCountForAddress ("server-a:1234" )).isEqualTo (1 );
477+
478+ @ SuppressWarnings ("unchecked" )
479+ RecordingClientCall <ReadRequest , PartialResultSet > firstDelegate =
480+ (RecordingClientCall <ReadRequest , PartialResultSet >)
481+ harness .endpointCache .latestCallForAddress ("server-a:1234" );
482+ firstDelegate .emitOnMessage (
483+ PartialResultSet .newBuilder ()
484+ .setMetadata (
485+ ResultSetMetadata .newBuilder ()
486+ .setTransaction (Transaction .newBuilder ().setId (transactionId )))
487+ .build ());
488+
489+ // Second read in same txn should route by key to server-b, not affinity-pin to server-a.
490+ ClientCall <ReadRequest , PartialResultSet > secondCall =
491+ harness .channel .newCall (SpannerGrpc .getStreamingReadMethod (), CallOptions .DEFAULT );
492+ secondCall .start (new CapturingListener <PartialResultSet >(), new Metadata ());
493+ secondCall .sendMessage (
494+ ReadRequest .newBuilder ()
495+ .setSession (SESSION )
496+ .setTransaction (TransactionSelector .newBuilder ().setId (transactionId ))
497+ .setRoutingHint (RoutingHint .newBuilder ().setKey (bytes ("n" )).build ())
498+ .build ());
499+
500+ assertThat (harness .endpointCache .callCountForAddress ("server-a:1234" )).isEqualTo (1 );
501+ assertThat (harness .endpointCache .callCountForAddress ("server-b:1234" )).isEqualTo (1 );
502+ assertThat (harness .defaultManagedChannel .callCount ()).isEqualTo (1 );
503+ }
504+
394505 @ Test
395506 public void readOnlyTransactionDoesNotRecordAffinity () throws Exception {
396507 TestHarness harness = createHarness ();
@@ -484,6 +595,63 @@ public void readOnlyTransactionCleanupOnClose() throws Exception {
484595 harness .channel .clearTransactionAffinity (transactionId );
485596 }
486597
598+ private static CacheUpdate createTwoRangeCacheUpdate () {
599+ return CacheUpdate .newBuilder ()
600+ .setDatabaseId (7L )
601+ .addRange (
602+ Range .newBuilder ()
603+ .setStartKey (bytes ("a" ))
604+ .setLimitKey (bytes ("m" ))
605+ .setGroupUid (1L )
606+ .setSplitId (1L )
607+ .setGeneration (bytes ("1" )))
608+ .addRange (
609+ Range .newBuilder ()
610+ .setStartKey (bytes ("m" ))
611+ .setLimitKey (bytes ("z" ))
612+ .setGroupUid (2L )
613+ .setSplitId (2L )
614+ .setGeneration (bytes ("1" )))
615+ .addGroup (
616+ Group .newBuilder ()
617+ .setGroupUid (1L )
618+ .setGeneration (bytes ("1" ))
619+ .addTablets (
620+ Tablet .newBuilder ()
621+ .setTabletUid (1L )
622+ .setServerAddress ("server-a:1234" )
623+ .setIncarnation (bytes ("1" ))
624+ .setDistance (0 )))
625+ .addGroup (
626+ Group .newBuilder ()
627+ .setGroupUid (2L )
628+ .setGeneration (bytes ("1" ))
629+ .addTablets (
630+ Tablet .newBuilder ()
631+ .setTabletUid (2L )
632+ .setServerAddress ("server-b:1234" )
633+ .setIncarnation (bytes ("1" ))
634+ .setDistance (0 )))
635+ .build ();
636+ }
637+
638+ private static void seedCache (TestHarness harness , CacheUpdate cacheUpdate ) {
639+ ClientCall <ExecuteSqlRequest , ResultSet > seedCall =
640+ harness .channel .newCall (SpannerGrpc .getExecuteSqlMethod (), CallOptions .DEFAULT );
641+ seedCall .start (new CapturingListener <ResultSet >(), new Metadata ());
642+ seedCall .sendMessage (
643+ ExecuteSqlRequest .newBuilder ()
644+ .setSession (SESSION )
645+ .setRoutingHint (RoutingHint .newBuilder ().setKey (bytes ("a" )).build ())
646+ .build ());
647+
648+ @ SuppressWarnings ("unchecked" )
649+ RecordingClientCall <ExecuteSqlRequest , ResultSet > seedDelegate =
650+ (RecordingClientCall <ExecuteSqlRequest , ResultSet >)
651+ harness .defaultManagedChannel .latestCall ();
652+ seedDelegate .emitOnMessage (ResultSet .newBuilder ().setCacheUpdate (cacheUpdate ).build ());
653+ }
654+
487655 private static TestHarness createHarness () throws IOException {
488656 FakeEndpointCache endpointCache = new FakeEndpointCache (DEFAULT_ADDRESS );
489657 InstantiatingGrpcChannelProvider provider =
@@ -574,6 +742,17 @@ int callCountForAddress(String address) {
574742 FakeEndpoint endpoint = endpoints .get (address );
575743 return endpoint == null ? 0 : endpoint .channel .callCount ();
576744 }
745+
746+ RecordingClientCall <?, ?> latestCallForAddress (String address ) {
747+ if (defaultAddress .equals (address )) {
748+ return defaultEndpoint .channel .latestCall ();
749+ }
750+ FakeEndpoint endpoint = endpoints .get (address );
751+ if (endpoint == null ) {
752+ throw new IllegalStateException ("No endpoint for address: " + address );
753+ }
754+ return endpoint .channel .latestCall ();
755+ }
577756 }
578757
579758 private static final class FakeEndpoint implements ChannelEndpoint {
0 commit comments