Skip to content
Open
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 @@ -257,6 +257,7 @@ public static GcpChannelPoolOptions createDefaultDynamicChannelPoolOptions() {
private final boolean enableEndToEndTracing;
private final String monitoringHost;
private final TransactionOptions defaultTransactionOptions;
private final boolean isExperimentalHost;

enum TracingFramework {
OPEN_CENSUS,
Expand Down Expand Up @@ -914,14 +915,15 @@ protected SpannerOptions(Builder builder) {
openTelemetry = builder.openTelemetry;
enableApiTracing = builder.enableApiTracing;
enableExtendedTracing = builder.enableExtendedTracing;
if (builder.experimentalHost != null) {
if (builder.isExperimentalHost) {
enableBuiltInMetrics = false;
} else {
enableBuiltInMetrics = builder.enableBuiltInMetrics;
}
enableEndToEndTracing = builder.enableEndToEndTracing;
monitoringHost = builder.monitoringHost;
defaultTransactionOptions = builder.defaultTransactionOptions;
isExperimentalHost = builder.isExperimentalHost;
}

private String getResolvedUniverseDomain() {
Expand Down Expand Up @@ -987,6 +989,15 @@ default String getMonitoringHost() {
default GoogleCredentials getDefaultExperimentalHostCredentials() {
return null;
}

/**
* Returns true if the experimental location API (SpanFE bypass) should be enabled. When
* enabled, the client will use location-aware routing to send requests directly to the
* appropriate Spanner server.
*/
default boolean isEnableLocationApi() {
return false;
}
}

static final String DEFAULT_SPANNER_EXPERIMENTAL_HOST_CREDENTIALS =
Expand All @@ -1011,6 +1022,8 @@ private static class SpannerEnvironmentImpl implements SpannerEnvironment {
private static final String SPANNER_DISABLE_DIRECT_ACCESS_GRPC_BUILTIN_METRICS =
"SPANNER_DISABLE_DIRECT_ACCESS_GRPC_BUILTIN_METRICS";
private static final String SPANNER_MONITORING_HOST = "SPANNER_MONITORING_HOST";
private static final String GOOGLE_SPANNER_EXPERIMENTAL_LOCATION_API =
"GOOGLE_SPANNER_EXPERIMENTAL_LOCATION_API";

private SpannerEnvironmentImpl() {}

Expand Down Expand Up @@ -1069,6 +1082,11 @@ public String getMonitoringHost() {
public GoogleCredentials getDefaultExperimentalHostCredentials() {
return getOAuthTokenFromFile(System.getenv(DEFAULT_SPANNER_EXPERIMENTAL_HOST_CREDENTIALS));
}

@Override
public boolean isEnableLocationApi() {
return Boolean.parseBoolean(System.getenv(GOOGLE_SPANNER_EXPERIMENTAL_LOCATION_API));
}
}

/** Builder for {@link SpannerOptions} instances. */
Expand Down Expand Up @@ -1139,8 +1157,7 @@ public static class Builder
private boolean enableBuiltInMetrics = SpannerOptions.environment.isEnableBuiltInMetrics();
private String monitoringHost = SpannerOptions.environment.getMonitoringHost();
private SslContext mTLSContext = null;
private String experimentalHost = null;
private boolean usePlainText = false;
private boolean isExperimentalHost = false;
private TransactionOptions defaultTransactionOptions = TransactionOptions.getDefaultInstance();

private static String createCustomClientLibToken(String token) {
Expand All @@ -1149,56 +1166,26 @@ private static String createCustomClientLibToken(String token) {

protected Builder() {
// Manually set retry and polling settings that work.
RetrySettings baseRetrySettings =
RetrySettings.newBuilder()
.setInitialRpcTimeoutDuration(Duration.ofSeconds(60L))
.setMaxRpcTimeoutDuration(Duration.ofSeconds(600L))
.setMaxRetryDelayDuration(Duration.ofSeconds(45L))
.setRetryDelayMultiplier(1.5)
.setRpcTimeoutMultiplier(1.5)
.setTotalTimeoutDuration(Duration.ofHours(48L))
.build();

// The polling setting with a short initial delay as we expect
// it to return soon.
OperationTimedPollAlgorithm shortInitialPollingDelayAlgorithm =
OperationTimedPollAlgorithm longRunningPollingAlgorithm =
OperationTimedPollAlgorithm.create(
baseRetrySettings.toBuilder()
.setInitialRetryDelayDuration(Duration.ofSeconds(1L))
RetrySettings.newBuilder()
.setInitialRpcTimeoutDuration(Duration.ofSeconds(60L))
.setMaxRpcTimeoutDuration(Duration.ofSeconds(600L))
.setInitialRetryDelayDuration(Duration.ofSeconds(20L))
.setMaxRetryDelayDuration(Duration.ofSeconds(45L))
.setRetryDelayMultiplier(1.5)
.setRpcTimeoutMultiplier(1.5)
.setTotalTimeoutDuration(Duration.ofHours(48L))
.build());
databaseAdminStubSettingsBuilder
.createDatabaseOperationSettings()
.setPollingAlgorithm(shortInitialPollingDelayAlgorithm);

// The polling setting with a long initial delay as we expect
// the operation to take a bit long time to return.
OperationTimedPollAlgorithm longInitialPollingDelayAlgorithm =
OperationTimedPollAlgorithm.create(
baseRetrySettings.toBuilder()
.setInitialRetryDelayDuration(Duration.ofSeconds(20L))
.build());
.setPollingAlgorithm(longRunningPollingAlgorithm);
databaseAdminStubSettingsBuilder
.createBackupOperationSettings()
.setPollingAlgorithm(longInitialPollingDelayAlgorithm);
.setPollingAlgorithm(longRunningPollingAlgorithm);
databaseAdminStubSettingsBuilder
.restoreDatabaseOperationSettings()
.setPollingAlgorithm(longInitialPollingDelayAlgorithm);

// updateDatabaseDdl requires a separate setting because
// it has no existing overrides on RPC timeouts for LRO polling.
databaseAdminStubSettingsBuilder
.updateDatabaseDdlOperationSettings()
.setPollingAlgorithm(
OperationTimedPollAlgorithm.create(
RetrySettings.newBuilder()
.setInitialRetryDelayDuration(Duration.ofMillis(1000L))
.setRetryDelayMultiplier(1.5)
.setMaxRetryDelayDuration(Duration.ofMillis(45000L))
.setInitialRpcTimeoutDuration(Duration.ZERO)
.setRpcTimeoutMultiplier(1.0)
.setMaxRpcTimeoutDuration(Duration.ZERO)
.setTotalTimeoutDuration(Duration.ofHours(48L))
.build()));
.setPollingAlgorithm(longRunningPollingAlgorithm);
}

Builder(SpannerOptions options) {
Expand Down Expand Up @@ -1676,19 +1663,10 @@ public Builder setHost(String host) {

@ExperimentalApi("https://github.com/googleapis/java-spanner/pull/3676")
public Builder setExperimentalHost(String host) {
if (this.usePlainText) {
Preconditions.checkArgument(
!host.startsWith("https:"),
"Please remove the 'https:' protocol prefix from the host string when using plain text"
+ " communication");
if (!host.startsWith("http")) {
host = "http://" + host;
}
}
super.setHost(host);
super.setProjectId(EXPERIMENTAL_HOST_PROJECT_ID);
setSessionPoolOption(SessionPoolOptions.newBuilder().setExperimentalHost().build());
this.experimentalHost = host;
this.isExperimentalHost = true;
return this;
}

Expand Down Expand Up @@ -1799,23 +1777,6 @@ public Builder useClientCert(String clientCertificate, String clientCertificateK
return this;
}

/**
* {@code usePlainText} will configure the transport to use plaintext (no TLS) and will set
* credentials to {@link com.google.cloud.NoCredentials} to avoid sending authentication over an
* unsecured channel.
*/
@ExperimentalApi("https://github.com/googleapis/java-spanner/pull/4264")
public Builder usePlainText() {
this.usePlainText = true;
this.setChannelConfigurator(ManagedChannelBuilder::usePlaintext)
.setCredentials(NoCredentials.getInstance());
if (this.experimentalHost != null) {
// Re-apply host settings to ensure http:// is prepended.
setExperimentalHost(this.experimentalHost);
}
return this;
}

/**
* Sets OpenTelemetry object to be used for Spanner Metrics and Traces. GlobalOpenTelemetry will
* be used as fallback if this options is not set.
Expand Down Expand Up @@ -1981,7 +1942,7 @@ public Builder setDefaultTransactionOptions(
@Override
public SpannerOptions build() {
// Set the host of emulator has been set.
if (emulatorHost != null && experimentalHost == null) {
if (emulatorHost != null) {
if (!emulatorHost.startsWith("http")) {
emulatorHost = "http://" + emulatorHost;
}
Expand All @@ -1991,7 +1952,7 @@ public SpannerOptions build() {
this.setChannelConfigurator(ManagedChannelBuilder::usePlaintext);
// As we are using plain text, we should never send any credentials.
this.setCredentials(NoCredentials.getInstance());
} else if (experimentalHost != null && credentials == null) {
} else if (isExperimentalHost && credentials == null) {
credentials = environment.getDefaultExperimentalHostCredentials();
}
if (this.numChannels == null) {
Expand Down Expand Up @@ -2033,6 +1994,12 @@ public static void useDefaultEnvironment() {
SpannerOptions.environment = SpannerEnvironmentImpl.INSTANCE;
}

/** Returns the current {@link SpannerEnvironment}. */
@InternalApi
public static SpannerEnvironment getEnvironment() {
return environment;
}

@InternalApi
public static GoogleCredentials getDefaultExperimentalCredentialsFromSysEnv() {
return getOAuthTokenFromFile(System.getenv(DEFAULT_SPANNER_EXPERIMENTAL_HOST_CREDENTIALS));
Expand Down Expand Up @@ -2379,6 +2346,10 @@ public TransactionOptions getDefaultTransactionOptions() {
return defaultTransactionOptions;
}

public boolean isExperimentalHost() {
return isExperimentalHost;
}

@BetaApi
public boolean isUseVirtualThreads() {
return useVirtualThreads;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
* 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.spi.v1;

import com.google.spanner.v1.CacheUpdate;
import com.google.spanner.v1.ReadRequest;
import com.google.spanner.v1.RoutingHint;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import javax.annotation.Nullable;

/**
* ChannelFinder is responsible for finding the correct Spanner server to route RPCs to.
*
* <p>It uses a {@link KeyRecipeCache} and a {@link KeyRangeCache} to store metadata about the
* database, including key recipes and range information. This metadata is updated through the
* {@link #update(CacheUpdate)} method.
*
* <p>The {@link #findServer(ReadRequest.Builder)} method is used to determine the appropriate
* server for a given read request.
*/
public final class ChannelFinder {
private final String deployment;
private final String databaseUri;
private final KeyRangeCache rangeCache;
private final KeyRecipeCache recipeCache;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private long databaseId = 0L;
private final ChannelFinderServerFactory serverFactory;

public ChannelFinder(
ChannelFinderServerFactory serverFactory, String deployment, String databaseUri) {
this.serverFactory = serverFactory;
this.deployment = deployment;
this.databaseUri = databaseUri;
this.rangeCache = new KeyRangeCache(serverFactory);
this.recipeCache = new KeyRecipeCache();
}

/**
* Updates the cache with new metadata.
*
* @param cacheUpdate The cache update information.
*/
public void update(CacheUpdate cacheUpdate) {
lock.writeLock().lock();
try {
if (databaseId != cacheUpdate.getDatabaseId()) {
System.out.println(
"DEBUG [BYPASS]: Database ID changed from "
+ databaseId
+ " to "
+ cacheUpdate.getDatabaseId()
+ ", clearing caches");
recipeCache.clear();
rangeCache.clear();
databaseId = cacheUpdate.getDatabaseId();
}
recipeCache.addRecipes(cacheUpdate.getKeyRecipes());
rangeCache.addRanges(cacheUpdate);
System.out.println(
"DEBUG [BYPASS]: Cache updated. Current state:\n" + rangeCache.debugString());
} finally {
lock.writeLock().unlock();
}
}

/**
* Finds the server for a given ReadRequest.
*
* @param reqBuilder The ReadRequest builder.
* @return The server to route the request to, or null if an error occurs.
*/
@Nullable
public ChannelFinderServer findServer(ReadRequest.Builder reqBuilder) {
RoutingHint.Builder hintBuilder = reqBuilder.getRoutingHintBuilder();
lock.readLock().lock();
try {
if (databaseId != 0) {
hintBuilder.setDatabaseId(databaseId);
}
System.out.println(
"DEBUG [BYPASS]: findServer - computing keys for table: " + reqBuilder.getTable());
recipeCache.computeKeys(reqBuilder); // Modifies hintBuilder within reqBuilder
System.out.println(
"DEBUG [BYPASS]: findServer - after computeKeys, key: "
+ hintBuilder.getKey().toStringUtf8());
ChannelFinderServer server =
rangeCache.fillRoutingInfo(reqBuilder.getSession(), false, hintBuilder);
System.out.println(
"DEBUG [BYPASS]: findServer - fillRoutingInfo returned server: "
+ (server != null ? server.getAddress() : "null"));
return server;
} finally {
lock.readLock().unlock();
}
}

/**
* Returns a debug string representation of the cache.
*
* @return A string containing debug information.
*/
public String debugString() {
lock.readLock().lock();
try {
return rangeCache.debugString();
} finally {
lock.readLock().unlock();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* 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.spi.v1;

import io.grpc.ManagedChannel;

/** Represents a Spanner server endpoint for location-aware routing. */
public interface ChannelFinderServer {
String getAddress();

boolean isHealthy();

ManagedChannel getChannel(); // Added to get the underlying channel for RPC calls
}
Loading
Loading