File tree

2 files changed

+94
-24
lines changed

2 files changed

+94
-24
lines changed
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
}
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)