Skip to content

Commit 89c3e77

Browse files
authored
fix: use resource type to identify type of error (#57)
* fix: use resource type to identify type of error * fix: add test for DatabaseNotFoundException
1 parent 74b6b98 commit 89c3e77

File tree

9 files changed

+180
-56
lines changed

9 files changed

+180
-56
lines changed

google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseNotFoundException.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,24 @@
1616

1717
package com.google.cloud.spanner;
1818

19+
import com.google.cloud.spanner.SpannerException.ResourceNotFoundException;
20+
import com.google.rpc.ResourceInfo;
1921
import javax.annotation.Nullable;
2022

2123
/**
2224
* Exception thrown by Cloud Spanner when an operation detects that the database that is being used
2325
* no longer exists. This type of error has its own subclass as it is a condition that should cause
2426
* the client library to stop trying to send RPCs to the backend until the user has taken action.
2527
*/
26-
public class DatabaseNotFoundException extends SpannerException {
28+
public class DatabaseNotFoundException extends ResourceNotFoundException {
2729
private static final long serialVersionUID = -6395746612598975751L;
2830

2931
/** Private constructor. Use {@link SpannerExceptionFactory} to create instances. */
3032
DatabaseNotFoundException(
31-
DoNotConstructDirectly token, @Nullable String message, @Nullable Throwable cause) {
32-
super(token, ErrorCode.NOT_FOUND, false, message, cause);
33+
DoNotConstructDirectly token,
34+
@Nullable String message,
35+
ResourceInfo resourceInfo,
36+
@Nullable Throwable cause) {
37+
super(token, message, resourceInfo, cause);
3338
}
3439
}

google-cloud-spanner/src/main/java/com/google/cloud/spanner/SessionNotFoundException.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,24 @@
1616

1717
package com.google.cloud.spanner;
1818

19+
import com.google.cloud.spanner.SpannerException.ResourceNotFoundException;
20+
import com.google.rpc.ResourceInfo;
1921
import javax.annotation.Nullable;
2022

2123
/**
2224
* Exception thrown by Cloud Spanner when an operation detects that the session that is being used
2325
* is no longer valid. This type of error has its own subclass as it is a condition that should
2426
* normally be hidden from the user, and the client library should try to fix this internally.
2527
*/
26-
public class SessionNotFoundException extends SpannerException {
28+
public class SessionNotFoundException extends ResourceNotFoundException {
2729
private static final long serialVersionUID = -6395746612598975751L;
2830

2931
/** Private constructor. Use {@link SpannerExceptionFactory} to create instances. */
3032
SessionNotFoundException(
31-
DoNotConstructDirectly token, @Nullable String message, @Nullable Throwable cause) {
32-
super(token, ErrorCode.NOT_FOUND, false, message, cause);
33+
DoNotConstructDirectly token,
34+
@Nullable String message,
35+
ResourceInfo resourceInfo,
36+
@Nullable Throwable cause) {
37+
super(token, message, resourceInfo, cause);
3338
}
3439
}

google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerException.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import com.google.cloud.grpc.BaseGrpcServiceException;
2020
import com.google.common.base.Preconditions;
2121
import com.google.protobuf.util.Durations;
22+
import com.google.rpc.ResourceInfo;
2223
import com.google.rpc.RetryInfo;
2324
import io.grpc.Metadata;
2425
import io.grpc.Status;
@@ -27,6 +28,24 @@
2728

2829
/** Base exception type for all exceptions produced by the Cloud Spanner service. */
2930
public class SpannerException extends BaseGrpcServiceException {
31+
/** Base exception type for NOT_FOUND exceptions for known resource types. */
32+
public abstract static class ResourceNotFoundException extends SpannerException {
33+
private final ResourceInfo resourceInfo;
34+
35+
ResourceNotFoundException(
36+
DoNotConstructDirectly token,
37+
@Nullable String message,
38+
ResourceInfo resourceInfo,
39+
@Nullable Throwable cause) {
40+
super(token, ErrorCode.NOT_FOUND, /* retryable */ false, message, cause);
41+
this.resourceInfo = resourceInfo;
42+
}
43+
44+
public String getResourceName() {
45+
return resourceInfo.getResourceName();
46+
}
47+
}
48+
3049
private static final long serialVersionUID = 20150916L;
3150
private static final Metadata.Key<RetryInfo> KEY_RETRY_INFO =
3251
ProtoUtils.keyForProto(RetryInfo.getDefaultInstance());

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

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@
2121
import com.google.cloud.spanner.SpannerException.DoNotConstructDirectly;
2222
import com.google.common.base.MoreObjects;
2323
import com.google.common.base.Predicate;
24+
import com.google.rpc.ResourceInfo;
2425
import io.grpc.Context;
26+
import io.grpc.Metadata;
2527
import io.grpc.Status;
2628
import io.grpc.StatusRuntimeException;
29+
import io.grpc.protobuf.ProtoUtils;
2730
import java.util.concurrent.CancellationException;
2831
import java.util.concurrent.TimeoutException;
29-
import java.util.regex.Pattern;
3032
import javax.annotation.Nullable;
3133
import javax.net.ssl.SSLHandshakeException;
3234

@@ -37,13 +39,11 @@
3739
* ErrorCode#ABORTED} are always represented by {@link AbortedException}.
3840
*/
3941
public final class SpannerExceptionFactory {
40-
static final String DATABASE_NOT_FOUND_MSG =
41-
"Database not found: projects/.*/instances/.*/databases/.*\n"
42-
+ "resource_type: \"type.googleapis.com/google.spanner.admin.database.v1.Database\"\n"
43-
+ "resource_name: \"projects/.*/instances/.*/databases/.*\"\n"
44-
+ "description: \"Database does not exist.\"\n";
45-
private static final Pattern DATABASE_NOT_FOUND_MSG_PATTERN =
46-
Pattern.compile(".*" + DATABASE_NOT_FOUND_MSG + ".*");
42+
static final String SESSION_RESOURCE_TYPE = "type.googleapis.com/google.spanner.v1.Session";
43+
static final String DATABASE_RESOURCE_TYPE =
44+
"type.googleapis.com/google.spanner.admin.database.v1.Database";
45+
private static final Metadata.Key<ResourceInfo> KEY_RESOURCE_INFO =
46+
ProtoUtils.keyForProto(ResourceInfo.getDefaultInstance());
4747

4848
public static SpannerException newSpannerException(ErrorCode code, @Nullable String message) {
4949
return newSpannerException(code, message, null);
@@ -175,6 +175,16 @@ private static String formatMessage(ErrorCode code, @Nullable String message) {
175175
return message.startsWith(code.toString()) ? message : code + ": " + message;
176176
}
177177

178+
private static ResourceInfo extractResourceInfo(Throwable cause) {
179+
if (cause != null) {
180+
Metadata trailers = Status.trailersFromThrowable(cause);
181+
if (trailers != null) {
182+
return trailers.get(KEY_RESOURCE_INFO);
183+
}
184+
}
185+
return null;
186+
}
187+
178188
private static SpannerException newSpannerExceptionPreformatted(
179189
ErrorCode code, @Nullable String message, @Nullable Throwable cause) {
180190
// This is the one place in the codebase that is allowed to call constructors directly.
@@ -183,10 +193,13 @@ private static SpannerException newSpannerExceptionPreformatted(
183193
case ABORTED:
184194
return new AbortedException(token, message, cause);
185195
case NOT_FOUND:
186-
if (message != null && message.contains("Session not found")) {
187-
return new SessionNotFoundException(token, message, cause);
188-
} else if (message != null && DATABASE_NOT_FOUND_MSG_PATTERN.matcher(message).matches()) {
189-
return new DatabaseNotFoundException(token, message, cause);
196+
ResourceInfo resourceInfo = extractResourceInfo(cause);
197+
if (resourceInfo != null) {
198+
if (resourceInfo.getResourceType().equals(SESSION_RESOURCE_TYPE)) {
199+
return new SessionNotFoundException(token, message, resourceInfo, cause);
200+
} else if (resourceInfo.getResourceType().equals(DATABASE_RESOURCE_TYPE)) {
201+
return new DatabaseNotFoundException(token, message, resourceInfo, cause);
202+
}
190203
}
191204
// Fall through to the default.
192205
default:

google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java

Lines changed: 12 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -51,24 +51,12 @@
5151

5252
@RunWith(JUnit4.class)
5353
public class DatabaseClientImplTest {
54-
private static final String DATABASE_NOT_FOUND_FORMAT =
55-
"Database not found: projects/%s/instances/%s/databases/%s\n"
56-
+ "resource_type: \"type.googleapis.com/google.spanner.admin.database.v1.Database\"\n"
57-
+ "resource_name: \"projects/%s/instances/%s/databases/%s\"\n"
58-
+ "description: \"Database does not exist.\"\n";
5954
private static final String TEST_PROJECT = "my-project";
6055
private static final String TEST_INSTANCE = "my-instance";
6156
private static final String TEST_DATABASE = "my-database";
62-
private static final String DATABASE_NOT_FOUND_MSG =
57+
private static final String DATABASE_NAME =
6358
String.format(
64-
"com.google.cloud.spanner.SpannerException: NOT_FOUND: io.grpc.StatusRuntimeException: NOT_FOUND: "
65-
+ DATABASE_NOT_FOUND_FORMAT,
66-
TEST_PROJECT,
67-
TEST_INSTANCE,
68-
TEST_DATABASE,
69-
TEST_PROJECT,
70-
TEST_INSTANCE,
71-
TEST_DATABASE);
59+
"projects/%s/instances/%s/databases/%s", TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE);
7260
private static MockSpannerServiceImpl mockSpanner;
7361
private static Server server;
7462
private static LocalChannelProvider channelProvider;
@@ -283,7 +271,8 @@ public Long run(TransactionContext transaction) throws Exception {
283271
public void testDatabaseDoesNotExistOnPrepareSession() throws Exception {
284272
mockSpanner.setBeginTransactionExecutionTime(
285273
SimulatedExecutionTime.ofStickyException(
286-
Status.NOT_FOUND.withDescription(DATABASE_NOT_FOUND_MSG).asRuntimeException()));
274+
SpannerExceptionFactoryTest.newStatusResourceNotFoundException(
275+
"Database", SpannerExceptionFactory.DATABASE_RESOURCE_TYPE, TEST_DATABASE)));
287276
DatabaseClientImpl dbClient =
288277
(DatabaseClientImpl)
289278
spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE));
@@ -323,7 +312,8 @@ public Void run(TransactionContext transaction) throws Exception {
323312
public void testDatabaseDoesNotExistOnInitialization() throws Exception {
324313
mockSpanner.setBatchCreateSessionsExecutionTime(
325314
SimulatedExecutionTime.ofStickyException(
326-
Status.NOT_FOUND.withDescription(DATABASE_NOT_FOUND_MSG).asRuntimeException()));
315+
SpannerExceptionFactoryTest.newStatusResourceNotFoundException(
316+
"Database", SpannerExceptionFactory.DATABASE_RESOURCE_TYPE, DATABASE_NAME)));
327317
DatabaseClientImpl dbClient =
328318
(DatabaseClientImpl)
329319
spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE));
@@ -342,7 +332,8 @@ public void testDatabaseDoesNotExistOnInitialization() throws Exception {
342332
public void testDatabaseDoesNotExistOnCreate() throws Exception {
343333
mockSpanner.setBatchCreateSessionsExecutionTime(
344334
SimulatedExecutionTime.ofStickyException(
345-
Status.NOT_FOUND.withDescription(DATABASE_NOT_FOUND_MSG).asRuntimeException()));
335+
SpannerExceptionFactoryTest.newStatusResourceNotFoundException(
336+
"Database", SpannerExceptionFactory.DATABASE_RESOURCE_TYPE, DATABASE_NAME)));
346337
// Ensure there are no sessions in the pool by default.
347338
try (Spanner spanner =
348339
SpannerOptions.newBuilder()
@@ -376,7 +367,8 @@ public void testDatabaseDoesNotExistOnCreate() throws Exception {
376367
public void testDatabaseDoesNotExistOnReplenish() throws Exception {
377368
mockSpanner.setBatchCreateSessionsExecutionTime(
378369
SimulatedExecutionTime.ofStickyException(
379-
Status.NOT_FOUND.withDescription(DATABASE_NOT_FOUND_MSG).asRuntimeException()));
370+
SpannerExceptionFactoryTest.newStatusResourceNotFoundException(
371+
"Database", SpannerExceptionFactory.DATABASE_RESOURCE_TYPE, DATABASE_NAME)));
380372
DatabaseClientImpl dbClient =
381373
(DatabaseClientImpl)
382374
spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE));
@@ -471,7 +463,8 @@ public void testDatabaseIsDeletedAndThenRecreated() throws Exception {
471463
// Simulate that the database has been deleted.
472464
mockSpanner.setStickyGlobalExceptions(true);
473465
mockSpanner.addException(
474-
Status.NOT_FOUND.withDescription(DATABASE_NOT_FOUND_MSG).asRuntimeException());
466+
SpannerExceptionFactoryTest.newStatusResourceNotFoundException(
467+
"Database", SpannerExceptionFactory.DATABASE_RESOURCE_TYPE, DATABASE_NAME));
475468

476469
// All subsequent calls should fail with a DatabaseNotFoundException.
477470
try (ResultSet rs = dbClient.singleUse().executeQuery(SELECT1)) {

google-cloud-spanner/src/test/java/com/google/cloud/spanner/MockSpannerServiceImpl.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import com.google.protobuf.Timestamp;
3232
import com.google.protobuf.Value.KindCase;
3333
import com.google.rpc.Code;
34+
import com.google.rpc.ResourceInfo;
3435
import com.google.rpc.RetryInfo;
3536
import com.google.spanner.v1.BatchCreateSessionsRequest;
3637
import com.google.spanner.v1.BatchCreateSessionsResponse;
@@ -727,10 +728,21 @@ public void getSession(GetSessionRequest request, StreamObserver<Session> respon
727728
}
728729

729730
private <T> void setSessionNotFound(String name, StreamObserver<T> responseObserver) {
731+
ResourceInfo resourceInfo =
732+
ResourceInfo.newBuilder()
733+
.setResourceType(SpannerExceptionFactory.SESSION_RESOURCE_TYPE)
734+
.setResourceName(name)
735+
.build();
736+
Metadata.Key<ResourceInfo> key =
737+
Metadata.Key.of(
738+
resourceInfo.getDescriptorForType().getFullName() + Metadata.BINARY_HEADER_SUFFIX,
739+
ProtoLiteUtils.metadataMarshaller(resourceInfo));
740+
Metadata trailers = new Metadata();
741+
trailers.put(key, resourceInfo);
730742
responseObserver.onError(
731743
Status.NOT_FOUND
732744
.withDescription(String.format("Session not found: Session with id %s not found", name))
733-
.asRuntimeException());
745+
.asRuntimeException(trailers));
734746
}
735747

736748
@Override

google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolStressTest.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -162,8 +162,7 @@ public ApiFuture<Empty> answer(InvocationOnMock invocation) throws Throwable {
162162
synchronized (lock) {
163163
if (expiredSessions.contains(session.getName())) {
164164
return ApiFutures.immediateFailedFuture(
165-
SpannerExceptionFactory.newSpannerException(
166-
ErrorCode.NOT_FOUND, "Session not found"));
165+
SpannerExceptionFactoryTest.newSessionNotFoundException(session.getName()));
167166
}
168167
if (sessions.remove(session.getName()) == null) {
169168
setFailed(closedSessions.get(session.getName()));
@@ -185,8 +184,7 @@ public ApiFuture<Empty> answer(InvocationOnMock invocation) throws Throwable {
185184
public Void answer(InvocationOnMock invocation) throws Throwable {
186185
if (random.nextInt(100) < 10) {
187186
expireSession(session);
188-
throw SpannerExceptionFactory.newSpannerException(
189-
ErrorCode.NOT_FOUND, "Session not found");
187+
throw SpannerExceptionFactoryTest.newSessionNotFoundException(session.getName());
190188
}
191189
synchronized (lock) {
192190
if (sessions.put(session.getName(), true)) {

google-cloud-spanner/src/test/java/com/google/cloud/spanner/SessionPoolTest.java

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ public class SessionPoolTest extends BaseSessionPoolTest {
9494
DatabaseId db = DatabaseId.of("projects/p/instances/i/databases/unused");
9595
SessionPool pool;
9696
SessionPoolOptions options;
97+
private String sessionName = String.format("%s/sessions/s", db.getName());
9798

9899
@Parameters(name = "min sessions = {0}")
99100
public static Collection<Object[]> data() {
@@ -818,7 +819,7 @@ public void poolWorksWhenSessionNotFound() {
818819
SessionImpl mockSession2 = mockSession();
819820
final LinkedList<SessionImpl> sessions =
820821
new LinkedList<>(Arrays.asList(mockSession1, mockSession2));
821-
doThrow(SpannerExceptionFactory.newSpannerException(ErrorCode.NOT_FOUND, "Session not found"))
822+
doThrow(SpannerExceptionFactoryTest.newSessionNotFoundException(sessionName))
822823
.when(mockSession1)
823824
.prepareReadWriteTransaction();
824825
doAnswer(
@@ -1029,8 +1030,7 @@ public void testSessionNotFoundSingleUse() {
10291030
ReadContext closedContext = mock(ReadContext.class);
10301031
ResultSet closedResultSet = mock(ResultSet.class);
10311032
when(closedResultSet.next())
1032-
.thenThrow(
1033-
SpannerExceptionFactory.newSpannerException(ErrorCode.NOT_FOUND, "Session not found"));
1033+
.thenThrow(SpannerExceptionFactoryTest.newSessionNotFoundException(sessionName));
10341034
when(closedContext.executeQuery(statement)).thenReturn(closedResultSet);
10351035
when(closedSession.singleUse()).thenReturn(closedContext);
10361036

@@ -1088,8 +1088,7 @@ public void testSessionNotFoundReadOnlyTransaction() {
10881088
Statement statement = Statement.of("SELECT 1");
10891089
final SessionImpl closedSession = mockSession();
10901090
when(closedSession.readOnlyTransaction())
1091-
.thenThrow(
1092-
SpannerExceptionFactory.newSpannerException(ErrorCode.NOT_FOUND, "Session not found"));
1091+
.thenThrow(SpannerExceptionFactoryTest.newSessionNotFoundException(sessionName));
10931092

10941093
final SessionImpl openSession = mockSession();
10951094
ReadOnlyTransaction openTransaction = mock(ReadOnlyTransaction.class);
@@ -1155,7 +1154,7 @@ public void testSessionNotFoundReadWriteTransaction() {
11551154
final Statement queryStatement = Statement.of("SELECT 1");
11561155
final Statement updateStatement = Statement.of("UPDATE FOO SET BAR=1 WHERE ID=2");
11571156
final SpannerException sessionNotFound =
1158-
SpannerExceptionFactory.newSpannerException(ErrorCode.NOT_FOUND, "Session not found");
1157+
SpannerExceptionFactoryTest.newSessionNotFoundException(sessionName);
11591158
for (ReadWriteTransactionTestStatementType statementType :
11601159
ReadWriteTransactionTestStatementType.values()) {
11611160
final ReadWriteTransactionTestStatementType executeStatementType = statementType;
@@ -1340,7 +1339,7 @@ public Integer run(TransactionContext transaction) throws Exception {
13401339
@Test
13411340
public void testSessionNotFoundOnPrepareTransaction() {
13421341
final SpannerException sessionNotFound =
1343-
SpannerExceptionFactory.newSpannerException(ErrorCode.NOT_FOUND, "Session not found");
1342+
SpannerExceptionFactoryTest.newSessionNotFoundException(sessionName);
13441343
final SessionImpl closedSession = mock(SessionImpl.class);
13451344
when(closedSession.getName())
13461345
.thenReturn("projects/dummy/instances/dummy/database/dummy/sessions/session-closed");
@@ -1394,7 +1393,7 @@ public void run() {
13941393
@Test
13951394
public void testSessionNotFoundWrite() {
13961395
SpannerException sessionNotFound =
1397-
SpannerExceptionFactory.newSpannerException(ErrorCode.NOT_FOUND, "Session not found");
1396+
SpannerExceptionFactoryTest.newSessionNotFoundException(sessionName);
13981397
List<Mutation> mutations = Arrays.asList(Mutation.newInsertBuilder("FOO").build());
13991398
final SessionImpl closedSession = mockSession();
14001399
when(closedSession.write(mutations)).thenThrow(sessionNotFound);
@@ -1446,7 +1445,7 @@ public void run() {
14461445
@Test
14471446
public void testSessionNotFoundWriteAtLeastOnce() {
14481447
SpannerException sessionNotFound =
1449-
SpannerExceptionFactory.newSpannerException(ErrorCode.NOT_FOUND, "Session not found");
1448+
SpannerExceptionFactoryTest.newSessionNotFoundException(sessionName);
14501449
List<Mutation> mutations = Arrays.asList(Mutation.newInsertBuilder("FOO").build());
14511450
final SessionImpl closedSession = mockSession();
14521451
when(closedSession.writeAtLeastOnce(mutations)).thenThrow(sessionNotFound);
@@ -1497,7 +1496,7 @@ public void run() {
14971496
@Test
14981497
public void testSessionNotFoundPartitionedUpdate() {
14991498
SpannerException sessionNotFound =
1500-
SpannerExceptionFactory.newSpannerException(ErrorCode.NOT_FOUND, "Session not found");
1499+
SpannerExceptionFactoryTest.newSessionNotFoundException(sessionName);
15011500
Statement statement = Statement.of("UPDATE FOO SET BAR=1 WHERE 1=1");
15021501
final SessionImpl closedSession = mockSession();
15031502
when(closedSession.executePartitionedUpdate(statement)).thenThrow(sessionNotFound);

0 commit comments

Comments
 (0)