Skip to content
This repository was archived by the owner on Apr 7, 2026. It is now read-only.

Commit cdf2f36

Browse files
committed
incorporate changes
1 parent a3a9d07 commit cdf2f36

3 files changed

Lines changed: 206 additions & 25 deletions

File tree

google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/ChannelFinderServer.java

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,51 @@
1616

1717
package com.google.cloud.spanner.spi.v1;
1818

19+
import com.google.api.core.InternalApi;
1920
import io.grpc.ManagedChannel;
2021

21-
/** Represents a Spanner server endpoint for location-aware routing. */
22+
/**
23+
* Represents a Spanner server endpoint for location-aware routing.
24+
*
25+
* <p>Each instance wraps a gRPC {@link ManagedChannel} connected to a specific Spanner server. The
26+
* {@link ChannelFinderServerFactory} creates and caches these instances.
27+
*
28+
* <p>Implementations must be thread-safe as instances may be shared across multiple concurrent
29+
* operations.
30+
*
31+
* @see ChannelFinderServerFactory
32+
*/
33+
@InternalApi
2234
public interface ChannelFinderServer {
35+
36+
/**
37+
* Returns the network address of this server.
38+
*
39+
* @return the server address in "host:port" format
40+
*/
2341
String getAddress();
2442

43+
/**
44+
* Returns whether this server is ready to accept RPCs.
45+
*
46+
* <p>A server is considered unhealthy if:
47+
*
48+
* <ul>
49+
* <li>The underlying channel is shutdown or terminated
50+
* <li>The channel is in a transient failure state
51+
* </ul>
52+
*
53+
* @return true if the server is healthy and ready to accept RPCs
54+
*/
2555
boolean isHealthy();
2656

27-
ManagedChannel getChannel(); // Added to get the underlying channel for RPC calls
57+
/**
58+
* Returns the gRPC channel for making RPCs to this server.
59+
*
60+
* <p>The returned channel is managed by the {@link ChannelFinderServerFactory} and should not be
61+
* shut down directly by callers.
62+
*
63+
* @return the managed channel for this server
64+
*/
65+
ManagedChannel getChannel();
2866
}

google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/ChannelFinderServerFactory.java

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,64 @@
1616

1717
package com.google.cloud.spanner.spi.v1;
1818

19-
/** Factory for creating and caching server connections for location-aware routing. */
19+
import com.google.api.core.InternalApi;
20+
21+
/**
22+
* Factory for creating and caching server connections for location-aware routing.
23+
*
24+
* <p>Implementations are expected to cache {@link ChannelFinderServer} instances such that repeated
25+
* calls with the same address return the same instance. This allows the {@link
26+
* com.google.cloud.spanner.spi.v1.KeyRangeCache} to efficiently manage server references.
27+
*
28+
* <p>Implementations must be thread-safe. Multiple threads may concurrently call {@link
29+
* #create(String)} with different addresses.
30+
*/
31+
@InternalApi
2032
public interface ChannelFinderServerFactory {
33+
34+
/**
35+
* Returns the default server endpoint.
36+
*
37+
* <p>The default server is the original endpoint configured in {@link
38+
* com.google.cloud.spanner.SpannerOptions}. It is used as a fallback when the location cache does
39+
* not have routing information for a request.
40+
*
41+
* @return the default server, never null
42+
*/
2143
ChannelFinderServer defaultServer();
2244

45+
/**
46+
* Creates or retrieves a cached server for the given address.
47+
*
48+
* <p>If a server for this address already exists in the cache, the cached instance is returned.
49+
* Otherwise, a new server connection is created and cached.
50+
*
51+
* @param address the server address in "host:port" format
52+
* @return a server instance for the address, never null
53+
* @throws com.google.cloud.spanner.SpannerException if the channel cannot be created
54+
*/
2355
ChannelFinderServer create(String address);
56+
57+
/**
58+
* Evicts a server from the cache and gracefully shuts down its channel.
59+
*
60+
* <p>This method should be called when a server becomes unhealthy or is no longer needed. The
61+
* channel shutdown is graceful: existing RPCs are allowed to complete, but new RPCs will not be
62+
* accepted on this channel.
63+
*
64+
* <p>If the address is not in the cache, this method does nothing.
65+
*
66+
* @param address the server address to evict
67+
*/
68+
void evict(String address);
69+
70+
/**
71+
* Shuts down all cached server connections.
72+
*
73+
* <p>This method should be called when the Spanner client is closed to release all resources.
74+
* Each channel is shut down gracefully, allowing in-flight RPCs to complete.
75+
*
76+
* <p>After calling this method, the factory should not be used to create new connections.
77+
*/
78+
void shutdown();
2479
}

