2626import io .grpc .ClientInterceptor ;
2727import io .grpc .ForwardingClientCall .SimpleForwardingClientCall ;
2828import io .grpc .ForwardingClientCallListener .SimpleForwardingClientCallListener ;
29+ import io .grpc .Grpc ;
2930import io .grpc .Metadata ;
3031import io .grpc .MethodDescriptor ;
3132import io .grpc .Status ;
3233import java .io .FileInputStream ;
3334import java .io .IOException ;
35+ import java .net .InetAddress ;
36+ import java .net .InetSocketAddress ;
37+ import java .net .SocketAddress ;
3438import java .util .Random ;
3539import java .util .concurrent .atomic .AtomicBoolean ;
3640
@@ -41,6 +45,12 @@ public class GceTestEnvConfig implements TestEnvConfig {
4145 public static final String GCE_CREDENTIALS_FILE = "spanner.gce.config.credentials_file" ;
4246 public static final String GCE_STREAM_BROKEN_PROBABILITY =
4347 "spanner.gce.config.stream_broken_probability" ;
48+ public static final String ATTEMPT_DIRECT_PATH = "spanner.attempt_directpath" ;
49+ public static final String DIRECT_PATH_TEST_SCENARIO = "spanner.directpath_test_scenario" ;
50+
51+ // IP address prefixes allocated for DirectPath backends.
52+ public static final String DP_IPV6_PREFIX = "2001:4860:8040" ;
53+ public static final String DP_IPV4_PREFIX = "34.126" ;
4454
4555 private final SpannerOptions options ;
4656
@@ -51,6 +61,8 @@ public GceTestEnvConfig() {
5161 double errorProbability =
5262 Double .parseDouble (System .getProperty (GCE_STREAM_BROKEN_PROBABILITY , "0.0" ));
5363 checkState (errorProbability <= 1.0 );
64+ boolean attemptDirectPath = Boolean .getBoolean (ATTEMPT_DIRECT_PATH );
65+ String directPathTestScenario = System .getProperty (DIRECT_PATH_TEST_SCENARIO , "" );
5466 SpannerOptions .Builder builder =
5567 SpannerOptions .newBuilder ().setAutoThrottleAdministrativeRequests ();
5668 if (!projectId .isEmpty ()) {
@@ -66,12 +78,14 @@ public GceTestEnvConfig() {
6678 throw new RuntimeException (e );
6779 }
6880 }
69- options =
70- builder
71- .setInterceptorProvider (
72- SpannerInterceptorProvider .createDefault ()
73- .with (new GrpcErrorInjector (errorProbability )))
74- .build ();
81+ SpannerInterceptorProvider interceptorProvider =
82+ SpannerInterceptorProvider .createDefault ().with (new GrpcErrorInjector (errorProbability ));
83+ if (attemptDirectPath ) {
84+ interceptorProvider =
85+ interceptorProvider .with (new DirectPathAddressCheckInterceptor (directPathTestScenario ));
86+ }
87+ builder .setInterceptorProvider (interceptorProvider );
88+ options = builder .build ();
7589 }
7690
7791 @ Override
@@ -87,6 +101,7 @@ public void tearDown() {}
87101
88102 /** Injects errors in streaming calls to simulate call restarts */
89103 private static class GrpcErrorInjector implements ClientInterceptor {
104+
90105 private final double errorProbability ;
91106 private final Random random = new Random ();
92107
@@ -140,4 +155,64 @@ private boolean mayInjectError() {
140155 return random .nextDouble () < errorProbability ;
141156 }
142157 }
158+
159+ /**
160+ * Captures the request attributes "Grpc.TRANSPORT_ATTR_REMOTE_ADDR" when connection is
161+ * established and verifies if the remote address is a DirectPath address. This is only used for
162+ * DirectPath testing. {@link ClientCall#getAttributes()}
163+ */
164+ private static class DirectPathAddressCheckInterceptor implements ClientInterceptor {
165+ private final String directPathTestScenario ;
166+
167+ DirectPathAddressCheckInterceptor (String directPathTestScenario ) {
168+ this .directPathTestScenario = directPathTestScenario ;
169+ }
170+
171+ @ Override
172+ public <ReqT , RespT > ClientCall <ReqT , RespT > interceptCall (
173+ MethodDescriptor <ReqT , RespT > method , CallOptions callOptions , Channel next ) {
174+ final ClientCall <ReqT , RespT > clientCall = next .newCall (method , callOptions );
175+ return new SimpleForwardingClientCall <ReqT , RespT >(clientCall ) {
176+ @ Override
177+ public void start (Listener <RespT > responseListener , Metadata headers ) {
178+ super .start (
179+ new SimpleForwardingClientCallListener <RespT >(responseListener ) {
180+ @ Override
181+ public void onHeaders (Metadata headers ) {
182+ // Check peer IP after connection is established.
183+ SocketAddress remoteAddr =
184+ clientCall .getAttributes ().get (Grpc .TRANSPORT_ATTR_REMOTE_ADDR );
185+ if (!verifyRemoteAddress (remoteAddr )) {
186+ throw new RuntimeException (
187+ String .format (
188+ "Synthetically aborting the current request because it did not adhere"
189+ + " to the test environment's requirement for DirectPath."
190+ + " Expected test for DirectPath %s scenario,"
191+ + " but RPC was destined for %s" ,
192+ directPathTestScenario , remoteAddr .toString ()));
193+ }
194+ super .onHeaders (headers );
195+ }
196+ },
197+ headers );
198+ }
199+ };
200+ }
201+
202+ private boolean verifyRemoteAddress (SocketAddress remoteAddr ) {
203+ if (remoteAddr instanceof InetSocketAddress ) {
204+ InetAddress inetAddress = ((InetSocketAddress ) remoteAddr ).getAddress ();
205+ String addr = inetAddress .getHostAddress ();
206+ if (directPathTestScenario .equals ("ipv4" )) {
207+ // For ipv4-only VM, client should connect to ipv4 DirectPath addresses.
208+ return addr .startsWith (DP_IPV4_PREFIX );
209+ } else if (directPathTestScenario .equals ("ipv6" )) {
210+ // For ipv6-enabled VM, client could connect to either ipv4 or ipv6 DirectPath addresses.
211+ return addr .startsWith (DP_IPV6_PREFIX ) || addr .startsWith (DP_IPV4_PREFIX );
212+ }
213+ }
214+ // For all other scenarios(e.g. fallback), we should allow both DirectPath and CFE addresses.
215+ return true ;
216+ }
217+ }
143218}
0 commit comments