@@ -67,6 +67,7 @@ public class LocationAwareSharedBackendReplicaHarnessTest {
6767 private static final String PROJECT = "fake-project" ;
6868 private static final String INSTANCE = "fake-instance" ;
6969 private static final String DATABASE = "fake-database" ;
70+ private static final AtomicInteger DATABASE_COUNTER = new AtomicInteger ();
7071 private static final String TABLE = "T" ;
7172 private static final String REPLICA_LOCATION = "us-east1" ;
7273 private static final Statement SEED_QUERY = Statement .of ("SELECT 1" );
@@ -85,6 +86,14 @@ public class LocationAwareSharedBackendReplicaHarnessTest {
8586 .build ();
8687 private static SharedBackendReplicaHarness harness ;
8788
89+ // Assigned a fresh value per test so each test uses a distinct database scope. The location-aware
90+ // routing layer keeps a process-wide EndpointLatencyRegistry keyed by (databaseScope,
91+ // operationUid, ...). Each test builds a new Spanner whose per-channel operationUid counter
92+ // restarts at 1, so without a unique scope a prior test's recorded error/latency penalties (e.g.
93+ // an aborted commit on the leader) would leak into a later test and skew its routing, making the
94+ // routing assertions flaky.
95+ private String database ;
96+
8897 @ BeforeClass
8998 public static void enableLocationAwareRouting () throws Exception {
9099 SpannerOptions .useEnvironment (
@@ -100,6 +109,7 @@ public boolean isEnableLocationApi() {
100109 @ Before
101110 public void resetHarness () {
102111 harness .reset ();
112+ database = DATABASE + "-" + DATABASE_COUNTER .incrementAndGet ();
103113 }
104114
105115 @ AfterClass
@@ -118,7 +128,7 @@ public static void restoreEnvironment() throws Exception {
118128 public void singleUseReadReroutesOnResourceExhaustedForBypassTraffic () throws Exception {
119129 try (Spanner spanner = createSpanner (harness )) {
120130 configureBackend (harness , singleRowReadResultSet ("b" ));
121- DatabaseClient client = spanner .getDatabaseClient (DatabaseId .of (PROJECT , INSTANCE , DATABASE ));
131+ DatabaseClient client = spanner .getDatabaseClient (DatabaseId .of (PROJECT , INSTANCE , database ));
122132
123133 seedLocationMetadata (client );
124134 waitForReplicaRoutedRead (client , harness );
@@ -135,7 +145,7 @@ public void singleUseReadReroutesOnResourceExhaustedForBypassTraffic() throws Ex
135145 KeySet .singleKey (Key .of ("b" )),
136146 Arrays .asList ("k" ),
137147 Options .directedRead (DIRECTED_READ_OPTIONS ))) {
138- assertTrue (resultSet . next ( ));
148+ assertTrue (drain ( resultSet ));
139149 }
140150
141151 String diagnostics = routingDiagnostics (harness );
@@ -154,7 +164,7 @@ public void singleUseReadReroutesOnResourceExhaustedForBypassTraffic() throws Ex
154164 public void singleUseReadCooldownSkipsReplicaOnNextRequestForBypassTraffic () throws Exception {
155165 try (Spanner spanner = createSpanner (harness )) {
156166 configureBackend (harness , singleRowReadResultSet ("b" ));
157- DatabaseClient client = spanner .getDatabaseClient (DatabaseId .of (PROJECT , INSTANCE , DATABASE ));
167+ DatabaseClient client = spanner .getDatabaseClient (DatabaseId .of (PROJECT , INSTANCE , database ));
158168
159169 seedLocationMetadata (client );
160170 waitForReplicaRoutedRead (client , harness );
@@ -172,7 +182,7 @@ public void singleUseReadCooldownSkipsReplicaOnNextRequestForBypassTraffic() thr
172182 KeySet .singleKey (Key .of ("b" )),
173183 Arrays .asList ("k" ),
174184 Options .directedRead (DIRECTED_READ_OPTIONS ))) {
175- assertTrue (firstRead . next ( ));
185+ assertTrue (drain ( firstRead ));
176186 }
177187
178188 try (ResultSet secondRead =
@@ -183,7 +193,7 @@ public void singleUseReadCooldownSkipsReplicaOnNextRequestForBypassTraffic() thr
183193 KeySet .singleKey (Key .of ("b" )),
184194 Arrays .asList ("k" ),
185195 Options .directedRead (DIRECTED_READ_OPTIONS ))) {
186- assertTrue (secondRead . next ( ));
196+ assertTrue (drain ( secondRead ));
187197 }
188198
189199 String diagnostics = routingDiagnostics (harness );
@@ -210,7 +220,7 @@ public void singleUseReadCooldownSkipsReplicaOnNextRequestForBypassTraffic() thr
210220 public void singleUseReadReroutesOnUnavailableForBypassTraffic () throws Exception {
211221 try (Spanner spanner = createSpanner (harness )) {
212222 configureBackend (harness , singleRowReadResultSet ("b" ));
213- DatabaseClient client = spanner .getDatabaseClient (DatabaseId .of (PROJECT , INSTANCE , DATABASE ));
223+ DatabaseClient client = spanner .getDatabaseClient (DatabaseId .of (PROJECT , INSTANCE , database ));
214224
215225 seedLocationMetadata (client );
216226 waitForReplicaRoutedRead (client , harness );
@@ -227,7 +237,7 @@ public void singleUseReadReroutesOnUnavailableForBypassTraffic() throws Exceptio
227237 KeySet .singleKey (Key .of ("b" )),
228238 Arrays .asList ("k" ),
229239 Options .directedRead (DIRECTED_READ_OPTIONS ))) {
230- assertTrue (resultSet . next ( ));
240+ assertTrue (drain ( resultSet ));
231241 }
232242
233243 String diagnostics = routingDiagnostics (harness );
@@ -247,7 +257,7 @@ public void singleUseReadCooldownSkipsUnavailableReplicaOnNextRequestForBypassTr
247257 throws Exception {
248258 try (Spanner spanner = createSpanner (harness )) {
249259 configureBackend (harness , singleRowReadResultSet ("b" ));
250- DatabaseClient client = spanner .getDatabaseClient (DatabaseId .of (PROJECT , INSTANCE , DATABASE ));
260+ DatabaseClient client = spanner .getDatabaseClient (DatabaseId .of (PROJECT , INSTANCE , database ));
251261
252262 seedLocationMetadata (client );
253263 waitForReplicaRoutedRead (client , harness );
@@ -264,7 +274,7 @@ public void singleUseReadCooldownSkipsUnavailableReplicaOnNextRequestForBypassTr
264274 KeySet .singleKey (Key .of ("b" )),
265275 Arrays .asList ("k" ),
266276 Options .directedRead (DIRECTED_READ_OPTIONS ))) {
267- assertTrue (firstRead . next ( ));
277+ assertTrue (drain ( firstRead ));
268278 }
269279
270280 try (ResultSet secondRead =
@@ -275,7 +285,7 @@ public void singleUseReadCooldownSkipsUnavailableReplicaOnNextRequestForBypassTr
275285 KeySet .singleKey (Key .of ("b" )),
276286 Arrays .asList ("k" ),
277287 Options .directedRead (DIRECTED_READ_OPTIONS ))) {
278- assertTrue (secondRead . next ( ));
288+ assertTrue (drain ( secondRead ));
279289 }
280290
281291 String diagnostics = routingDiagnostics (harness );
@@ -303,7 +313,7 @@ public void singleUseReadMidStreamRecvFailureWithoutRetryInfoRetriesForBypassTra
303313 throws Exception {
304314 try (Spanner spanner = createSpanner (harness )) {
305315 configureBackend (harness , multiRowReadResultSet ("b" , "c" , "d" ));
306- DatabaseClient client = spanner .getDatabaseClient (DatabaseId .of (PROJECT , INSTANCE , DATABASE ));
316+ DatabaseClient client = spanner .getDatabaseClient (DatabaseId .of (PROJECT , INSTANCE , database ));
307317
308318 seedLocationMetadata (client );
309319 waitForReplicaRoutedRead (client , harness );
@@ -354,12 +364,75 @@ public void singleUseReadMidStreamRecvFailureWithoutRetryInfoRetriesForBypassTra
354364 }
355365 }
356366
367+ @ Test
368+ public void readWriteTransactionInlineBeginOnDefaultKeepsReadOnDefaultForBypassTraffic ()
369+ throws Exception {
370+ try (Spanner spanner = createSpanner (harness )) {
371+ configureBackend (harness , singleRowReadResultSet ("b" ), /* leaderReplicaIndex= */ 1 );
372+ DatabaseClient client = spanner .getDatabaseClient (DatabaseId .of (PROJECT , INSTANCE , database ));
373+
374+ seedLocationMetadata (client );
375+ waitForReplicaRoutedStrongRead (client , harness , /* expectedReplicaIndex= */ 1 );
376+ harness .clearRequests ();
377+
378+ client
379+ .readWriteTransaction ()
380+ .run (
381+ transaction -> {
382+ try (ResultSet resultSet = transaction .executeQuery (SEED_QUERY )) {
383+ assertTrue (drain (resultSet ));
384+ }
385+ try (ResultSet resultSet =
386+ transaction .read (TABLE , KeySet .singleKey (Key .of ("b" )), Arrays .asList ("k" ))) {
387+ assertTrue (drain (resultSet ));
388+ }
389+ return null ;
390+ });
391+
392+ String diagnostics = routingDiagnostics (harness );
393+ assertEquals (
394+ "Read after inline begin on default should stay on default.\n " + diagnostics ,
395+ 1 ,
396+ harness
397+ .defaultReplica
398+ .getRequests (SharedBackendReplicaHarness .METHOD_STREAMING_READ )
399+ .size ());
400+ assertEquals (
401+ "Direct replicas should not receive the transaction read.\n " + diagnostics ,
402+ 0 ,
403+ harness
404+ .replicas
405+ .get (0 )
406+ .getRequests (SharedBackendReplicaHarness .METHOD_STREAMING_READ )
407+ .size ()
408+ + harness
409+ .replicas
410+ .get (1 )
411+ .getRequests (SharedBackendReplicaHarness .METHOD_STREAMING_READ )
412+ .size ());
413+ assertEquals (
414+ "Commit should use the default transaction affinity.\n " + diagnostics ,
415+ 1 ,
416+ harness .defaultReplica .getRequests (SharedBackendReplicaHarness .METHOD_COMMIT ).size ());
417+ assertEquals (
418+ "Direct replicas should not receive commit for the default-pinned transaction.\n "
419+ + diagnostics ,
420+ 0 ,
421+ harness .replicas .get (0 ).getRequests (SharedBackendReplicaHarness .METHOD_COMMIT ).size ()
422+ + harness
423+ .replicas
424+ .get (1 )
425+ .getRequests (SharedBackendReplicaHarness .METHOD_COMMIT )
426+ .size ());
427+ }
428+ }
429+
357430 @ Test
358431 public void readWriteTransactionAbortedCommitUsesReadAffinityReplicaForBypassTraffic ()
359432 throws Exception {
360433 try (Spanner spanner = createSpanner (harness )) {
361434 configureBackend (harness , singleRowReadResultSet ("b" ), /* leaderReplicaIndex= */ 1 );
362- DatabaseClient client = spanner .getDatabaseClient (DatabaseId .of (PROJECT , INSTANCE , DATABASE ));
435+ DatabaseClient client = spanner .getDatabaseClient (DatabaseId .of (PROJECT , INSTANCE , database ));
363436
364437 seedLocationMetadata (client );
365438 waitForReplicaRoutedStrongRead (client , harness , /* expectedReplicaIndex= */ 1 );
@@ -374,7 +447,7 @@ public void readWriteTransactionAbortedCommitUsesReadAffinityReplicaForBypassTra
374447 int attempt = attempts .incrementAndGet ();
375448 try (ResultSet resultSet =
376449 transaction .read (TABLE , KeySet .singleKey (Key .of ("b" )), Arrays .asList ("k" ))) {
377- assertTrue (resultSet . next ( ));
450+ assertTrue (drain ( resultSet ));
378451 }
379452
380453 if (attempt == 1 ) {
@@ -486,6 +559,20 @@ private static void seedLocationMetadata(DatabaseClient client) {
486559 }
487560 }
488561
562+ /**
563+ * Fully consumes the result set so the underlying gRPC streaming read closes promptly. Leaving a
564+ * stream open keeps the routed endpoint's active-request count above zero, which inflates its
565+ * selection cost and can bounce the next request to a different replica, making routing
566+ * assertions flaky under load. Returns whether at least one row was seen.
567+ */
568+ private static boolean drain (ResultSet resultSet ) {
569+ boolean sawRow = false ;
570+ while (resultSet .next ()) {
571+ sawRow = true ;
572+ }
573+ return sawRow ;
574+ }
575+
489576 private static int waitForReplicaRoutedRead (
490577 DatabaseClient client , SharedBackendReplicaHarness harness ) throws InterruptedException {
491578 long deadlineNanos = System .nanoTime () + TimeUnit .SECONDS .toNanos (10 );
@@ -498,7 +585,7 @@ private static int waitForReplicaRoutedRead(
498585 KeySet .singleKey (Key .of ("b" )),
499586 Arrays .asList ("k" ),
500587 Options .directedRead (DIRECTED_READ_OPTIONS ))) {
501- if (resultSet . next ( )) {
588+ if (drain ( resultSet )) {
502589 for (int replicaIndex = 0 ; replicaIndex < harness .replicas .size (); replicaIndex ++) {
503590 if (!harness
504591 .replicas
@@ -521,17 +608,18 @@ private static void waitForReplicaRoutedStrongRead(
521608 long deadlineNanos = System .nanoTime () + TimeUnit .SECONDS .toNanos (10 );
522609 while (System .nanoTime () < deadlineNanos ) {
523610 harness .clearRequests ();
611+ boolean sawRow ;
524612 try (ResultSet resultSet =
525613 client .singleUse ().read (TABLE , KeySet .singleKey (Key .of ("b" )), Arrays .asList ("k" ))) {
526- if (resultSet .next ()) {
527- if (!harness
614+ sawRow = drain (resultSet );
615+ }
616+ if (sawRow
617+ && !harness
528618 .replicas
529619 .get (expectedReplicaIndex )
530620 .getRequests (SharedBackendReplicaHarness .METHOD_STREAMING_READ )
531621 .isEmpty ()) {
532- return ;
533- }
534- }
622+ return ;
535623 }
536624 Thread .sleep (50L );
537625 }
0 commit comments