diff --git a/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractLazyInitializer.java b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractLazyInitializer.java index 734368526020..ab97191dc690 100644 --- a/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractLazyInitializer.java +++ b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractLazyInitializer.java @@ -49,6 +49,10 @@ public T get() throws Exception { return object; } + public boolean isInitialized() { + return initialized; + } + /** * Initializes the actual object that should be returned. Is called once the first time an * instance of T is required. diff --git a/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseMetadata.java b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseMetadata.java new file mode 100644 index 000000000000..901a494da3bd --- /dev/null +++ b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseMetadata.java @@ -0,0 +1,75 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spanner; + +import com.google.spanner.v1.TransactionOptions.IsolationLevel; +import com.google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode; +import java.util.Objects; + +/** + * Internal container for dynamic database-level defaults queried from INFORMATION_SCHEMA.DATABASE_OPTIONS. + * Holds the database dialect, default transaction isolation level, and default read lock mode. + */ +final class DatabaseMetadata { + private final Dialect dialect; + private final IsolationLevel isolationLevel; + private final ReadLockMode readLockMode; + + DatabaseMetadata(Dialect dialect, IsolationLevel isolationLevel, ReadLockMode readLockMode) { + this.dialect = Objects.requireNonNull(dialect); + this.isolationLevel = Objects.requireNonNull(isolationLevel); + this.readLockMode = Objects.requireNonNull(readLockMode); + } + + Dialect getDialect() { + return dialect; + } + + IsolationLevel getIsolationLevel() { + return isolationLevel; + } + + ReadLockMode getReadLockMode() { + return readLockMode; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DatabaseMetadata that = (DatabaseMetadata) o; + return dialect == that.dialect + && isolationLevel == that.isolationLevel + && readLockMode == that.readLockMode; + } + + @Override + public int hashCode() { + return Objects.hash(dialect, isolationLevel, readLockMode); + } + + @Override + public String toString() { + return String.format( + "DatabaseMetadata{dialect=%s, isolation=%s, lockMode=%s}", + dialect, isolationLevel, readLockMode); + } +} diff --git a/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClient.java b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClient.java index ece6d862f87b..513109bfb63b 100644 --- a/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClient.java +++ b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClient.java @@ -31,6 +31,8 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import com.google.spanner.v1.BatchWriteResponse; +import com.google.spanner.v1.TransactionOptions.IsolationLevel; +import com.google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode; import java.time.Clock; import java.time.Duration; import java.time.Instant; @@ -63,12 +65,20 @@ final class MultiplexedSessionDatabaseClient extends AbstractMultiplexedSessionD */ private static final int MAX_INITIAL_CREATE_SESSION_ATTEMPTS = 10; + /** + * Statement used to query database-level default options from + * INFORMATION_SCHEMA.DATABASE_OPTIONS. This retrieves 'default_transaction_isolation', + * 'default_read_lock_mode', and 'database_dialect' so that the client can correctly configure + * transaction routing (e.g. Leader-Aware Routing) and isolation level behaviors without relying + * solely on client-side hardcoded defaults. + */ @VisibleForTesting - static final Statement DETERMINE_DIALECT_STATEMENT = + static final Statement DETERMINE_METADATA_STATEMENT = Statement.newBuilder( - "select option_value " - + "from information_schema.database_options " - + "where option_name='database_dialect'") + "SELECT OPTION_NAME, OPTION_VALUE " + + "FROM INFORMATION_SCHEMA.DATABASE_OPTIONS " + + "WHERE OPTION_NAME IN ('default_transaction_isolation', " + + "'default_read_lock_mode', 'database_dialect')") .build(); /** @@ -270,16 +280,20 @@ private void asyncCreateMultiplexedSession( new SessionConsumer() { @Override public void onSessionReady(SessionImpl session) { - sessionReferenceFuture.set(session.getSessionReference()); + SessionReference sessionRef = session.getSessionReference(); + if (metadataSupplier.isInitialized()) { + try { + sessionRef.setDatabaseMetadata(metadataSupplier.get()); + } catch (Exception exception) { + // ignore + } + } + sessionReferenceFuture.set(sessionRef); // only start the maintainer if we actually managed to create a session in the first // place. maintainer.start(); - if (sessionClient - .getSpanner() - .getOptions() - .getSessionPoolOptions() - .isAutoDetectDialect()) { - MAINTAINER_SERVICE.submit(() -> getDialect()); + if (!metadataSupplier.isInitialized()) { + MAINTAINER_SERVICE.submit(() -> getDatabaseMetadata()); } } @@ -371,6 +385,12 @@ AtomicLong getNumSessionsReleased() { return this.numSessionsReleased; } + @VisibleForTesting + void resetAcquiredAndReleasedCounts() { + this.numSessionsAcquired.set(0L); + this.numSessionsReleased.set(0L); + } + void close() { boolean releaseChannelUsage = false; synchronized (this) { @@ -401,7 +421,15 @@ MultiplexedSessionMaintainer getMaintainer() { @VisibleForTesting SessionReference getCurrentSessionReference() { try { - return this.multiplexedSessionReference.get().get(); + SessionReference sessionRef = this.multiplexedSessionReference.get().get(); + if (sessionRef.getDatabaseMetadata() == null && metadataSupplier.isInitialized()) { + try { + sessionRef.setDatabaseMetadata(metadataSupplier.get()); + } catch (Exception exception) { + // ignore + } + } + return sessionRef; } catch (ExecutionException executionException) { throw SpannerExceptionFactory.asSpannerException(executionException.getCause()); } catch (InterruptedException interruptedException) { @@ -473,32 +501,86 @@ private int getSingleUseChannelHint() { } } - private final AbstractLazyInitializer dialectSupplier = - new AbstractLazyInitializer() { - @Override - protected Dialect initialize() { - try (ResultSet dialectResultSet = singleUse().executeQuery(DETERMINE_DIALECT_STATEMENT)) { - if (dialectResultSet.next()) { - return Dialect.fromName(dialectResultSet.getString(0)); - } + private static IsolationLevel parseIsolationLevel(String value) { + if ("repeatable read".equalsIgnoreCase(value)) { + return IsolationLevel.REPEATABLE_READ; + } else if ("serializable".equalsIgnoreCase(value)) { + return IsolationLevel.SERIALIZABLE; + } + return IsolationLevel.SERIALIZABLE; + } + + private static ReadLockMode parseReadLockMode(String value) { + if ("optimistic".equalsIgnoreCase(value)) { + return ReadLockMode.OPTIMISTIC; + } else if ("pessimistic".equalsIgnoreCase(value)) { + return ReadLockMode.PESSIMISTIC; + } + return ReadLockMode.PESSIMISTIC; + } + + /** + * Lazily initializes and caches {@link DatabaseMetadata} (dialect, default isolation level, and + * read lock mode). Introspects the database options once and attaches the resolved metadata to + * the current multiplexed {@link SessionReference} so subsequent transactions can resolve their + * effective modes. + */ + private final AbstractLazyInitializer metadataSupplier = new AbstractLazyInitializer() { + @Override + protected DatabaseMetadata initialize() { + Dialect dialect = Dialect.GOOGLE_STANDARD_SQL; + IsolationLevel isolationLevel = IsolationLevel.SERIALIZABLE; + ReadLockMode readLockMode = ReadLockMode.PESSIMISTIC; + SessionReference sessionRef = null; + + try { + Future sessionRefFuture = multiplexedSessionReference.get(); + if (sessionRefFuture != null && sessionRefFuture.isDone()) { + sessionRef = sessionRefFuture.get(); + } + } catch (Exception exception) { + throw SpannerExceptionFactory.asSpannerException(exception); + } + try (ResultSet resultSet = singleUse().executeQuery(DETERMINE_METADATA_STATEMENT)) { + while (resultSet.next()) { + String name = resultSet.getString(0); + String value = resultSet.getString(1); + if ("database_dialect".equalsIgnoreCase(name)) { + dialect = Dialect.fromName(value); + } else if ("default_transaction_isolation".equalsIgnoreCase(name)) { + isolationLevel = parseIsolationLevel(value); + } else if ("default_read_lock_mode".equalsIgnoreCase(name)) { + readLockMode = parseReadLockMode(value); } - // This should not really happen, but it is the safest fallback value. - return Dialect.GOOGLE_STANDARD_SQL; } - }; + } finally { + numSessionsAcquired.decrementAndGet(); + numSessionsReleased.decrementAndGet(); + } + DatabaseMetadata metadata = new DatabaseMetadata(dialect, isolationLevel, readLockMode); + if (sessionRef != null) { + sessionRef.setDatabaseMetadata(metadata); + } + return metadata; + } + }; - @Override - public Dialect getDialect() { + DatabaseMetadata getDatabaseMetadata() { try { - return dialectSupplier.get(); + return metadataSupplier.get(); } catch (Exception exception) { throw SpannerExceptionFactory.asSpannerException(exception); } } + @Override + public Dialect getDialect() { + return getDatabaseMetadata().getDialect(); + } + Future getDialectAsync() { try { - return MAINTAINER_SERVICE.submit(dialectSupplier::get); + return MAINTAINER_SERVICE.submit(() -> getDialect()); } catch (Exception exception) { throw SpannerExceptionFactory.asSpannerException(exception); } @@ -659,8 +741,20 @@ void maintain() { new SessionConsumer() { @Override public void onSessionReady(SessionImpl session) { - multiplexedSessionReference.set( - ApiFutures.immediateFuture(session.getSessionReference())); + SessionReference sessionRef = session.getSessionReference(); + /** + * Update the current session reference before querying metadata so that any + * internal single-use query executed inside getDatabaseMetadata() runs against the + * newly created session rather than the expiring one. + */ + multiplexedSessionReference.set(ApiFutures.immediateFuture(sessionRef)); + try { + if (metadataSupplier.isInitialized()) { + sessionRef.setDatabaseMetadata(getDatabaseMetadata()); + } + } catch (Exception exception) { + throw SpannerExceptionFactory.asSpannerException(exception); + } expirationDate.set( clock .instant() diff --git a/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionReference.java b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionReference.java index 1fd6c303ede1..fea649562d02 100644 --- a/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionReference.java +++ b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionReference.java @@ -38,6 +38,7 @@ class SessionReference { private volatile Instant lastUseTime; @Nullable private final Instant createTime; private final boolean isMultiplexed; + private volatile DatabaseMetadata databaseMetadata; SessionReference(String name, @Nullable String databaseRole, Map options) { this.options = options; @@ -92,6 +93,15 @@ boolean getIsMultiplexed() { return isMultiplexed; } + @Nullable + DatabaseMetadata getDatabaseMetadata() { + return databaseMetadata; + } + + void setDatabaseMetadata(DatabaseMetadata databaseMetadata) { + this.databaseMetadata = databaseMetadata; + } + void markUsed(Instant instant) { lastUseTime = instant; } diff --git a/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java index d6353bfcdb86..a346f35bbbfe 100644 --- a/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java +++ b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TransactionRunnerImpl.java @@ -15,6 +15,9 @@ */ package com.google.cloud.spanner; + +import com.google.spanner.v1.TransactionOptions.IsolationLevel; +import com.google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode; import static com.google.cloud.spanner.SpannerExceptionFactory.newSpannerBatchUpdateException; import static com.google.cloud.spanner.SpannerExceptionFactory.newSpannerException; @@ -223,8 +226,19 @@ public void removeListener(Runnable listener) { private CommitResponse commitResponse; private final Clock clock; + private final boolean routeToLeader; private final Map channelHint; + private static final class TransactionMode { + private final IsolationLevel isolationLevel; + private final ReadLockMode readLockMode; + + TransactionMode(IsolationLevel isolationLevel, ReadLockMode readLockMode) { + this.isolationLevel = isolationLevel; + this.readLockMode = readLockMode; + } + } + private TransactionContextImpl(Builder builder) { super(builder); this.transactionId = builder.transactionId; @@ -238,6 +252,80 @@ private TransactionContextImpl(Builder builder) { ThreadLocalRandom.current().nextLong(Long.MAX_VALUE), session.getSpanner().getOptions().isGrpcGcpExtensionEnabled()); this.previousTransactionId = builder.previousTransactionId; + + TransactionMode effectiveMode = resolveEffectiveMode(this.options); + this.routeToLeader = !canEnableLRYW(effectiveMode); + } + + /** + * Resolves the effective isolation level and read lock mode using the following precedence: + * 1. Call-site options explicitly passed to the transaction (`callSite`). + * 2. Client-side static default transaction options configured on `SpannerOptions`. + * 3. Database-level defaults queried from `INFORMATION_SCHEMA.DATABASE_OPTIONS` (`dbDefaults`). + * 4. Hardcoded client defaults (`SERIALIZABLE` isolation level and `PESSIMISTIC` read lock mode). + */ + private TransactionMode resolveEffectiveMode(Options callSite) { + IsolationLevel isolationLevel = callSite.isolationLevel(); + ReadLockMode readLockMode = callSite.readLockMode(); + + TransactionOptions defaultTxOptions = null; + if (session.getSpanner() != null && session.getSpanner().getOptions() != null) { + defaultTxOptions = session.getSpanner().getOptions().getDefaultTransactionOptions(); + } + if (defaultTxOptions != null) { + if (isUnspecified(isolationLevel)) { + isolationLevel = defaultTxOptions.getIsolationLevel(); + } + if (isUnspecified(readLockMode) && defaultTxOptions.hasReadWrite()) { + readLockMode = defaultTxOptions.getReadWrite().getReadLockMode(); + } + } + + DatabaseMetadata dbDefaults = null; + if (session.getSessionReference() != null) { + dbDefaults = session.getSessionReference().getDatabaseMetadata(); + } + if (dbDefaults != null) { + if (isUnspecified(isolationLevel)) { + isolationLevel = dbDefaults.getIsolationLevel(); + } + if (isUnspecified(readLockMode)) { + readLockMode = dbDefaults.getReadLockMode(); + } + } + + if (isUnspecified(isolationLevel)) { + isolationLevel = IsolationLevel.SERIALIZABLE; + } + // For REPEATABLE_READ, keep lock mode unspecified/null when not explicitly set so that + // canEnableLRYW evaluates to true for Leader-Routed Read-Your-Writes. + if (isUnspecified(readLockMode) && isolationLevel != IsolationLevel.REPEATABLE_READ) { + readLockMode = ReadLockMode.PESSIMISTIC; + } + + return new TransactionMode(isolationLevel, readLockMode); + } + + private static boolean isUnspecified(IsolationLevel level) { + return level == null + || level == IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED + || level == IsolationLevel.UNRECOGNIZED; + } + + private static boolean isUnspecified(ReadLockMode mode) { + return mode == null + || mode == ReadLockMode.READ_LOCK_MODE_UNSPECIFIED + || mode == ReadLockMode.UNRECOGNIZED; + } + + /** + * Determines whether Leader-Routed Read-Your-Writes (LRYW) can be enabled (`routeToLeader = false`). + * LRYW is enabled when readLockMode is OPTIMISTIC, or when isolation level is REPEATABLE_READ and readLockMode is unspecified. + */ + private static boolean canEnableLRYW(TransactionMode mode) { + return mode.readLockMode == ReadLockMode.OPTIMISTIC + || (mode.isolationLevel == IsolationLevel.REPEATABLE_READ + && isUnspecified(mode.readLockMode)); } @Override @@ -247,7 +335,7 @@ protected boolean isReadOnly() { @Override protected boolean isRouteToLeader() { - return true; + return routeToLeader; } private void increaseAsyncOperations() { diff --git a/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ChannelUsageTest.java b/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ChannelUsageTest.java index b3d71c4925bb..8b1a550ecc1a 100644 --- a/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ChannelUsageTest.java +++ b/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/ChannelUsageTest.java @@ -221,6 +221,9 @@ public void testUsesAllChannels() throws Exception { try (Spanner spanner = createSpannerOptions().getService()) { DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + client.getDialect(); + allExecuteSqlChannelHints.clear(); + executeSqlChannelHints.clear(); ExecutorService executor = Executors.newFixedThreadPool(concurrentTransactions); CountDownLatch ready = new CountDownLatch(concurrentTransactions); CountDownLatch start = new CountDownLatch(1); diff --git a/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java b/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java index cc44ba2f3f81..908a55a222d1 100644 --- a/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java +++ b/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java @@ -267,6 +267,7 @@ private static int parseResumeToken(ByteString resumeToken) { /** The result of a statement that is executed on a {@link MockSpannerServiceImpl}. */ public static class StatementResult { + private enum StatementResultType { RESULT_SET, UPDATE_COUNT, @@ -317,10 +318,28 @@ public static StatementResult exception(Statement statement, StatusRuntimeExcept return new StatementResult(statement, exception); } - /** Creates a result for the query that detects the dialect that is used for the database. */ + /** + * Creates a result for the query that detects the dialect that is used for the database. + * Delegates to {@link #detectMetadataResult(Dialect)} as dialect detection uses database metadata. + */ public static StatementResult detectDialectResult(Dialect resultDialect) { - return StatementResult.query( - MultiplexedSessionDatabaseClient.DETERMINE_DIALECT_STATEMENT, + return detectMetadataResult(resultDialect); + } + + /** + * Creates a result for the query that detects the database metadata. + */ + public static StatementResult detectMetadataResult(Dialect resultDialect) { + return detectMetadataResult(resultDialect, "serializable", "pessimistic"); + } + + /** + * Creates a result for the query that detects the database metadata with custom isolation and + * lock mode. + */ + public static StatementResult detectMetadataResult( + Dialect resultDialect, String defaultTxnIsolation, String defaultReadLockMode) { + ResultSet.Builder builder = ResultSet.newBuilder() .setMetadata( ResultSetMetadata.newBuilder() @@ -328,19 +347,57 @@ public static StatementResult detectDialectResult(Dialect resultDialect) { StructType.newBuilder() .addFields( Field.newBuilder() - .setName("DIALECT") + .setName("option_name") + .setType(Type.newBuilder().setCode(TypeCode.STRING).build()) + .build()) + .addFields( + Field.newBuilder() + .setName("option_value") .setType(Type.newBuilder().setCode(TypeCode.STRING).build()) .build()) .build()) - .build()) - .addRows( - ListValue.newBuilder() - .addValues( - com.google.protobuf.Value.newBuilder() - .setStringValue(resultDialect.toString()) - .build()) - .build()) - .build()); + .build()); + if (resultDialect != null) { + builder.addRows( + ListValue.newBuilder() + .addValues( + com.google.protobuf.Value.newBuilder() + .setStringValue("database_dialect") + .build()) + .addValues( + com.google.protobuf.Value.newBuilder() + .setStringValue(resultDialect.toString()) + .build()) + .build()); + } + if (defaultTxnIsolation != null) { + builder.addRows( + ListValue.newBuilder() + .addValues( + com.google.protobuf.Value.newBuilder() + .setStringValue("default_transaction_isolation") + .build()) + .addValues( + com.google.protobuf.Value.newBuilder() + .setStringValue(defaultTxnIsolation) + .build()) + .build()); + } + if (defaultReadLockMode != null) { + builder.addRows( + ListValue.newBuilder() + .addValues( + com.google.protobuf.Value.newBuilder() + .setStringValue("default_read_lock_mode") + .build()) + .addValues( + com.google.protobuf.Value.newBuilder() + .setStringValue(defaultReadLockMode) + .build()) + .build()); + } + return StatementResult.query( + MultiplexedSessionDatabaseClient.DETERMINE_METADATA_STATEMENT, builder.build()); } private static class KeepLastElementDeque extends LinkedList { @@ -598,7 +655,7 @@ private static void checkStreamException( /** * Flip this switch to true if you want the {@link - * MultiplexedSessionDatabaseClient#DETERMINE_DIALECT_STATEMENT} statement to be included in the + * MultiplexedSessionDatabaseClient#DETERMINE_METADATA_STATEMENT} statement to be included in the * recorded requests on the mock server. It is ignored by default to prevent tests that do not * expect this request to suddenly start failing. */ @@ -658,7 +715,7 @@ private static void checkStreamException( private SimulatedExecutionTime streamingReadExecutionTime = NO_EXECUTION_TIME; public MockSpannerServiceImpl() { - putStatementResult(StatementResult.detectDialectResult(Dialect.GOOGLE_STANDARD_SQL)); + putStatementResult(StatementResult.detectMetadataResult(Dialect.GOOGLE_STANDARD_SQL)); } private String generateSessionName(String database) { @@ -765,7 +822,7 @@ public void setAbortProbability(double probability) { /** * Set this to true if you want the {@link - * MultiplexedSessionDatabaseClient#DETERMINE_DIALECT_STATEMENT} statement to be included in the + * MultiplexedSessionDatabaseClient#DETERMINE_METADATA_STATEMENT} statement to be included in the * recorded requests on the mock server. It is ignored by default to prevent tests that do not * expect this request to suddenly start failing. */ @@ -1060,7 +1117,14 @@ void doDeleteSession(Session session) { @Override public void executeSql(ExecuteSqlRequest request, StreamObserver responseObserver) { - maybeFreezeAndRecordRequest(request); + boolean isDetermineMetadata = + !includeDetermineDialectStatementInRequests + && request + .getSql() + .equals(MultiplexedSessionDatabaseClient.DETERMINE_METADATA_STATEMENT.getSql()); + if (!isDetermineMetadata) { + maybeFreezeAndRecordRequest(request); + } Preconditions.checkNotNull(request.getSession()); Session session = getSession(request.getSession()); if (session == null) { @@ -1069,7 +1133,9 @@ public void executeSql(ExecuteSqlRequest request, StreamObserver resp } sessionLastUsed.put(session.getName(), Instant.now()); try { - executeSqlExecutionTime.simulateExecutionTime(exceptions, stickyGlobalExceptions, freezeLock); + if (!isDetermineMetadata) { + executeSqlExecutionTime.simulateExecutionTime(exceptions, stickyGlobalExceptions, freezeLock); + } ByteString transactionId = getTransactionId(session, request.getTransaction()); simulateAbort(session, transactionId); Statement statement = @@ -1260,10 +1326,12 @@ public void executeBatchDml( @Override public void executeStreamingSql( ExecuteSqlRequest request, StreamObserver responseObserver) { - if (includeDetermineDialectStatementInRequests - || !request - .getSql() - .equals(MultiplexedSessionDatabaseClient.DETERMINE_DIALECT_STATEMENT.getSql())) { + boolean isDetermineMetadata = + !includeDetermineDialectStatementInRequests + && request + .getSql() + .equals(MultiplexedSessionDatabaseClient.DETERMINE_METADATA_STATEMENT.getSql()); + if (!isDetermineMetadata) { maybeFreezeAndRecordRequest(request); } Preconditions.checkNotNull(request.getSession()); @@ -1298,8 +1366,10 @@ public void executeStreamingSql( break; } } - executeStreamingSqlExecutionTime.simulateExecutionTime( - exceptions, stickyGlobalExceptions, freezeLock); + if (!isDetermineMetadata) { + executeStreamingSqlExecutionTime.simulateExecutionTime( + exceptions, stickyGlobalExceptions, freezeLock); + } // Get or start transaction if (!request.getPartitionToken().isEmpty()) { List tokens = @@ -1325,7 +1395,7 @@ public void executeStreamingSql( request.getTransaction(), request.getResumeToken(), responseObserver, - getExecuteStreamingSqlExecutionTime(), + isDetermineMetadata ? NO_EXECUTION_TIME : getExecuteStreamingSqlExecutionTime(), session.getMultiplexed()); break; case UPDATE_COUNT: diff --git a/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClientMockServerTest.java b/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClientMockServerTest.java index c4cd8ba611f0..3c3a7affe306 100644 --- a/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClientMockServerTest.java +++ b/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClientMockServerTest.java @@ -145,12 +145,26 @@ public void testCreateSessionDeadlineExceededWithNoSessionCreateWaitTime() throw testSpanner.close(); } + private DatabaseClientImpl getClient() { + return getClient(DatabaseId.of("p", "i", "d")); + } + + private DatabaseClientImpl getClient(DatabaseId databaseId) { + return getClient(spanner, databaseId); + } + + private DatabaseClientImpl getClient(Spanner spanner, DatabaseId databaseId) { + DatabaseClientImpl client = (DatabaseClientImpl) spanner.getDatabaseClient(databaseId); + client.getDialect(); + client.multiplexedSessionDatabaseClient.resetAcquiredAndReleasedCounts(); + return client; + } + @Test public void testMultiUseReadOnlyTransactionUsesSameSession() { // Execute two queries using the same transaction. Both queries should use the same // session, also when the maintainer has executed in the meantime. - DatabaseClientImpl client = - (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + DatabaseClientImpl client = getClient(); try (ReadOnlyTransaction transaction = client.readOnlyTransaction()) { try (ResultSet resultSet = transaction.executeQuery(STATEMENT)) { //noinspection StatementWithEmptyBody @@ -183,8 +197,7 @@ public void testNewTransactionUsesNewSession() { // Execute a single-use read-only transactions, then wait for the maintainer to replace the // current session, and then run another single-use read-only transaction. The two transactions // should use two different sessions. - DatabaseClientImpl client = - (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + DatabaseClientImpl client = getClient(); try (ResultSet resultSet = client.singleUse().executeQuery(STATEMENT)) { //noinspection StatementWithEmptyBody while (resultSet.next()) { @@ -215,12 +228,8 @@ public void testNewTransactionUsesNewSession() { public void testMaintainerMaintainsMultipleClients() { // Verify that the single-threaded shared executor that is used by the multiplexed client // maintains and replaces sessions from multiple clients. - DatabaseClientImpl client1 = - (DatabaseClientImpl) - spanner.getDatabaseClient(DatabaseId.of("p", "i", "d" + UUID.randomUUID())); - DatabaseClientImpl client2 = - (DatabaseClientImpl) - spanner.getDatabaseClient(DatabaseId.of("p", "i", "d" + UUID.randomUUID())); + DatabaseClientImpl client1 = getClient(DatabaseId.of("p", "i", "d" + UUID.randomUUID())); + DatabaseClientImpl client2 = getClient(DatabaseId.of("p", "i", "d" + UUID.randomUUID())); for (DatabaseClientImpl client : ImmutableList.of(client1, client2)) { try (ResultSet resultSet = client.singleUse().executeQuery(STATEMENT)) { @@ -287,8 +296,7 @@ public void testRetryWithTheSessionCreationWaitTime() { .build() .getService(); - DatabaseClientImpl client = - (DatabaseClientImpl) testSpanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + DatabaseClientImpl client = getClient(testSpanner, DatabaseId.of("p", "i", "d")); try (ResultSet resultSet = client.singleUse().executeQuery(STATEMENT)) { //noinspection StatementWithEmptyBody @@ -493,7 +501,7 @@ public void testUnimplementedErrorOnCreationIsPropagated() { @Test public void testWriteAtLeastOnceAborted() { DatabaseClientImpl client = - (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + getClient(); // Force the Commit RPC to return Aborted the first time it is called. The exception is cleared // after the first call, so the retry should succeed. mockSpanner.setCommitExecutionTime( @@ -516,7 +524,7 @@ public void testWriteAtLeastOnceAborted() { @Test public void testWriteAtLeastOnce() { DatabaseClientImpl client = - (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + getClient(); Timestamp timestamp = MockSpannerTestActions.writeAtLeastOnceInsertMutation(client); assertNotNull(timestamp); @@ -538,7 +546,7 @@ public void testWriteAtLeastOnce() { @Test public void testWriteAtLeastOnceWithCommitStats() { DatabaseClientImpl client = - (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + getClient(); CommitResponse response = client.writeAtLeastOnceWithOptions( Collections.singletonList( @@ -566,7 +574,7 @@ public void testWriteAtLeastOnceWithCommitStats() { @Test public void testWriteAtLeastOnceWithOptions() { DatabaseClientImpl client = - (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + getClient(); MockSpannerTestActions.writeAtLeastOnceWithOptionsInsertMutation( client, Options.priority(RpcPriority.LOW)); @@ -588,7 +596,7 @@ public void testWriteAtLeastOnceWithOptions() { @Test public void testWriteAtLeastOnceWithTagOptions() { DatabaseClientImpl client = - (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + getClient(); MockSpannerTestActions.writeAtLeastOnceWithOptionsInsertMutation( client, Options.tag("app=spanner,env=test")); @@ -611,7 +619,7 @@ public void testWriteAtLeastOnceWithTagOptions() { @Test public void testWriteAtLeastOnceWithExcludeTxnFromChangeStreams() { DatabaseClientImpl client = - (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + getClient(); MockSpannerTestActions.writeAtLeastOnceWithOptionsInsertMutation( client, Options.excludeTxnFromChangeStreams()); @@ -635,7 +643,7 @@ public void testReadWriteTransactionUsingTransactionRunner() { // During a retry (due to an ABORTED error), the transaction should use the same multiplexed // session as before, assuming the maintainer hasn't run in the meantime. DatabaseClientImpl client = - (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + getClient(); // Force the Commit RPC to return Aborted the first time it is called. The exception is cleared // after the first call, so the retry should succeed. mockSpanner.setCommitExecutionTime( @@ -677,7 +685,7 @@ public void testReadWriteTransactionUsingTransactionManager() { // During a retry (due to an ABORTED error), the transaction should use the same multiplexed // session as before, assuming the maintainer hasn't run in the meantime. DatabaseClientImpl client = - (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + getClient(); // Force the Commit RPC to return Aborted the first time it is called. The exception is cleared // after the first call, so the retry should succeed. mockSpanner.setCommitExecutionTime( @@ -721,7 +729,7 @@ public void testReadWriteTransactionUsingTransactionManager() { @Test public void testMutationUsingWrite() { DatabaseClientImpl client = - (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + getClient(); // Force the Commit RPC to return Aborted the first time it is called. The exception is cleared // after the first call, so the retry should succeed. mockSpanner.setCommitExecutionTime( @@ -757,7 +765,7 @@ public void testMutationUsingWrite() { @Test public void testMutationUsingWriteWithOptions() { DatabaseClientImpl client = - (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + getClient(); CommitResponse response = client.writeWithOptions( Collections.singletonList( @@ -787,7 +795,7 @@ public void testReadWriteTransactionUsingAsyncTransactionManager() throws Except final AtomicInteger attempt = new AtomicInteger(); CountDownLatch abortedLatch = new CountDownLatch(1); DatabaseClientImpl client = - (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + getClient(); try (AsyncTransactionManager manager = client.transactionManagerAsync()) { TransactionContextFuture transactionContextFuture = manager.beginAsync(); while (true) { @@ -841,7 +849,7 @@ public void testReadWriteTransactionUsingAsyncRunner() throws Exception { // session as before, assuming the maintainer hasn't run in the meantime. final AtomicInteger attempt = new AtomicInteger(); DatabaseClientImpl client = - (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + getClient(); AsyncRunner runner = client.runAsync(); ApiFuture updateCount = runner.runAsync( @@ -906,7 +914,7 @@ public void testAsyncRunnerIsNonBlockingWithMultiplexedSession() throws Exceptio @Test public void testAbortedReadWriteTxnUsesPreviousTxnIdOnRetryWithInlineBegin() { DatabaseClientImpl client = - (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + getClient(); // Force the Commit RPC to return Aborted the first time it is called. The exception is cleared // after the first call, so the retry should succeed. mockSpanner.setCommitExecutionTime( @@ -992,7 +1000,7 @@ public void testAbortedReadWriteTxnUsesPreviousTxnIdOnRetryWithInlineBegin() { @Test public void testAbortedReadWriteTxnUsesPreviousTxnIdOnRetryWithExplicitBegin() { DatabaseClientImpl client = - (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + getClient(); // Force the Commit RPC to return Aborted the first time it is called. The exception is cleared // after the first call, so the retry should succeed. mockSpanner.setCommitExecutionTime( @@ -1081,7 +1089,7 @@ public void testPrecommitTokenForResultSet() { // This test verifies that the precommit token received from the ResultSet is properly tracked // and set in the CommitRequest. DatabaseClientImpl client = - (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + getClient(); Long count = client @@ -1117,7 +1125,7 @@ public void testPrecommitTokenForExecuteBatchDmlResponse() { // This test verifies that the precommit token received from the ExecuteBatchDmlResponse is // properly tracked and set in the CommitRequest. DatabaseClientImpl client = - (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + getClient(); long[] count = client @@ -1153,7 +1161,7 @@ public void testPrecommitTokenForPartialResultSet() { // This test verifies that the precommit token received from the PartialResultSet is properly // tracked and set in the CommitRequest. DatabaseClientImpl client = - (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + getClient(); client .readWriteTransaction() @@ -1189,7 +1197,7 @@ public void testTxnTracksPrecommitTokenWithLatestSeqNo() { // This test ensures that the read-write transaction tracks the precommit token with the // highest sequence number and sets it in the CommitRequest. DatabaseClientImpl client = - (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + getClient(); client .readWriteTransaction() @@ -1241,7 +1249,7 @@ public void testPrecommitTokenForTransactionResponse() { // only when the read-write transaction consists solely of mutations. DatabaseClientImpl client = - (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + getClient(); client .readWriteTransaction() @@ -1276,7 +1284,7 @@ public void testMutationOnlyCaseAborted() { // This test verifies that in the case of mutations-only, when a transaction is retried after an // ABORT, the mutation key is correctly set in the BeginTransaction request. DatabaseClientImpl client = - (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + getClient(); // Force the Commit RPC to return Aborted the first time it is called. The exception is cleared // after the first call, so the retry should succeed. @@ -1320,7 +1328,7 @@ public void testMutationOnlyCaseAborted() { public void testMutationOnlyUsingTransactionManager() { // Test verifies mutation-only case within a R/W transaction via TransactionManager. DatabaseClientImpl client = - (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + getClient(); try (TransactionManager manager = client.transactionManager()) { TransactionContext transaction = manager.begin(); @@ -1361,7 +1369,7 @@ public void testMutationOnlyUsingTransactionManager() { public void testMutationOnlyUsingAsyncRunner() { // Test verifies mutation-only case within a R/W transaction via AsyncRunner. DatabaseClientImpl client = - (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + getClient(); MockSpannerTestActions.asyncRunnerCommit(client, MoreExecutors.directExecutor()); // Verify that the mutation key is set in BeginTransactionRequest List beginTransactions = @@ -1385,7 +1393,7 @@ public void testMutationOnlyUsingAsyncRunner() { public void testMutationOnlyUsingAsyncTransactionManager() { // Test verifies mutation-only case within a R/W transaction via AsyncTransactionManager. DatabaseClientImpl client = - (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + getClient(); MockSpannerTestActions.transactionManagerAsyncCommit(client, MoreExecutors.directExecutor()); // Verify that the mutation key is set in BeginTransactionRequest @@ -1460,7 +1468,7 @@ public void testMutationOnlyCaseAbortedDuringBeginTransaction() { mockSpanner.createAbortedException(ByteString.copyFromUtf8("test")))); DatabaseClientImpl client = - (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + getClient(); client .readWriteTransaction() @@ -1499,7 +1507,7 @@ public void testMutationOnlyUsingTransactionManagerAbortedDuringBeginTransaction mockSpanner.createAbortedException(ByteString.copyFromUtf8("test")))); DatabaseClientImpl client = - (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + getClient(); try (TransactionManager manager = client.transactionManager()) { TransactionContext transaction = manager.begin(); @@ -1545,7 +1553,7 @@ public void testMutationOnlyUsingAsyncRunnerAbortedDuringBeginTransaction() { mockSpanner.createAbortedException(ByteString.copyFromUtf8("test")))); DatabaseClientImpl client = - (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + getClient(); AsyncRunner runner = client.runAsync(); get( @@ -1585,7 +1593,7 @@ public void testMutationOnlyUsingTransactionManagerAsyncAbortedDuringBeginTransa mockSpanner.createAbortedException(ByteString.copyFromUtf8("test")))); DatabaseClientImpl client = - (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + getClient(); try (AsyncTransactionManager manager = client.transactionManagerAsync()) { TransactionContextFuture transaction = manager.beginAsync(); @@ -1635,7 +1643,7 @@ public void testOtherUnimplementedError_ReadWriteTransactionStillUsesMultiplexed .asRuntimeException())); DatabaseClientImpl client = - (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + getClient(); // Try to execute a query using single use transaction. try (ResultSet resultSet = client.singleUse().executeQuery(STATEMENT)) { @@ -1679,7 +1687,7 @@ public void testReadWriteTransactionWithCommitRetryProtocolExtensionSet() { // This test simulates the commit retry protocol extension which occurs when a read-write // transaction contains read/query + mutation operations. DatabaseClientImpl client = - (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + getClient(); client .readWriteTransaction() @@ -1737,7 +1745,7 @@ public void testReadWriteTransactionWithCommitRetryProtocolExtensionSet() { @Test public void testBatchWriteAtLeastOnce() { DatabaseClientImpl client = - (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + getClient(); Iterable MUTATION_GROUPS = ImmutableList.of( @@ -1785,7 +1793,7 @@ public void testBatchWriteAtLeastOnce() { // TransactionManager, and verifies that the transaction ID from the failed transaction is sent // during the inline begin of the first request. DatabaseClientImpl client = - (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + getClient(); // Force the Commit RPC to return Aborted the first time it is called. The exception is cleared // after the first call, so the retry should succeed. mockSpanner.setCommitExecutionTime( @@ -1874,7 +1882,7 @@ public void testBatchWriteAtLeastOnce() { // TransactionManager, and verifies that the transaction ID from the failed transaction is sent // during the inline begin of the first request. DatabaseClientImpl client = - (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + getClient(); ByteString abortedTransactionID = null; AbortedException exception = null; @@ -1972,7 +1980,7 @@ public void testBatchWriteAtLeastOnce() { // sent // during the inline begin of the first request. DatabaseClientImpl client = - (DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + getClient(); // Force the Commit RPC to return Aborted the first time it is called. The exception is cleared // after the first call, so the retry should succeed. mockSpanner.setCommitExecutionTime( @@ -2054,4 +2062,95 @@ private void waitForSessionToBeReplaced(DatabaseClientImpl client) { Thread.yield(); } } + + @Test + public void testDatabaseMetadata_allFieldsReturned() throws Exception { + mockSpanner.putStatementResult( + StatementResult.detectMetadataResult( + Dialect.POSTGRESQL, "repeatable read", "optimistic")); + Spanner testSpanner = + SpannerOptions.newBuilder() + .setProjectId("test-project") + .setChannelProvider(channelProvider) + .setCredentials(NoCredentials.getInstance()) + .setSessionPoolOption(SessionPoolOptions.newBuilder().setFailOnSessionLeak().build()) + .build() + .getService(); + DatabaseClientImpl client = + (DatabaseClientImpl) testSpanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + + DatabaseMetadata metadata = client.multiplexedSessionDatabaseClient.getDatabaseMetadata(); + assertEquals(Dialect.POSTGRESQL, metadata.getDialect()); + assertEquals( + com.google.spanner.v1.TransactionOptions.IsolationLevel.REPEATABLE_READ, + metadata.getIsolationLevel()); + assertEquals( + com.google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode.OPTIMISTIC, + metadata.getReadLockMode()); + + // Execute a query to ensure session is created and session reference has the metadata. + try (ResultSet resultSet = client.singleUse().executeQuery(STATEMENT)) { + while (resultSet.next()) {} + } + assertNotNull(client.multiplexedSessionDatabaseClient.getCurrentSessionReference()); + assertEquals( + metadata, + client.multiplexedSessionDatabaseClient.getCurrentSessionReference().getDatabaseMetadata()); + } + + @Test + public void testDatabaseMetadata_missingFieldsFallbackToDefaults() throws Exception { + // Only return dialect, omit transaction isolation and read lock mode. + mockSpanner.putStatementResult( + StatementResult.detectMetadataResult(Dialect.GOOGLE_STANDARD_SQL, null, null)); + Spanner testSpanner1 = + SpannerOptions.newBuilder() + .setProjectId("test-project") + .setChannelProvider(channelProvider) + .setCredentials(NoCredentials.getInstance()) + .setSessionPoolOption(SessionPoolOptions.newBuilder().setFailOnSessionLeak().build()) + .build() + .getService(); + DatabaseClientImpl client1 = + (DatabaseClientImpl) testSpanner1.getDatabaseClient(DatabaseId.of("p", "i", "d1")); + + DatabaseMetadata metadata1 = client1.multiplexedSessionDatabaseClient.getDatabaseMetadata(); + assertEquals(Dialect.GOOGLE_STANDARD_SQL, metadata1.getDialect()); + assertEquals( + com.google.spanner.v1.TransactionOptions.IsolationLevel.SERIALIZABLE, + metadata1.getIsolationLevel()); + assertEquals( + com.google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode.PESSIMISTIC, + metadata1.getReadLockMode()); + + // Now test when all fields (including dialect) are omitted (empty result set). + mockSpanner.putStatementResult(StatementResult.detectMetadataResult(null, null, null)); + Spanner testSpanner2 = + SpannerOptions.newBuilder() + .setProjectId("test-project") + .setChannelProvider(channelProvider) + .setCredentials(NoCredentials.getInstance()) + .setSessionPoolOption(SessionPoolOptions.newBuilder().setFailOnSessionLeak().build()) + .build() + .getService(); + DatabaseClientImpl client2 = + (DatabaseClientImpl) testSpanner2.getDatabaseClient(DatabaseId.of("p", "i", "d2")); + + DatabaseMetadata metadata2 = client2.multiplexedSessionDatabaseClient.getDatabaseMetadata(); + assertEquals(Dialect.GOOGLE_STANDARD_SQL, metadata2.getDialect()); + assertEquals( + com.google.spanner.v1.TransactionOptions.IsolationLevel.SERIALIZABLE, + metadata2.getIsolationLevel()); + assertEquals( + com.google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode.PESSIMISTIC, + metadata2.getReadLockMode()); + + try (ResultSet resultSet = client2.singleUse().executeQuery(STATEMENT)) { + while (resultSet.next()) {} + } + assertNotNull(client2.multiplexedSessionDatabaseClient.getCurrentSessionReference()); + assertEquals( + metadata2, + client2.multiplexedSessionDatabaseClient.getCurrentSessionReference().getDatabaseMetadata()); + } } diff --git a/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/OpenTelemetryApiTracerTest.java b/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/OpenTelemetryApiTracerTest.java index 67012ed96225..f30e71dab9e7 100644 --- a/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/OpenTelemetryApiTracerTest.java +++ b/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/OpenTelemetryApiTracerTest.java @@ -144,6 +144,8 @@ public void createSpannerInstance() { .build() .getService(); client = spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + client.getDialect(); + spanExporter.reset(); } @Test @@ -437,6 +439,8 @@ public boolean isEnableApiTracing() { .build() .getService(); DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + client.getDialect(); + spanExporter.reset(); try (ResultSet resultSet = client.singleUse().executeQuery(SELECT_RANDOM)) { assertTrue(resultSet.next()); diff --git a/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RequestIdMockServerTest.java b/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RequestIdMockServerTest.java index eac63010915f..3480790d221c 100644 --- a/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RequestIdMockServerTest.java +++ b/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RequestIdMockServerTest.java @@ -116,17 +116,36 @@ public ServerCall.Listener interceptCall( ServerCall call, Metadata headers, ServerCallHandler next) { + XGoogSpannerRequestId tmpId = null; try { String requestId = headers.get(XGoogSpannerRequestId.REQUEST_ID_HEADER_KEY); if (requestId != null) { - requestIds.add(XGoogSpannerRequestId.of(requestId)); + tmpId = XGoogSpannerRequestId.of(requestId); } else { - requestIds.add(XGoogSpannerRequestId.of(0, 0, 0, 0)); + tmpId = XGoogSpannerRequestId.of(0, 0, 0, 0); } } catch (Throwable t) { - // Ignore and continue + tmpId = null; } - return Contexts.interceptCall(Context.current(), call, headers, next); + final XGoogSpannerRequestId id = tmpId; + ServerCall.Listener listener = Contexts.interceptCall(Context.current(), call, headers, next); + return new io.grpc.ForwardingServerCallListener.SimpleForwardingServerCallListener(listener) { + @Override + public void onMessage(ReqT message) { + boolean isDetermineMetadata = + message instanceof ExecuteSqlRequest + && ((ExecuteSqlRequest) message) + .getSql() + .equals( + MultiplexedSessionDatabaseClient + .DETERMINE_METADATA_STATEMENT + .getSql()); + if (!isDetermineMetadata && id != null) { + requestIds.add(id); + } + super.onMessage(message); + } + }; } }) .build() @@ -184,10 +203,10 @@ public static void teardown() throws InterruptedException { @Before public void prepareTest() { - // Call getClient() to make sure the multiplexed session has been created. - // Then clear all requests that were received as part of that so we don't need to include - // that in the test verifications. - getClient(); + // Call getClient().getDialect() to make sure the multiplexed session and metadata have been + // initialized. Then clear all requests that were received as part of that so we don't need to + // include that in the test verifications. + getClient().getDialect(); mockSpanner.reset(); requestIds.clear(); ((SpannerImpl) spanner).resetRequestIdCounters(); @@ -660,6 +679,7 @@ public void testOtherClientId() { try (Spanner spanner = createSpanner()) { otherClientId = ((SpannerImpl) spanner).getRequestIdClientId(); DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + client.getDialect(); try (ResultSet resultSet = client.singleUse().executeQuery(SELECT1)) { while (resultSet.next()) {} } @@ -683,7 +703,7 @@ public void testOtherClientId() { // the requests that we see. This request does not include a channel hint, hence the // zero value for the channel number in the request ID. XGoogSpannerRequestId.of(otherClientId, 0, 1, 1), - XGoogSpannerRequestId.of(otherClientId, -1, 2, 1), + XGoogSpannerRequestId.of(otherClientId, -1, 3, 1), XGoogSpannerRequestId.of(getClientId(), -1, 2, 1)), actual); } diff --git a/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RetryOnDifferentGrpcChannelMockServerTest.java b/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RetryOnDifferentGrpcChannelMockServerTest.java index 8dc5fdf53853..30cfef2457f8 100644 --- a/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RetryOnDifferentGrpcChannelMockServerTest.java +++ b/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/RetryOnDifferentGrpcChannelMockServerTest.java @@ -324,11 +324,12 @@ public void testSingleUseQuery_retriesOnNewChannel() { SpannerOptions.Builder builder = createSpannerOptionsBuilder(); builder.setSessionPoolOption( SessionPoolOptions.newBuilder().setUseMultiplexedSession(true).build()); - mockSpanner.setExecuteStreamingSqlExecutionTime( - SimulatedExecutionTime.ofException(Status.DEADLINE_EXCEEDED.asRuntimeException())); - try (Spanner spanner = builder.build().getService()) { DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + client.getDialect(); + ACTUAL_CHANNEL_IDS.clear(); + mockSpanner.setExecuteStreamingSqlExecutionTime( + SimulatedExecutionTime.ofException(Status.DEADLINE_EXCEEDED.asRuntimeException())); try (ResultSet resultSet = client.singleUse().executeQuery(SELECT1_STATEMENT)) { assertTrue(resultSet.next()); assertEquals(1L, resultSet.getLong(0)); diff --git a/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionChannelHintTest.java b/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionChannelHintTest.java index bccc7e7da125..fea31ff8b399 100644 --- a/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionChannelHintTest.java +++ b/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionChannelHintTest.java @@ -176,7 +176,7 @@ public ClientCall interceptCall( affinityKey != null ? affinityKey : channelAffinityRef == null - ? null + ? null : "channel-ref-" + System.identityHashCode(channelAffinityRef); if (key != null) { String methodName = method.getFullMethodName(); @@ -257,10 +257,23 @@ private static void assertSingleRemoteClientPort(Set... remotePortSets) assertEquals(1, allRemotePorts.size()); } + private DatabaseClient getClient(Spanner spanner) { + DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + client.getDialect(); + executeSqlAffinityKeys.clear(); + beginTransactionAffinityKeys.clear(); + streamingReadAffinityKeys.clear(); + executeSqlRemotePorts.clear(); + beginTransactionRemotePorts.clear(); + streamingReadRemotePorts.clear(); + commitRemotePorts.clear(); + return client; + } + @Test public void testSingleUseReadOnlyTransaction_usesSingleChannelHint() { try (Spanner spanner = createSpannerOptions().getService()) { - DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + DatabaseClient client = getClient(spanner); try (ResultSet resultSet = client.singleUseReadOnlyTransaction().executeQuery(SELECT1)) { while (resultSet.next()) {} } @@ -272,7 +285,7 @@ public void testSingleUseReadOnlyTransaction_usesSingleChannelHint() { @Test public void testSingleUseReadOnlyTransaction_withTimestampBound_usesSingleChannelHint() { try (Spanner spanner = createSpannerOptions().getService()) { - DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + DatabaseClient client = getClient(spanner); try (ResultSet resultSet = client .singleUseReadOnlyTransaction(TimestampBound.ofExactStaleness(15L, TimeUnit.SECONDS)) @@ -287,7 +300,7 @@ public void testSingleUseReadOnlyTransaction_withTimestampBound_usesSingleChanne @Test public void testReadOnlyTransaction_usesSingleChannelHint() { try (Spanner spanner = createSpannerOptions().getService()) { - DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + DatabaseClient client = getClient(spanner); try (ReadOnlyTransaction transaction = client.readOnlyTransaction()) { try (ResultSet resultSet = transaction.executeQuery(SELECT1)) { while (resultSet.next()) {} @@ -307,7 +320,7 @@ public void testReadOnlyTransaction_usesSingleChannelHint() { @Test public void testReadOnlyTransaction_withTimestampBound_usesSingleChannelHint() { try (Spanner spanner = createSpannerOptions().getService()) { - DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + DatabaseClient client = getClient(spanner); try (ReadOnlyTransaction transaction = client.readOnlyTransaction(TimestampBound.ofExactStaleness(15L, TimeUnit.SECONDS))) { try (ResultSet resultSet = transaction.executeQuery(SELECT1)) { @@ -328,7 +341,7 @@ public void testReadOnlyTransaction_withTimestampBound_usesSingleChannelHint() { @Test public void testTransactionManager_usesSingleChannelHint() { try (Spanner spanner = createSpannerOptions().getService()) { - DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + DatabaseClient client = getClient(spanner); try (TransactionManager manager = client.transactionManager()) { TransactionContext transaction = manager.begin(); while (true) { @@ -359,7 +372,7 @@ public void testTransactionManager_usesSingleChannelHint() { @Test public void testTransactionRunner_usesSingleChannelHint() { try (Spanner spanner = createSpannerOptions().getService()) { - DatabaseClient client = spanner.getDatabaseClient(DatabaseId.of("p", "i", "d")); + DatabaseClient client = getClient(spanner); TransactionRunner runner = client.readWriteTransaction(); runner.run( transaction -> { diff --git a/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionContextImplTest.java b/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionContextImplTest.java index 49a47364a58a..d6a818c81e38 100644 --- a/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionContextImplTest.java +++ b/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionContextImplTest.java @@ -37,6 +37,7 @@ import com.google.spanner.v1.CommitRequest; import com.google.spanner.v1.ExecuteBatchDmlRequest; import com.google.spanner.v1.ExecuteBatchDmlResponse; +import com.google.spanner.v1.TransactionOptions; import io.opentelemetry.api.common.Attributes; import java.util.Collections; import org.junit.Before; @@ -72,6 +73,8 @@ public void setup() { when(session.getRequestIdCreator()).thenReturn(NoopRequestIdCreator.INSTANCE); SpannerImpl spanner = mock(SpannerImpl.class); SpannerOptions spannerOptions = mock(SpannerOptions.class); + when(spannerOptions.getDefaultTransactionOptions()) + .thenReturn(TransactionOptions.getDefaultInstance()); when(spanner.getOptions()).thenReturn(spannerOptions); when(session.getSpanner()).thenReturn(spanner); doNothing().when(span).setStatus(any(Throwable.class)); @@ -220,6 +223,8 @@ private void batchDml(int status) { when(session.getRequestIdCreator()).thenReturn(NoopRequestIdCreator.INSTANCE); SpannerImpl spanner = mock(SpannerImpl.class); SpannerOptions spannerOptions = mock(SpannerOptions.class); + when(spannerOptions.getDefaultTransactionOptions()) + .thenReturn(TransactionOptions.getDefaultInstance()); when(spanner.getOptions()).thenReturn(spannerOptions); when(session.getSpanner()).thenReturn(spanner); SpannerRpc rpc = mock(SpannerRpc.class); diff --git a/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionRoutingTest.java b/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionRoutingTest.java new file mode 100644 index 000000000000..4b261f3d68af --- /dev/null +++ b/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionRoutingTest.java @@ -0,0 +1,188 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spanner; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.google.cloud.spanner.TransactionRunnerImpl.TransactionContextImpl; +import com.google.cloud.spanner.spi.v1.SpannerRpc; +import com.google.protobuf.ByteString; +import com.google.spanner.v1.TransactionOptions; +import com.google.spanner.v1.TransactionOptions.IsolationLevel; +import com.google.spanner.v1.TransactionOptions.ReadWrite.ReadLockMode; +import java.util.UUID; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class TransactionRoutingTest { + + private SpannerRpc rpc; + private SessionImpl session; + private SessionReference sessionReference; + private SpannerImpl spanner; + private SpannerOptions spannerOptions; + + @Before + public void setUp() { + rpc = mock(SpannerRpc.class); + session = mock(SessionImpl.class); + sessionReference = mock(SessionReference.class); + spanner = mock(SpannerImpl.class); + spannerOptions = mock(SpannerOptions.class); + + when(session.getSessionReference()).thenReturn(sessionReference); + when(session.getSpanner()).thenReturn(spanner); + when(spanner.getOptions()).thenReturn(spannerOptions); + + // Default: system-level unspecified (Tier 4 fallback) + setClientDefaultOptions(IsolationLevel.ISOLATION_LEVEL_UNSPECIFIED, ReadLockMode.READ_LOCK_MODE_UNSPECIFIED); + + TraceWrapper tracer = mock(TraceWrapper.class); + ISpan span = mock(ISpan.class); + when(session.getTracer()).thenReturn(tracer); + when(tracer.getCurrentSpan()).thenReturn(span); + } + + private void setClientDefaultOptions(IsolationLevel iso, ReadLockMode lock) { + TransactionOptions defaultTxnOptions = TransactionOptions.newBuilder() + .setIsolationLevel(iso) + .setReadWrite(TransactionOptions.ReadWrite.newBuilder().setReadLockMode(lock)) + .build(); + when(spannerOptions.getDefaultTransactionOptions()).thenReturn(defaultTxnOptions); + } + + private TransactionContextImpl createContext(Options options) { + return TransactionContextImpl.newBuilder() + .setSession(session) + .setTransactionId(ByteString.copyFromUtf8(UUID.randomUUID().toString())) + .setOptions(options) + .setRpc(rpc) + .setTracer(session.getTracer()) + .setSpan(session.getTracer().getCurrentSpan()) + .build(); + } + + @Test + public void testPrecedenceTier4SystemDefaults() { + // Tier 4: Unspecified everywhere should fall back to SERIALIZABLE / PESSIMISTIC, routeToLeader=true. + TransactionContextImpl context = createContext(Options.fromTransactionOptions()); + assertTrue(context.isRouteToLeader()); + } + + @Test + public void testPrecedenceTier3DatabaseDefaultsRepeatableRead() { + // Tier 3: Database Defaults sets REPEATABLE_READ + OPTIMISTIC, routeToLeader=false. + DatabaseMetadata dbMetadata = new DatabaseMetadata( + Dialect.GOOGLE_STANDARD_SQL, IsolationLevel.REPEATABLE_READ, ReadLockMode.OPTIMISTIC); + when(sessionReference.getDatabaseMetadata()).thenReturn(dbMetadata); + + TransactionContextImpl context = createContext(Options.fromTransactionOptions()); + assertFalse(context.isRouteToLeader()); + } + + @Test + public void testPrecedenceTier3DatabaseDefaultsRepeatableReadUnspecifiedLock() { + // Tier 3: Database Defaults sets REPEATABLE_READ + unspecified lock, routeToLeader=false. + DatabaseMetadata dbMetadata = new DatabaseMetadata( + Dialect.GOOGLE_STANDARD_SQL, IsolationLevel.REPEATABLE_READ, ReadLockMode.READ_LOCK_MODE_UNSPECIFIED); + when(sessionReference.getDatabaseMetadata()).thenReturn(dbMetadata); + + TransactionContextImpl context = createContext(Options.fromTransactionOptions()); + assertFalse(context.isRouteToLeader()); + } + + @Test + public void testPrecedenceTier3DatabaseDefaultsSerializable() { + // Tier 3: Database Defaults sets SERIALIZABLE + PESSIMISTIC, routeToLeader=true. + DatabaseMetadata dbMetadata = new DatabaseMetadata( + Dialect.GOOGLE_STANDARD_SQL, IsolationLevel.SERIALIZABLE, ReadLockMode.PESSIMISTIC); + when(sessionReference.getDatabaseMetadata()).thenReturn(dbMetadata); + + TransactionContextImpl context = createContext(Options.fromTransactionOptions()); + assertTrue(context.isRouteToLeader()); + } + + @Test + public void testPrecedenceTier2ClientStaticOverridesDatabase() { + // Tier 2: Client-Side Static config is REPEATABLE_READ + OPTIMISTIC. + // Database Defaults has SERIALIZABLE + PESSIMISTIC. + // routeToLeader=false because Tier 2 overrides Tier 3. + setClientDefaultOptions(IsolationLevel.REPEATABLE_READ, ReadLockMode.OPTIMISTIC); + + DatabaseMetadata dbMetadata = new DatabaseMetadata( + Dialect.GOOGLE_STANDARD_SQL, IsolationLevel.SERIALIZABLE, ReadLockMode.PESSIMISTIC); + when(sessionReference.getDatabaseMetadata()).thenReturn(dbMetadata); + + TransactionContextImpl context = createContext(Options.fromTransactionOptions()); + assertFalse(context.isRouteToLeader()); + } + + @Test + public void testPrecedenceTier1CallSiteOverridesAll() { + // Tier 1: Call-site options explicitly set REPEATABLE_READ + OPTIMISTIC. + // Client-Side Static has SERIALIZABLE + PESSIMISTIC. + // Database Defaults has SERIALIZABLE + PESSIMISTIC. + // routeToLeader=false because Tier 1 overrides everything. + setClientDefaultOptions(IsolationLevel.SERIALIZABLE, ReadLockMode.PESSIMISTIC); + + DatabaseMetadata dbMetadata = new DatabaseMetadata( + Dialect.GOOGLE_STANDARD_SQL, IsolationLevel.SERIALIZABLE, ReadLockMode.PESSIMISTIC); + when(sessionReference.getDatabaseMetadata()).thenReturn(dbMetadata); + + Options callSiteOptions = Options.fromTransactionOptions( + Options.isolationLevel(IsolationLevel.REPEATABLE_READ), + Options.readLockMode(ReadLockMode.OPTIMISTIC)); + + TransactionContextImpl context = createContext(callSiteOptions); + assertFalse(context.isRouteToLeader()); + } + + @Test + public void testPrecedenceTier1CallSiteSerializableOverridesAll() { + // Tier 1: Call-site options explicitly set SERIALIZABLE + PESSIMISTIC. + // Client-Side Static has REPEATABLE_READ + OPTIMISTIC. + // Database Defaults has REPEATABLE_READ + OPTIMISTIC. + // routeToLeader=true because Tier 1 overrides everything. + setClientDefaultOptions(IsolationLevel.REPEATABLE_READ, ReadLockMode.OPTIMISTIC); + + DatabaseMetadata dbMetadata = new DatabaseMetadata( + Dialect.GOOGLE_STANDARD_SQL, IsolationLevel.REPEATABLE_READ, ReadLockMode.OPTIMISTIC); + when(sessionReference.getDatabaseMetadata()).thenReturn(dbMetadata); + + Options callSiteOptions = Options.fromTransactionOptions( + Options.isolationLevel(IsolationLevel.SERIALIZABLE), + Options.readLockMode(ReadLockMode.PESSIMISTIC)); + + TransactionContextImpl context = createContext(callSiteOptions); + assertTrue(context.isRouteToLeader()); + } + + @Test + public void testRepeatableReadWithNullLockModeCanEnableLRYW() { + when(sessionReference.getDatabaseMetadata()).thenReturn(null); + Options callSiteOptions = Options.fromTransactionOptions( + Options.isolationLevel(IsolationLevel.REPEATABLE_READ)); + TransactionContextImpl context = createContext(callSiteOptions); + assertFalse(context.isRouteToLeader()); + } +} diff --git a/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionRunnerImplTest.java b/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionRunnerImplTest.java index 1dd2418aa05a..b1ad6573ab3d 100644 --- a/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionRunnerImplTest.java +++ b/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/TransactionRunnerImplTest.java @@ -127,6 +127,8 @@ public void setUp() { when(rpc.getRequestIdCreator()).thenReturn(NoopRequestIdCreator.INSTANCE); SpannerImpl spanner = mock(SpannerImpl.class); SpannerOptions spannerOptions = mock(SpannerOptions.class); + when(spannerOptions.getDefaultTransactionOptions()) + .thenReturn(TransactionOptions.getDefaultInstance()); when(spanner.getOptions()).thenReturn(spannerOptions); when(session.getSpanner()).thenReturn(spanner); when(rpc.executeQuery(Mockito.any(ExecuteSqlRequest.class), Mockito.anyMap(), eq(true))) diff --git a/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/AbstractMockServerTest.java b/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/AbstractMockServerTest.java index 43a6b9d4feff..829adf7a697a 100644 --- a/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/AbstractMockServerTest.java +++ b/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/AbstractMockServerTest.java @@ -206,7 +206,7 @@ public void getOperation( StatementResult.query(SELECT_RANDOM_STATEMENT, RANDOM_RESULT_SET)); mockSpanner.putStatementResult(StatementResult.query(SELECT1_STATEMENT, SELECT1_RESULTSET)); mockSpanner.putStatementResult( - StatementResult.detectDialectResult(Dialect.GOOGLE_STANDARD_SQL)); + MockSpannerServiceImpl.StatementResult.detectMetadataResult(Dialect.GOOGLE_STANDARD_SQL)); futureParentHandlers = Logger.getLogger(AbstractFuture.class.getName()).getUseParentHandlers(); exceptionRunnableParentHandlers = diff --git a/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/TransactionMockServerTest.java b/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/TransactionMockServerTest.java index 45f68b11a5b6..bc27f4afbef1 100644 --- a/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/TransactionMockServerTest.java +++ b/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/TransactionMockServerTest.java @@ -209,7 +209,7 @@ public void testBeginTransactionIsolationLevel() { SpannerPool.closeSpannerPool(); for (Dialect dialect : new Dialect[] {Dialect.POSTGRESQL, Dialect.GOOGLE_STANDARD_SQL}) { mockSpanner.putStatementResult( - MockSpannerServiceImpl.StatementResult.detectDialectResult(dialect)); + MockSpannerServiceImpl.StatementResult.detectMetadataResult(dialect)); try (Connection connection = super.createConnection()) { for (IsolationLevel isolationLevel : @@ -260,7 +260,7 @@ public void testBeginTransactionIsolationLevel() { public void testSetTransactionIsolationLevel() { SpannerPool.closeSpannerPool(); mockSpanner.putStatementResult( - MockSpannerServiceImpl.StatementResult.detectDialectResult(Dialect.POSTGRESQL)); + MockSpannerServiceImpl.StatementResult.detectMetadataResult(Dialect.POSTGRESQL)); try (Connection connection = super.createConnection()) { for (boolean autocommit : new boolean[] {true, false}) { diff --git a/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpcTest.java b/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpcTest.java index 275bbe66c20d..cf71e99b8df9 100644 --- a/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpcTest.java +++ b/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/spi/v1/GapicSpannerRpcTest.java @@ -829,6 +829,7 @@ public void testRouteToLeaderHeaderForReadWrite() { try (Spanner spanner = options.getService()) { final DatabaseClient databaseClient = spanner.getDatabaseClient(DatabaseId.of("[PROJECT]", "[INSTANCE]", "[DATABASE]")); + databaseClient.getDialect(); TransactionRunner runner = databaseClient.readWriteTransaction(); runner.run( transaction -> { diff --git a/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/spi/v1/GfeLatencyTest.java b/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/spi/v1/GfeLatencyTest.java index bcded26d685f..21b057c7a671 100644 --- a/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/spi/v1/GfeLatencyTest.java +++ b/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/spi/v1/GfeLatencyTest.java @@ -86,6 +86,7 @@ public class GfeLatencyTest { private static DatabaseClient databaseClientNoHeader; private static final String INSTANCE_ID = "fake-instance"; + private static final String INSTANCE_ID_NO_HEADER = "fake-instance-noheader"; private static final String DATABASE_ID = "fake-database"; private static final String PROJECT_ID = "fake-project"; @@ -181,7 +182,10 @@ public void sendHeaders(Metadata headers) { .start(); spannerNoHeader = createSpannerOptions(addressNoHeader, serverNoHeader).getService(); databaseClientNoHeader = - spannerNoHeader.getDatabaseClient(DatabaseId.of(PROJECT_ID, INSTANCE_ID, DATABASE_ID)); + spannerNoHeader.getDatabaseClient( + DatabaseId.of(PROJECT_ID, INSTANCE_ID_NO_HEADER, DATABASE_ID)); + databaseClient.getDialect(); + databaseClientNoHeader.getDialect(); } @AfterClass @@ -347,7 +351,7 @@ private long getMetric(View view, String method, boolean withOverride) { List tagValues = new java.util.ArrayList<>(); for (TagKey column : view.getColumns()) { if (column == SpannerRpcViews.INSTANCE_ID) { - tagValues.add(TagValue.create(INSTANCE_ID)); + tagValues.add(TagValue.create(withOverride ? INSTANCE_ID_NO_HEADER : INSTANCE_ID)); } else if (column == SpannerRpcViews.DATABASE_ID) { tagValues.add(TagValue.create(DATABASE_ID)); } else if (column == SpannerRpcViews.METHOD) {