Skip to content

Commit d0e3d41

Browse files
authored
fix: stop preparing session on most errors (#181)
Most errors that occur during preparing a session for a read/write transaction should be considered permanent, and should stop the automatic preparing of sessions. Any subsequent call to get a read/write session will cause an in-process BeginTransaction RPC to be executed. If the problem has been fixed in the meantime, the RPC will succeed and the automatic prepare of sessions will start again. Otherwise, the error is propagated to the user. Fixes #177
1 parent d80428a commit d0e3d41

File tree

2 files changed

+94
-24
lines changed

2 files changed

+94
-24
lines changed

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

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
import com.google.common.base.Supplier;
5050
import com.google.common.collect.ImmutableList;
5151
import com.google.common.collect.ImmutableMap;
52+
import com.google.common.collect.ImmutableSet;
5253
import com.google.common.util.concurrent.ListenableFuture;
5354
import com.google.common.util.concurrent.MoreExecutors;
5455
import com.google.common.util.concurrent.SettableFuture;
@@ -102,6 +103,17 @@ final class SessionPool {
102103
private static final Logger logger = Logger.getLogger(SessionPool.class.getName());
103104
private static final Tracer tracer = Tracing.getTracer();
104105
static final String WAIT_FOR_SESSION = "SessionPool.WaitForSession";
106+
static final ImmutableSet<ErrorCode> SHOULD_STOP_PREPARE_SESSIONS_ERROR_CODES =
107+
ImmutableSet.of(
108+
ErrorCode.UNKNOWN,
109+
ErrorCode.INVALID_ARGUMENT,
110+
ErrorCode.PERMISSION_DENIED,
111+
ErrorCode.UNAUTHENTICATED,
112+
ErrorCode.RESOURCE_EXHAUSTED,
113+
ErrorCode.FAILED_PRECONDITION,
114+
ErrorCode.OUT_OF_RANGE,
115+
ErrorCode.UNIMPLEMENTED,
116+
ErrorCode.INTERNAL);
105117

106118
/**
107119
* Wrapper around current time so that we can fake it in tests. TODO(user): Replace with Java 8
@@ -1114,6 +1126,9 @@ private static enum Position {
11141126
@GuardedBy("lock")
11151127
private ResourceNotFoundException resourceNotFoundException;
11161128

1129+
@GuardedBy("lock")
1130+
private boolean stopAutomaticPrepare;
1131+
11171132
@GuardedBy("lock")
11181133
private final LinkedList<PooledSession> readSessions = new LinkedList<>();
11191134

@@ -1348,8 +1363,9 @@ private boolean isDatabaseOrInstanceNotFound(SpannerException e) {
13481363
return e instanceof DatabaseNotFoundException || e instanceof InstanceNotFoundException;
13491364
}
13501365

1351-
private boolean isPermissionDenied(SpannerException e) {
1352-
return e.getErrorCode() == ErrorCode.PERMISSION_DENIED;
1366+
private boolean shouldStopPrepareSessions(SpannerException e) {
1367+
return isDatabaseOrInstanceNotFound(e)
1368+
|| SHOULD_STOP_PREPARE_SESSIONS_ERROR_CODES.contains(e.getErrorCode());
13531369
}
13541370

13551371
private void invalidateSession(PooledSession session) {
@@ -1477,7 +1493,7 @@ PooledSession getReadWriteSession() {
14771493
// session.
14781494
while (true) {
14791495
Waiter waiter = null;
1480-
boolean inProcessPrepare = false;
1496+
boolean inProcessPrepare = stopAutomaticPrepare;
14811497
synchronized (lock) {
14821498
if (closureFuture != null) {
14831499
span.addAnnotation("Pool has been closed");
@@ -1494,7 +1510,7 @@ PooledSession getReadWriteSession() {
14941510
}
14951511
sess = writePreparedSessions.poll();
14961512
if (sess == null) {
1497-
if (numSessionsBeingPrepared <= prepareThreadPoolSize) {
1513+
if (!inProcessPrepare && numSessionsBeingPrepared <= prepareThreadPoolSize) {
14981514
if (numSessionsBeingPrepared <= readWriteWaiters.size()) {
14991515
PooledSession readSession = readSessions.poll();
15001516
if (readSession != null) {
@@ -1550,12 +1566,16 @@ PooledSession getReadWriteSession() {
15501566
if (inProcessPrepare) {
15511567
try {
15521568
sess.prepareReadWriteTransaction();
1569+
// Session prepare succeeded, restart automatic prepare if it had been stopped.
1570+
synchronized (lock) {
1571+
stopAutomaticPrepare = false;
1572+
}
15531573
} catch (Throwable t) {
1554-
sess = null;
15551574
SpannerException e = newSpannerException(t);
15561575
if (!isClosed()) {
15571576
handlePrepareSessionFailure(e, sess, false);
15581577
}
1578+
sess = null;
15591579
if (!isSessionNotFound(e)) {
15601580
throw e;
15611581
}
@@ -1696,25 +1716,30 @@ private void handlePrepareSessionFailure(
16961716
synchronized (lock) {
16971717
if (isSessionNotFound(e)) {
16981718
invalidateSession(session);
1699-
} else if (isDatabaseOrInstanceNotFound(e) || isPermissionDenied(e)) {
1700-
// Database has been deleted or the user has no permission to write to this database. We
1701-
// should stop trying to prepare any transactions. Also propagate the error to all waiters,
1702-
// as any further waiting is pointless.
1719+
} else if (shouldStopPrepareSessions(e)) {
1720+
// Database has been deleted or the user has no permission to write to this database, or
1721+
// there is some other semi-permanent error. We should stop trying to prepare any
1722+
// transactions. Also propagate the error to all waiters if the database or instance has
1723+
// been deleted, as any further waiting is pointless.
1724+
stopAutomaticPrepare = true;
17031725
while (readWriteWaiters.size() > 0) {
17041726
readWriteWaiters.poll().put(e);
17051727
}
17061728
while (readWaiters.size() > 0) {
17071729
readWaiters.poll().put(e);
17081730
}
1709-
// Remove the session from the pool.
1710-
allSessions.remove(session);
1711-
if (isClosed()) {
1712-
decrementPendingClosures(1);
1731+
if (isDatabaseOrInstanceNotFound(e)) {
1732+
// Remove the session from the pool.
1733+
if (isClosed()) {
1734+
decrementPendingClosures(1);
1735+
}
1736+
allSessions.remove(session);
1737+
this.resourceNotFoundException =
1738+
MoreObjects.firstNonNull(
1739+
this.resourceNotFoundException, (ResourceNotFoundException) e);
1740+
} else {
1741+
releaseSession(session, Position.FIRST);
17131742
}
1714-
this.resourceNotFoundException =
1715-
MoreObjects.firstNonNull(
1716-
this.resourceNotFoundException,
1717-
isDatabaseOrInstanceNotFound(e) ? (ResourceNotFoundException) e : null);
17181743
} else if (informFirstWaiter && readWriteWaiters.size() > 0) {
17191744
releaseSession(session, Position.FIRST);
17201745
readWriteWaiters.poll().put(e);
@@ -1809,6 +1834,9 @@ private boolean shouldUnblockReader() {
18091834

18101835
private boolean shouldPrepareSession() {
18111836
synchronized (lock) {
1837+
if (stopAutomaticPrepare) {
1838+
return false;
1839+
}
18121840
int preparedSessions = writePreparedSessions.size() + numSessionsBeingPrepared;
18131841
return preparedSessions < Math.floor(options.getWriteSessionsFraction() * totalSessions());
18141842
}

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

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -469,12 +469,25 @@ public void testDatabaseOrInstanceDoesNotExistOnReplenish() throws Exception {
469469

470470
@Test
471471
public void testPermissionDeniedOnPrepareSession() throws Exception {
472+
testExceptionOnPrepareSession(
473+
Status.PERMISSION_DENIED
474+
.withDescription(
475+
"Caller is missing IAM permission spanner.databases.beginOrRollbackReadWriteTransaction on resource")
476+
.asRuntimeException());
477+
}
478+
479+
@Test
480+
public void testFailedPreconditionOnPrepareSession() throws Exception {
481+
testExceptionOnPrepareSession(
482+
Status.FAILED_PRECONDITION
483+
.withDescription("FAILED_PRECONDITION: Database is in read-only mode")
484+
.asRuntimeException());
485+
}
486+
487+
private void testExceptionOnPrepareSession(StatusRuntimeException exception)
488+
throws InterruptedException {
472489
mockSpanner.setBeginTransactionExecutionTime(
473-
SimulatedExecutionTime.ofStickyException(
474-
Status.PERMISSION_DENIED
475-
.withDescription(
476-
"Caller is missing IAM permission spanner.databases.beginOrRollbackReadWriteTransaction on resource")
477-
.asRuntimeException()));
490+
SimulatedExecutionTime.ofStickyException(exception));
478491
DatabaseClientImpl dbClient =
479492
(DatabaseClientImpl)
480493
spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE));
@@ -503,10 +516,39 @@ public Void run(TransactionContext transaction) throws Exception {
503516
return null;
504517
}
505518
});
506-
fail("missing expected PERMISSION_DENIED exception");
519+
fail(String.format("missing expected %s exception", exception.getStatus().getCode().name()));
507520
} catch (SpannerException e) {
508-
assertThat(e.getErrorCode(), is(equalTo(ErrorCode.PERMISSION_DENIED)));
521+
assertThat(e.getErrorCode(), is(equalTo(ErrorCode.fromGrpcStatus(exception.getStatus()))));
522+
}
523+
// Remove the semi-permanent error condition. Getting a read/write transaction should now
524+
// succeed, and the automatic preparing of sessions should be restarted.
525+
mockSpanner.setBeginTransactionExecutionTime(SimulatedExecutionTime.none());
526+
dbClient
527+
.readWriteTransaction()
528+
.run(
529+
new TransactionCallable<Void>() {
530+
@Override
531+
public Void run(TransactionContext transaction) throws Exception {
532+
return null;
533+
}
534+
});
535+
for (int i = 0; i < spanner.getOptions().getSessionPoolOptions().getMinSessions(); i++) {
536+
dbClient.pool.getReadSession().close();
509537
}
538+
int expectedPreparedSessions =
539+
(int)
540+
Math.ceil(
541+
dbClient.pool.getNumberOfSessionsInPool()
542+
* spanner.getOptions().getSessionPoolOptions().getWriteSessionsFraction());
543+
watch = watch.reset().start();
544+
while (watch.elapsed(TimeUnit.SECONDS) < 5
545+
&& dbClient.pool.getNumberOfAvailableWritePreparedSessions() < expectedPreparedSessions) {
546+
Thread.sleep(1L);
547+
}
548+
assertThat(dbClient.pool.getNumberOfSessionsBeingPrepared(), is(equalTo(0)));
549+
assertThat(
550+
dbClient.pool.getNumberOfAvailableWritePreparedSessions(),
551+
is(equalTo(expectedPreparedSessions)));
510552
}
511553

512554
/**

0 commit comments

Comments
 (0)