1717package com .google .cloud .spanner ;
1818
1919import static com .google .cloud .spanner .DisableDefaultMtlsProvider .disableDefaultMtlsProvider ;
20- import static io . grpc . Grpc . TRANSPORT_ATTR_REMOTE_ADDR ;
20+ import static java . util . stream . Collectors . toSet ;
2121import static org .junit .Assert .assertEquals ;
2222import static org .junit .Assert .assertTrue ;
2323
3131import com .google .spanner .v1 .StructType ;
3232import com .google .spanner .v1 .StructType .Field ;
3333import com .google .spanner .v1 .TypeCode ;
34- import io .grpc .Attributes ;
3534import io .grpc .Context ;
3635import io .grpc .Contexts ;
3736import io .grpc .Metadata ;
@@ -101,9 +100,9 @@ public static Collection<Object[]> data() {
101100 private static MockSpannerServiceImpl mockSpanner ;
102101 private static Server server ;
103102 private static InetSocketAddress address ;
104- private static final Set < InetSocketAddress > batchCreateSessionLocalIps =
105- ConcurrentHashMap .newKeySet ();
106- private static final Set <InetSocketAddress > executeSqlLocalIps = ConcurrentHashMap .newKeySet ();
103+ // Track channel hints (from X-Goog-Spanner-Request-Id header) per RPC method
104+ private static final Set < Long > batchCreateSessionChannelHints = ConcurrentHashMap .newKeySet ();
105+ private static final Set <Long > executeSqlChannelHints = ConcurrentHashMap .newKeySet ();
107106
108107 private static Level originalLogLevel ;
109108
@@ -118,8 +117,8 @@ public static void startServer() throws Exception {
118117 server =
119118 NettyServerBuilder .forAddress (address )
120119 .addService (mockSpanner )
121- // Add a server interceptor to register the remote addresses that we are seeing. This
122- // indicates how many channels are used client side to communicate with the server .
120+ // Add a server interceptor to extract channel hints from X-Goog-Spanner-Request-Id
121+ // header. This verifies that the client uses all configured channels .
123122 .intercept (
124123 new ServerInterceptor () {
125124 @ Override
@@ -133,22 +132,26 @@ public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
133132 headers .get (
134133 Metadata .Key .of (
135134 "x-response-encoding" , Metadata .ASCII_STRING_MARSHALLER )));
136- Attributes attributes = call .getAttributes ();
137- @ SuppressWarnings ({"unchecked" , "deprecation" })
138- Attributes .Key <InetSocketAddress > key =
139- (Attributes .Key <InetSocketAddress >)
140- attributes .keys ().stream ()
141- .filter (k -> k .equals (TRANSPORT_ATTR_REMOTE_ADDR ))
142- .findFirst ()
143- .orElse (null );
144- if (key != null ) {
145- if (call .getMethodDescriptor ()
146- .equals (SpannerGrpc .getBatchCreateSessionsMethod ())) {
147- batchCreateSessionLocalIps .add (attributes .get (key ));
148- }
149- if (call .getMethodDescriptor ()
150- .equals (SpannerGrpc .getExecuteStreamingSqlMethod ())) {
151- executeSqlLocalIps .add (attributes .get (key ));
135+ // Extract channel hint from X-Goog-Spanner-Request-Id header
136+ String requestId = headers .get (XGoogSpannerRequestId .REQUEST_HEADER_KEY );
137+ if (requestId != null ) {
138+ // Format:
139+ // <version>.<randProcessId>.<nthClientId>.<nthChannelId>.<nthRequest>.<attempt>
140+ String [] parts = requestId .split ("\\ ." );
141+ if (parts .length >= 4 ) {
142+ try {
143+ long channelHint = Long .parseLong (parts [3 ]);
144+ if (call .getMethodDescriptor ()
145+ .equals (SpannerGrpc .getBatchCreateSessionsMethod ())) {
146+ batchCreateSessionChannelHints .add (channelHint );
147+ }
148+ if (call .getMethodDescriptor ()
149+ .equals (SpannerGrpc .getExecuteStreamingSqlMethod ())) {
150+ executeSqlChannelHints .add (channelHint );
151+ }
152+ } catch (NumberFormatException e ) {
153+ // Ignore parse errors
154+ }
152155 }
153156 }
154157 return Contexts .interceptCall (Context .current (), call , headers , next );
@@ -180,8 +183,8 @@ public static void resetLogging() {
180183 @ After
181184 public void reset () {
182185 mockSpanner .reset ();
183- batchCreateSessionLocalIps .clear ();
184- executeSqlLocalIps .clear ();
186+ batchCreateSessionChannelHints .clear ();
187+ executeSqlChannelHints .clear ();
185188 }
186189
187190 private SpannerOptions createSpannerOptions () {
@@ -238,6 +241,23 @@ public void testUsesAllChannels() throws InterruptedException {
238241 executor .shutdown ();
239242 assertTrue (executor .awaitTermination (Duration .ofSeconds (10L )));
240243 }
241- assertEquals (numChannels , executeSqlLocalIps .size ());
244+ // Bound the channel hints to numChannels (matching gRPC-GCP behavior) and verify
245+ // that channels are being distributed. The raw channel hints may be unbounded (based on
246+ // session index), but gRPC-GCP bounds them to the actual number of channels.
247+ Set <Long > boundedChannelHints =
248+ executeSqlChannelHints .stream ().map (hint -> hint % numChannels ).collect (toSet ());
249+ // Verify that channel distribution is working:
250+ // - For numChannels=1, exactly 1 channel should be used
251+ // - For numChannels>1, multiple channels should be used (at least half)
252+ if (numChannels == 1 ) {
253+ assertEquals (1 , boundedChannelHints .size ());
254+ } else {
255+ assertTrue (
256+ "Expected at least "
257+ + (numChannels / 2 )
258+ + " channels to be used, but got "
259+ + boundedChannelHints .size (),
260+ boundedChannelHints .size () >= numChannels / 2 );
261+ }
242262 }
243263}
0 commit comments