google-cloud-spanner/src/main/java/com/google/cloud/spanner/spi/v1/GrpcChannelFinderServerFactory.java

Lines changed: 110 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,25 +16,51 @@
1616

1717
package com.google.cloud.spanner.spi.v1;
1818

19+
import com.google.api.core.InternalApi;
1920
import com.google.api.gax.grpc.GrpcTransportChannel;
2021
import com.google.api.gax.grpc.InstantiatingGrpcChannelProvider;
22+
import com.google.api.gax.rpc.TransportChannelProvider;
23+
import com.google.cloud.spanner.ErrorCode;
24+
import com.google.cloud.spanner.SpannerExceptionFactory;
25+
import com.google.common.annotations.VisibleForTesting;
26+
import io.grpc.ConnectivityState;
2127
import io.grpc.ManagedChannel;
2228
import java.io.IOException;
2329
import java.util.Map;
2430
import java.util.concurrent.ConcurrentHashMap;
31+
import java.util.concurrent.TimeUnit;
2532

33+
/**
34+
* gRPC implementation of {@link ChannelFinderServerFactory}.
35+
*
36+
* <p>This factory creates and caches gRPC channels per address. It uses {@link
37+
* InstantiatingGrpcChannelProvider#withEndpoint(String)} to create new channels with the same
38+
* configuration but different endpoints, avoiding race conditions.
39+
*/
40+
@InternalApi
2641
class GrpcChannelFinderServerFactory implements ChannelFinderServerFactory {
27-
private final InstantiatingGrpcChannelProvider.Builder channelBuilder;
42+
43+
/** Timeout for graceful channel shutdown. */
44+
private static final long SHUTDOWN_TIMEOUT_SECONDS = 5;
45+
46+
private final InstantiatingGrpcChannelProvider baseProvider;
2847
private final Map<String, GrpcChannelFinderServer> servers = new ConcurrentHashMap<>();
2948
private final GrpcChannelFinderServer defaultServer;
49+
private volatile boolean isShutdown = false;
3050

31-
public GrpcChannelFinderServerFactory(InstantiatingGrpcChannelProvider.Builder channelBuilder)
51+
/**
52+
* Creates a new factory with the given channel provider.
53+
*
54+
* @param channelProvider the base provider used to create channels. New channels for different
55+
* endpoints are created using {@link InstantiatingGrpcChannelProvider#withEndpoint(String)}.
56+
* @throws IOException if the default channel cannot be created
57+
*/
58+
public GrpcChannelFinderServerFactory(InstantiatingGrpcChannelProvider channelProvider)
3259
throws IOException {
33-
this.channelBuilder = channelBuilder;
34-
// The "default" server will use the original endpoint from the builder.
35-
this.defaultServer =
36-
new GrpcChannelFinderServer(this.channelBuilder.getEndpoint(), channelBuilder.build());
37-
this.servers.put(this.defaultServer.getAddress(), this.defaultServer);
60+
this.baseProvider = channelProvider;
61+
String defaultEndpoint = channelProvider.getEndpoint();
62+
this.defaultServer = new GrpcChannelFinderServer(defaultEndpoint, channelProvider);
63+
this.servers.put(defaultEndpoint, this.defaultServer);
3864
}
3965

4066
@Override
@@ -44,37 +70,95 @@ public ChannelFinderServer defaultServer() {
4470

4571
@Override
4672
public ChannelFinderServer create(String address) {
73+
if (isShutdown) {
74+
throw SpannerExceptionFactory.newSpannerException(
75+
ErrorCode.FAILED_PRECONDITION, "ChannelFinderServerFactory has been shut down");
76+
}
77+
4778
return servers.computeIfAbsent(
4879
address,
4980
addr -> {
5081
try {
51-
// Modify the builder to use the new address
52-
synchronized (channelBuilder) {
53-
InstantiatingGrpcChannelProvider.Builder newBuilder =
54-
channelBuilder.setEndpoint(addr);
55-
return new GrpcChannelFinderServer(addr, newBuilder.build());
56-
}
82+
// Create a new provider with the same config but different endpoint.
83+
// This is thread-safe as withEndpoint() returns a new provider instance.
84+
TransportChannelProvider newProvider = baseProvider.withEndpoint(addr);
85+
return new GrpcChannelFinderServer(addr, newProvider);
5786
} catch (IOException e) {
58-
throw new RuntimeException("Failed to create channel for address: " + addr, e);
87+
throw SpannerExceptionFactory.newSpannerException(
88+
ErrorCode.INTERNAL, "Failed to create channel for address: " + addr, e);
5989
}
6090
});
6191
}
6292

93+
@Override
94+
public void evict(String address) {
95+
if (defaultServer.getAddress().equals(address)) {
96+
return;
97+
}
98+
GrpcChannelFinderServer server = servers.remove(address);
99+
if (server != null) {
100+
shutdownServerGracefully(server);
101+
}
102+
}
103+
104+
@Override
105+
public void shutdown() {
106+
isShutdown = true;
107+
for (GrpcChannelFinderServer server : servers.values()) {
108+
shutdownServerGracefully(server);
109+
}
110+
servers.clear();
111+
}
112+
113+
/**
114+
* Gracefully shuts down a server's channel.
115+
*
116+
* <p>First attempts a graceful shutdown, waiting for in-flight RPCs to complete. If the timeout
117+
* is exceeded, forces immediate shutdown.
118+
*/
119+
private void shutdownServerGracefully(GrpcChannelFinderServer server) {
120+
ManagedChannel channel = server.getChannel();
121+
if (channel.isShutdown()) {
122+
return;
123+
}
124+
125+
channel.shutdown();
126+
try {
127+
if (!channel.awaitTermination(SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
128+
channel.shutdownNow();
129+
}
130+
} catch (InterruptedException e) {
131+
channel.shutdownNow();
132+
Thread.currentThread().interrupt();
133+
}
134+
}
135+
136+
/** gRPC implementation of {@link ChannelFinderServer}. */
63137
static class GrpcChannelFinderServer implements ChannelFinderServer {
64138
private final String address;
65139
private final ManagedChannel channel;
66140

67-
public GrpcChannelFinderServer(String address, InstantiatingGrpcChannelProvider provider)
68-
throws IOException {
141+
/**
142+
* Creates a server from a channel provider.
143+
*
144+
* @param address the server address
145+
* @param provider the channel provider (must be a gRPC provider)
146+
* @throws IOException if the channel cannot be created
147+
*/
148+
GrpcChannelFinderServer(String address, TransportChannelProvider provider) throws IOException {
69149
this.address = address;
70-
// It's assumed that getTransportChannel() returns a ManagedChannel or can be cast to one.
71-
// For this example, GrpcTransportChannel is used as in KeyAwareChannel.
72150
GrpcTransportChannel transportChannel = (GrpcTransportChannel) provider.getTransportChannel();
73151
this.channel = (ManagedChannel) transportChannel.getChannel();
74152
}
75153

76-
// Constructor for the default server that already has a channel
77-
public GrpcChannelFinderServer(String address, ManagedChannel channel) {
154+
/**
155+
* Creates a server with an existing channel. Primarily for testing.
156+
*
157+
* @param address the server address
158+
* @param channel the managed channel
159+
*/
160+
@VisibleForTesting
161+
GrpcChannelFinderServer(String address, ManagedChannel channel) {
78162
this.address = address;
79163
this.channel = channel;
80164
}
@@ -86,8 +170,12 @@ public String getAddress() {
86170

87171
@Override
88172
public boolean isHealthy() {
89-
// A simple health check. In a real scenario, this might involve a ping or other checks.
90-
return !channel.isShutdown() && !channel.isTerminated();
173+
if (channel.isShutdown() || channel.isTerminated()) {
174+
return false;
175+
}
176+
// Check connectivity state without triggering a connection attempt
177+
ConnectivityState state = channel.getState(false);
178+
return state != ConnectivityState.SHUTDOWN && state != ConnectivityState.TRANSIENT_FAILURE;
91179
}
92180

93181
@Override

0 commit comments

Comments
 (0)