-
Notifications
You must be signed in to change notification settings - Fork 101
/
SpannerExceptionFactory.java
257 lines (239 loc) · 10.2 KB
/
SpannerExceptionFactory.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
/*
* Copyright 2017 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;
import com.google.api.gax.grpc.GrpcStatusCode;
import com.google.api.gax.rpc.ApiException;
import com.google.cloud.spanner.SpannerException.DoNotConstructDirectly;
import com.google.common.base.MoreObjects;
import com.google.common.base.Predicate;
import io.grpc.Context;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import java.util.concurrent.CancellationException;
import java.util.concurrent.TimeoutException;
import javax.annotation.Nullable;
import javax.net.ssl.SSLHandshakeException;
/**
* A factory for creating instances of {@link SpannerException} and its subtypes. All creation of
* these exceptions is directed through the factory. This ensures that particular types of errors
* are always expressed as the same concrete exception type. For example, exceptions of type {@link
* ErrorCode#ABORTED} are always represented by {@link AbortedException}.
*/
public final class SpannerExceptionFactory {
public static SpannerException newSpannerException(ErrorCode code, @Nullable String message) {
return newSpannerException(code, message, null);
}
public static SpannerException newSpannerException(
ErrorCode code, @Nullable String message, @Nullable Throwable cause) {
return newSpannerExceptionPreformatted(code, formatMessage(code, message), cause);
}
public static SpannerException propagateInterrupt(InterruptedException e) {
Thread.currentThread().interrupt();
return SpannerExceptionFactory.newSpannerException(ErrorCode.CANCELLED, "Interrupted", e);
}
/**
* Transforms a {@code TimeoutException} to a {@code SpannerException}.
*
* <pre>
* <code>
* try {
* Spanner spanner = SpannerOptions.getDefaultInstance();
* spanner
* .getDatabaseAdminClient()
* .createDatabase("[INSTANCE_ID]", "[DATABASE_ID]", [STATEMENTS])
* .get();
* } catch (TimeoutException e) {
* propagateTimeout(e);
* }
* </code>
* </pre>
*/
public static SpannerException propagateTimeout(TimeoutException e) {
return SpannerExceptionFactory.newSpannerException(
ErrorCode.DEADLINE_EXCEEDED, "Operation did not complete in the given time", e);
}
/**
* Creates a new exception based on {@code cause}.
*
* <p>Intended for internal library use; user code should use {@link
* #newSpannerException(ErrorCode, String)} instead of this method.
*/
public static SpannerException newSpannerException(Throwable cause) {
return newSpannerException(null, cause);
}
public static SpannerBatchUpdateException newSpannerBatchUpdateException(
ErrorCode code, String message, long[] updateCounts) {
DoNotConstructDirectly token = DoNotConstructDirectly.ALLOWED;
return new SpannerBatchUpdateException(token, code, message, updateCounts);
}
/**
* Constructs a specific aborted exception that should only be thrown by a connection after an
* internal retry aborted due to concurrent modifications.
*/
public static AbortedDueToConcurrentModificationException
newAbortedDueToConcurrentModificationException(AbortedException cause) {
return new AbortedDueToConcurrentModificationException(
DoNotConstructDirectly.ALLOWED,
"The transaction was aborted and could not be retried due to a concurrent modification",
cause);
}
/**
* Constructs a specific aborted exception that should only be thrown by a connection after an
* internal retry aborted because a database call caused an exception that did not happen during
* the original attempt.
*/
public static AbortedDueToConcurrentModificationException
newAbortedDueToConcurrentModificationException(
AbortedException cause, SpannerException databaseError) {
return new AbortedDueToConcurrentModificationException(
DoNotConstructDirectly.ALLOWED,
"The transaction was aborted and could not be retried due to a database error during the retry",
cause,
databaseError);
}
/**
* Creates a new exception based on {@code cause}. If {@code cause} indicates cancellation, {@code
* context} will be inspected to establish the type of cancellation.
*
* <p>Intended for internal library use; user code should use {@link
* #newSpannerException(ErrorCode, String)} instead of this method.
*/
public static SpannerException newSpannerException(@Nullable Context context, Throwable cause) {
if (cause instanceof SpannerException) {
SpannerException e = (SpannerException) cause;
return newSpannerExceptionPreformatted(e.getErrorCode(), e.getMessage(), e);
} else if (cause instanceof CancellationException) {
return newSpannerExceptionForCancellation(context, cause);
} else if (cause instanceof ApiException) {
return fromApiException((ApiException) cause);
}
// Extract gRPC status. This will produce "UNKNOWN" for non-gRPC exceptions.
Status status = Status.fromThrowable(cause);
if (status.getCode() == Status.Code.CANCELLED) {
return newSpannerExceptionForCancellation(context, cause);
}
return newSpannerException(ErrorCode.fromGrpcStatus(status), cause.getMessage(), cause);
}
static SpannerException newSpannerExceptionForCancellation(
@Nullable Context context, @Nullable Throwable cause) {
if (context != null && context.isCancelled()) {
Throwable cancellationCause = context.cancellationCause();
Throwable throwable =
cause == null && cancellationCause == null
? null
: MoreObjects.firstNonNull(cause, cancellationCause);
if (cancellationCause instanceof TimeoutException) {
return newSpannerException(
ErrorCode.DEADLINE_EXCEEDED, "Current context exceeded deadline", throwable);
} else {
return newSpannerException(ErrorCode.CANCELLED, "Current context was cancelled", throwable);
}
}
return newSpannerException(
ErrorCode.CANCELLED, cause == null ? "Cancelled" : cause.getMessage(), cause);
}
private static String formatMessage(ErrorCode code, @Nullable String message) {
if (message == null) {
return code.toString();
}
// gRPC exceptions already start with the code, which happens to be the same prefix we use.
return message.startsWith(code.toString()) ? message : code + ": " + message;
}
private static SpannerException newSpannerExceptionPreformatted(
ErrorCode code, @Nullable String message, @Nullable Throwable cause) {
// This is the one place in the codebase that is allowed to call constructors directly.
DoNotConstructDirectly token = DoNotConstructDirectly.ALLOWED;
switch (code) {
case ABORTED:
return new AbortedException(token, message, cause);
case NOT_FOUND:
if (message != null && message.contains("Session not found")) {
return new SessionNotFoundException(token, message, cause);
} else if (message != null && message.contains("Database not found")) {
return new DatabaseNotFoundException(token, message, cause);
}
// Fall through to the default.
default:
return new SpannerException(token, code, isRetryable(code, cause), message, cause);
}
}
private static SpannerException fromApiException(ApiException exception) {
Status.Code code = ((GrpcStatusCode) exception.getStatusCode()).getTransportCode();
ErrorCode errorCode = ErrorCode.fromGrpcStatus(Status.fromCode(code));
if (exception.getCause() != null) {
return SpannerExceptionFactory.newSpannerException(
errorCode, exception.getMessage(), exception.getCause());
} else {
return SpannerExceptionFactory.newSpannerException(errorCode, exception.getMessage());
}
}
private static boolean isRetryable(ErrorCode code, @Nullable Throwable cause) {
switch (code) {
case INTERNAL:
return hasCauseMatching(cause, Matchers.isRetryableInternalError);
case UNAVAILABLE:
// SSLHandshakeException is (probably) not retryable, as it is an indication that the server
// certificate was not accepted by the client.
return !hasCauseMatching(cause, Matchers.isSSLHandshakeException);
case RESOURCE_EXHAUSTED:
return SpannerException.extractRetryDelay(cause) > 0;
default:
return false;
}
}
private static boolean hasCauseMatching(
@Nullable Throwable cause, Predicate<? super Throwable> matcher) {
while (cause != null) {
if (matcher.apply(cause)) {
return true;
}
cause = cause.getCause();
}
return false;
}
private static class Matchers {
static final Predicate<Throwable> isRetryableInternalError =
new Predicate<Throwable>() {
@Override
public boolean apply(Throwable cause) {
if (cause instanceof StatusRuntimeException
&& ((StatusRuntimeException) cause).getStatus().getCode() == Status.Code.INTERNAL) {
if (cause.getMessage().contains("HTTP/2 error code: INTERNAL_ERROR")) {
// See b/25451313.
return true;
}
if (cause.getMessage().contains("Connection closed with unknown cause")) {
// See b/27794742.
return true;
}
if (cause
.getMessage()
.contains("Received unexpected EOS on DATA frame from server")) {
return true;
}
}
return false;
}
};
static final Predicate<Throwable> isSSLHandshakeException =
new Predicate<Throwable>() {
@Override
public boolean apply(Throwable input) {
return input instanceof SSLHandshakeException;
}
};
}
}