Skip to content
This repository was archived by the owner on Apr 7, 2026. It is now read-only.

Commit bc9ded4

Browse files
fix: Retry multiplexed session failures
1 parent 3d585cf commit bc9ded4

2 files changed

Lines changed: 185 additions & 23 deletions

File tree

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

Lines changed: 48 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
import java.util.concurrent.atomic.AtomicInteger;
5454
import java.util.concurrent.atomic.AtomicLong;
5555
import java.util.concurrent.atomic.AtomicReference;
56+
import java.util.concurrent.locks.ReentrantLock;
5657

5758
/**
5859
* {@link TransactionRunner} that automatically handles "UNIMPLEMENTED" errors with the message
@@ -315,6 +316,10 @@ public void close() {
315316
*/
316317
private final AtomicBoolean unimplemented = new AtomicBoolean(false);
317318

319+
private final AtomicBoolean retryingSessionCreation = new AtomicBoolean(true);
320+
321+
private final ReentrantLock sessionCreationLock = new ReentrantLock();
322+
318323
/**
319324
* This flag is set to true if the server return UNIMPLEMENTED when a read-write transaction is
320325
* executed on a multiplexed session. TODO: Remove once this is guaranteed to be available.
@@ -358,11 +363,20 @@ public void close() {
358363
SettableApiFuture.create();
359364
this.readWriteBeginTransactionReferenceFuture = SettableApiFuture.create();
360365
this.multiplexedSessionReference = new AtomicReference<>(initialSessionReferenceFuture);
366+
asyncCreateMultiplexedSession(initialSessionReferenceFuture);
367+
maybeWaitForSessionCreation(
368+
sessionClient.getSpanner().getOptions().getSessionPoolOptions(),
369+
initialSessionReferenceFuture);
370+
}
371+
372+
private void asyncCreateMultiplexedSession(
373+
SettableApiFuture<SessionReference> sessionReferenceFuture) {
361374
this.sessionClient.asyncCreateMultiplexedSession(
362375
new SessionConsumer() {
363376
@Override
364377
public void onSessionReady(SessionImpl session) {
365-
initialSessionReferenceFuture.set(session.getSessionReference());
378+
retryingSessionCreation.set(false);
379+
sessionReferenceFuture.set(session.getSessionReference());
366380
// only start the maintainer if we actually managed to create a session in the first
367381
// place.
368382
maintainer.start();
@@ -394,13 +408,11 @@ public void onSessionReady(SessionImpl session) {
394408
public void onSessionCreateFailure(Throwable t, int createFailureForSessionCount) {
395409
// Mark multiplexes sessions as unimplemented and fall back to regular sessions if
396410
// UNIMPLEMENTED is returned.
411+
retryingSessionCreation.set(false);
397412
maybeMarkUnimplemented(t);
398-
initialSessionReferenceFuture.setException(t);
413+
sessionReferenceFuture.setException(t);
399414
}
400415
});
401-
maybeWaitForSessionCreation(
402-
sessionClient.getSpanner().getOptions().getSessionPoolOptions(),
403-
initialSessionReferenceFuture);
404416
}
405417

406418
void setPool(SessionPool pool) {
@@ -546,8 +558,27 @@ MultiplexedSessionMaintainer getMaintainer() {
546558
return this.maintainer;
547559
}
548560

561+
ApiFuture<SessionReference> getCurrentSessionReferenceFuture() {
562+
return ApiFutures.immediateFuture(getCurrentSessionReference());
563+
}
564+
549565
@VisibleForTesting
550566
SessionReference getCurrentSessionReference() {
567+
try {
568+
return this.multiplexedSessionReference.get().get();
569+
} catch (ExecutionException | InterruptedException exception) {
570+
return maybeRetrySessionCreation();
571+
}
572+
}
573+
574+
private SessionReference maybeRetrySessionCreation() {
575+
sessionCreationLock.lock();
576+
if (isMultiplexedSessionsSupported() && retryingSessionCreation.compareAndSet(false, true)) {
577+
SettableApiFuture<SessionReference> settableApiFuture = SettableApiFuture.create();
578+
asyncCreateMultiplexedSession(settableApiFuture);
579+
multiplexedSessionReference.set(settableApiFuture);
580+
}
581+
sessionCreationLock.unlock();
551582
try {
552583
return this.multiplexedSessionReference.get().get();
553584
} catch (ExecutionException executionException) {
@@ -587,28 +618,22 @@ private DatabaseClient createMultiplexedSessionTransaction(boolean singleUse) {
587618

588619
private MultiplexedSessionTransaction createDirectMultiplexedSessionTransaction(
589620
boolean singleUse) {
590-
try {
591-
return new MultiplexedSessionTransaction(
592-
this,
593-
tracer.getCurrentSpan(),
594-
// Getting the result of the SettableApiFuture that contains the multiplexed session will
595-
// also automatically propagate any error that happened during the creation of the
596-
// session, such as for example a DatabaseNotFound exception. We therefore do not need
597-
// any special handling of such errors.
598-
multiplexedSessionReference.get().get(),
599-
singleUse ? getSingleUseChannelHint() : NO_CHANNEL_HINT,
600-
singleUse,
601-
this.pool);
602-
} catch (ExecutionException executionException) {
603-
throw SpannerExceptionFactory.asSpannerException(executionException.getCause());
604-
} catch (InterruptedException interruptedException) {
605-
throw SpannerExceptionFactory.propagateInterrupt(interruptedException);
606-
}
621+
return new MultiplexedSessionTransaction(
622+
this,
623+
tracer.getCurrentSpan(),
624+
// Getting the result of the SettableApiFuture that contains the multiplexed session will
625+
// also automatically propagate any error that happened during the creation of the
626+
// session, such as for example a DatabaseNotFound exception. We therefore do not need
627+
// any special handling of such errors.
628+
getCurrentSessionReference(),
629+
singleUse ? getSingleUseChannelHint() : NO_CHANNEL_HINT,
630+
singleUse,
631+
this.pool);
607632
}
608633

609634
private DelayedMultiplexedSessionTransaction createDelayedMultiplexSessionTransaction() {
610635
return new DelayedMultiplexedSessionTransaction(
611-
this, tracer.getCurrentSpan(), multiplexedSessionReference.get(), this.pool);
636+
this, tracer.getCurrentSpan(), getCurrentSessionReferenceFuture(), this.pool);
612637
}
613638

614639
private int getSingleUseChannelHint() {

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

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@
5454
import java.time.Duration;
5555
import java.util.*;
5656
import java.util.concurrent.CountDownLatch;
57+
import java.util.concurrent.ExecutorService;
58+
import java.util.concurrent.Executors;
5759
import java.util.concurrent.TimeUnit;
5860
import java.util.concurrent.atomic.AtomicInteger;
5961
import java.util.concurrent.atomic.AtomicReference;
@@ -245,6 +247,141 @@ public void testUnimplementedErrorOnCreation_fallsBackToRegularSessions() {
245247
assertEquals(0L, client.multiplexedSessionDatabaseClient.getNumSessionsReleased().get());
246248
}
247249

250+
@Test
251+
public void testDeadlineExceededErrorWithOneRetry() {
252+
// Setting up two exceptions
253+
mockSpanner.setCreateSessionExecutionTime(
254+
SimulatedExecutionTime.ofExceptions(
255+
Arrays.asList(
256+
Status.DEADLINE_EXCEEDED
257+
.withDescription(
258+
"CallOptions deadline exceeded after 22.986872393s. "
259+
+ "Name resolution delay 6.911918521 seconds. [closed=[], "
260+
+ "open=[[connecting_and_lb_delay=32445014148ns, was_still_waiting]]]")
261+
.asRuntimeException(),
262+
Status.DEADLINE_EXCEEDED
263+
.withDescription(
264+
"CallOptions deadline exceeded after 22.986872393s. "
265+
+ "Name resolution delay 6.911918521 seconds. [closed=[], "
266+
+ "open=[[connecting_and_lb_delay=32445014148ns, was_still_waiting]]]")
267+
.asRuntimeException())));
268+
DatabaseClientImpl client =
269+
(DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d"));
270+
assertNotNull(client.multiplexedSessionDatabaseClient);
271+
272+
// initial fetch call fails with exception
273+
// this call will try to fetch it again which again throws an exception
274+
assertThrows(
275+
SpannerException.class,
276+
() -> {
277+
try (ResultSet resultSet = client.singleUse().executeQuery(STATEMENT)) {
278+
//noinspection StatementWithEmptyBody
279+
while (resultSet.next()) {
280+
// ignore
281+
}
282+
}
283+
});
284+
285+
// When third request comes it should succeed
286+
try (ResultSet resultSet = client.singleUse().executeQuery(STATEMENT)) {
287+
//noinspection StatementWithEmptyBody
288+
while (resultSet.next()) {
289+
// ignore
290+
}
291+
}
292+
293+
// Verify that we received one ExecuteSqlRequest, and that it used a multiplexed session.
294+
assertEquals(1, mockSpanner.countRequestsOfType(ExecuteSqlRequest.class));
295+
List<ExecuteSqlRequest> requests = mockSpanner.getRequestsOfType(ExecuteSqlRequest.class);
296+
297+
Session session = mockSpanner.getSession(requests.get(0).getSession());
298+
assertNotNull(session);
299+
assertTrue(session.getMultiplexed());
300+
301+
assertNotNull(client.multiplexedSessionDatabaseClient);
302+
assertEquals(1L, client.multiplexedSessionDatabaseClient.getNumSessionsAcquired().get());
303+
assertEquals(1L, client.multiplexedSessionDatabaseClient.getNumSessionsReleased().get());
304+
}
305+
306+
@Test
307+
public void testDeadlineExceededErrorWithOneRetryWithParallelRequests()
308+
throws InterruptedException {
309+
mockSpanner.setCreateSessionExecutionTime(
310+
SimulatedExecutionTime.ofMinimumAndRandomTimeAndExceptions(
311+
2000, 0,
312+
Arrays.asList(
313+
Status.DEADLINE_EXCEEDED
314+
.withDescription(
315+
"CallOptions deadline exceeded after 22.986872393s. "
316+
+ "Name resolution delay 6.911918521 seconds. [closed=[], "
317+
+ "open=[[connecting_and_lb_delay=32445014148ns, was_still_waiting]]]")
318+
.asRuntimeException(),
319+
Status.DEADLINE_EXCEEDED
320+
.withDescription(
321+
"CallOptions deadline exceeded after 22.986872393s. "
322+
+ "Name resolution delay 6.911918521 seconds. [closed=[], "
323+
+ "open=[[connecting_and_lb_delay=32445014148ns, was_still_waiting]]]")
324+
.asRuntimeException())));
325+
DatabaseClientImpl client =
326+
(DatabaseClientImpl) spanner.getDatabaseClient(DatabaseId.of("p", "i", "d"));
327+
assertNotNull(client.multiplexedSessionDatabaseClient);
328+
329+
330+
ExecutorService executor = Executors.newCachedThreadPool();
331+
332+
// First set of request should fail with an error
333+
CountDownLatch failureCountDownLatch = new CountDownLatch(3);
334+
for (int i = 0; i < 3; i++) {
335+
executor.submit(() -> {
336+
try {
337+
try (ResultSet resultSet = client.singleUse().executeQuery(STATEMENT)) {
338+
//noinspection StatementWithEmptyBody
339+
while (resultSet.next()) {
340+
// ignore
341+
}
342+
}
343+
} catch (SpannerException e) {
344+
failureCountDownLatch.countDown();
345+
}
346+
});
347+
}
348+
349+
assertTrue(failureCountDownLatch.await(2, TimeUnit.SECONDS));
350+
assertEquals(0, failureCountDownLatch.getCount());
351+
352+
// Second set of requests should pass
353+
CountDownLatch countDownLatch = new CountDownLatch(3);
354+
for (int i = 0; i < 3; i++) {
355+
executor.submit(() -> {
356+
try {
357+
try (ResultSet resultSet = client.singleUse().executeQuery(STATEMENT)) {
358+
//noinspection StatementWithEmptyBody
359+
while (resultSet.next()) {
360+
// ignore
361+
}
362+
}
363+
} catch (SpannerException e) {
364+
countDownLatch.countDown();
365+
}
366+
});
367+
}
368+
369+
assertFalse(countDownLatch.await(3, TimeUnit.SECONDS));
370+
assertEquals(3, countDownLatch.getCount());
371+
372+
// Verify that we received 3 ExecuteSqlRequest, and that it used a multiplexed session.
373+
assertEquals(3, mockSpanner.countRequestsOfType(ExecuteSqlRequest.class));
374+
List<ExecuteSqlRequest> requests = mockSpanner.getRequestsOfType(ExecuteSqlRequest.class);
375+
376+
Session session = mockSpanner.getSession(requests.get(0).getSession());
377+
assertNotNull(session);
378+
assertTrue(session.getMultiplexed());
379+
380+
assertNotNull(client.multiplexedSessionDatabaseClient);
381+
assertEquals(3L, client.multiplexedSessionDatabaseClient.getNumSessionsAcquired().get());
382+
assertEquals(3L, client.multiplexedSessionDatabaseClient.getNumSessionsReleased().get());
383+
}
384+
248385
@Test
249386
public void
250387
testUnimplementedErrorOnCreation_firstReceivesError_secondFallsBackToRegularSessions() {

0 commit comments

Comments
 (0)