Skip to content

Commit 1835cd7

Browse files
authored
Spanner: Throw exception when SSLHandshakeException occurs instead of infinite retry loop (#4663)
* #3889 throw exception when an SSLHandshakeException occurs SSLHandshakeExceptions are not retryable, as it is most probably an indication that the client does not accept the server certificate. * #3889 added test for retryability of SSLHandshakeException * fixed formatting
1 parent e4c11c0 commit 1835cd7

File tree

2 files changed

+60
-3
lines changed

2 files changed

+60
-3
lines changed

google-cloud-clients/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerExceptionFactory.java

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,9 @@
1616

1717
package com.google.cloud.spanner;
1818

19-
import static com.google.cloud.spanner.SpannerException.DoNotConstructDirectly;
20-
2119
import com.google.api.gax.grpc.GrpcStatusCode;
2220
import com.google.api.gax.rpc.ApiException;
21+
import com.google.cloud.spanner.SpannerException.DoNotConstructDirectly;
2322
import com.google.common.base.MoreObjects;
2423
import com.google.common.base.Predicate;
2524
import io.grpc.Context;
@@ -28,6 +27,7 @@
2827
import java.util.concurrent.CancellationException;
2928
import java.util.concurrent.TimeoutException;
3029
import javax.annotation.Nullable;
30+
import javax.net.ssl.SSLHandshakeException;
3131

3232
/**
3333
* A factory for creating instances of {@link SpannerException} and its subtypes. All creation of
@@ -168,7 +168,9 @@ private static boolean isRetryable(ErrorCode code, @Nullable Throwable cause) {
168168
case INTERNAL:
169169
return hasCauseMatching(cause, Matchers.isRetryableInternalError);
170170
case UNAVAILABLE:
171-
return true;
171+
// SSLHandshakeException is (probably) not retryable, as it is an indication that the server
172+
// certificate was not accepted by the client.
173+
return !hasCauseMatching(cause, Matchers.isSSLHandshakeException);
172174
case RESOURCE_EXHAUSTED:
173175
return SpannerException.extractRetryDelay(cause) > 0;
174176
default:
@@ -211,5 +213,12 @@ public boolean apply(Throwable cause) {
211213
return false;
212214
}
213215
};
216+
static final Predicate<Throwable> isSSLHandshakeException =
217+
new Predicate<Throwable>() {
218+
@Override
219+
public boolean apply(Throwable input) {
220+
return input instanceof SSLHandshakeException;
221+
}
222+
};
214223
}
215224
}

google-cloud-clients/google-cloud-spanner/src/test/java/com/google/cloud/spanner/SpannerImplTest.java

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,16 @@
1717
package com.google.cloud.spanner;
1818

1919
import static com.google.common.truth.Truth.assertThat;
20+
import static org.hamcrest.CoreMatchers.is;
21+
import static org.junit.Assert.assertThat;
2022
import static org.junit.Assert.fail;
2123

2224
import com.google.cloud.grpc.GrpcTransportOptions;
2325
import com.google.cloud.spanner.spi.v1.SpannerRpc;
2426
import java.util.HashMap;
2527
import java.util.Map;
2628
import java.util.concurrent.Callable;
29+
import javax.net.ssl.SSLHandshakeException;
2730
import org.junit.Before;
2831
import org.junit.Test;
2932
import org.junit.runner.RunWith;
@@ -133,4 +136,49 @@ public Void call() throws Exception {
133136
assertThat(e.getMessage().contains("Unexpected exception thrown"));
134137
}
135138
}
139+
140+
@Test
141+
public void sslHandshakeExceptionIsNotRetryable() {
142+
// Verify that a SpannerException with code UNAVAILABLE and cause SSLHandshakeException is not
143+
// retryable.
144+
boolean gotExpectedException = false;
145+
try {
146+
SpannerImpl.runWithRetries(
147+
new Callable<Object>() {
148+
@Override
149+
public Void call() throws Exception {
150+
throw SpannerExceptionFactory.newSpannerException(
151+
ErrorCode.UNAVAILABLE,
152+
"This exception should not be retryable",
153+
new SSLHandshakeException("some SSL handshake exception"));
154+
}
155+
});
156+
} catch (SpannerException e) {
157+
gotExpectedException = true;
158+
assertThat(e.isRetryable(), is(false));
159+
assertThat(e.getErrorCode()).isEqualTo(ErrorCode.UNAVAILABLE);
160+
assertThat(e.getMessage().contains("This exception should not be retryable"));
161+
}
162+
assertThat(gotExpectedException, is(true));
163+
164+
// Verify that any other SpannerException with code UNAVAILABLE is retryable.
165+
SpannerImpl.runWithRetries(
166+
new Callable<Object>() {
167+
private boolean firstTime = true;
168+
169+
@Override
170+
public Void call() throws Exception {
171+
// Keep track of whethr this is the first call or a subsequent call to avoid an infinite
172+
// loop.
173+
if (firstTime) {
174+
firstTime = false;
175+
throw SpannerExceptionFactory.newSpannerException(
176+
ErrorCode.UNAVAILABLE,
177+
"This exception should be retryable",
178+
new Exception("some other exception"));
179+
}
180+
return null;
181+
}
182+
});
183+
}
136184
}

0 commit comments

Comments
 (0)