Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ public T get() throws Exception {
return object;
}
Comment thread
shobhitsg marked this conversation as resolved.

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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();

/**
Expand Down Expand Up @@ -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());
}
}

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -473,32 +501,86 @@ private int getSingleUseChannelHint() {
}
}

private final AbstractLazyInitializer<Dialect> dialectSupplier =
new AbstractLazyInitializer<Dialect>() {
@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<DatabaseMetadata> metadataSupplier = new AbstractLazyInitializer<DatabaseMetadata>() {
@Override
protected DatabaseMetadata initialize() {
Dialect dialect = Dialect.GOOGLE_STANDARD_SQL;
IsolationLevel isolationLevel = IsolationLevel.SERIALIZABLE;
ReadLockMode readLockMode = ReadLockMode.PESSIMISTIC;
SessionReference sessionRef = null;

try {
Future<SessionReference> 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;
}
};
Comment thread
shobhitsg marked this conversation as resolved.

@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<Dialect> getDialectAsync() {
try {
return MAINTAINER_SERVICE.submit(dialectSupplier::get);
return MAINTAINER_SERVICE.submit(() -> getDialect());
} catch (Exception exception) {
throw SpannerExceptionFactory.asSpannerException(exception);
}
Expand Down Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<SpannerRpc.Option, ?> options) {
this.options = options;
Expand Down Expand Up @@ -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;
}
Expand Down
Loading
Loading