From 0b5566bdc812e10bd6a44db7402281a99035f299 Mon Sep 17 00:00:00 2001 From: Sakthivel Subramanian Date: Fri, 23 Oct 2026 15:17:04 +0000 Subject: [PATCH 1/5] fix(spanner): do not cache retryable errors in AbstractLazyInitializer When AbstractLazyInitializer.get() is called and initialize() fails with an exception, the failure was previously cached permanently regardless of whether the underlying error was transient or retryable. In MultiplexedSessionDatabaseClient and DatabaseClientImpl, this caused calls to getDialect() or getStatementFactory() to permanently cache transient startup or query errors (such as DEADLINE_EXCEEDED, RESOURCE_EXHAUSTED, or UNAVAILABLE) and continually rethrow the cached exception even after session creation succeeded on subsequent retries. This change modifies AbstractLazyInitializer to selectively cache only resource-not-found exceptions (DatabaseNotFoundException, InstanceNotFoundException, SessionNotFoundException) and non-retryable errors. Retryable SpannerExceptions (with error codes matching MultiplexedSessionDatabaseClient.RETRYABLE_ERROR_CODES: DEADLINE_EXCEEDED, RESOURCE_EXHAUSTED, UNAVAILABLE) are rethrown without setting initialized = true, allowing subsequent get() calls to retry initialization. In addition: - Relaxed visibility of MultiplexedSessionDatabaseClient.RETRYABLE_ERROR_CODES from private to package-private to share the retryable error classification. - In DatabaseClientImpl.statementFactorySupplier, unwrapped ExecutionException to surface the underlying cause to SpannerExceptionFactory.asSpannerException, and converted TimeoutException via SpannerExceptionFactory.propagateTimeout to produce DEADLINE_EXCEEDED. - Added comprehensive unit tests in LazySpannerInitializerTest and DatabaseClientImplTest verifying retry behavior on transient errors and concurrent retry handling. --- .../spanner/AbstractLazyInitializer.java | 23 +++- .../cloud/spanner/DatabaseClientImpl.java | 6 +- .../MultiplexedSessionDatabaseClient.java | 2 +- .../cloud/spanner/DatabaseClientImplTest.java | 118 ++++++++++++++++++ .../spanner/LazySpannerInitializerTest.java | 80 ++++++++++++ 5 files changed, 224 insertions(+), 5 deletions(-) diff --git a/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractLazyInitializer.java b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractLazyInitializer.java index 734368526020..00883c1c3f77 100644 --- a/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractLazyInitializer.java +++ b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractLazyInitializer.java @@ -36,10 +36,16 @@ public T get() throws Exception { if (!initialized) { try { object = initialize(); + error = null; + initialized = true; } catch (Exception e) { - error = e; + if (shouldCacheError(e)) { + error = e; + initialized = true; + } else { + throw e; + } } - initialized = true; } } } @@ -49,6 +55,19 @@ public T get() throws Exception { return object; } + private static boolean shouldCacheError(Exception e) { + if (e instanceof DatabaseNotFoundException + || e instanceof InstanceNotFoundException + || e instanceof SessionNotFoundException) { + return true; + } + if (e instanceof SpannerException) { + return !MultiplexedSessionDatabaseClient.RETRYABLE_ERROR_CODES.contains( + ((SpannerException) e).getErrorCode()); + } + return true; + } + /** * Initializes the actual object that should be returned. Is called once the first time an * instance of T is required. diff --git a/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java index ecd82500a9c4..1679ebddc899 100644 --- a/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java +++ b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java @@ -91,8 +91,10 @@ protected StatementFactory initialize() { try { Dialect dialect = getDialectAsync().get(30, TimeUnit.SECONDS); return new StatementFactory(dialect); - } catch (ExecutionException | TimeoutException e) { - throw SpannerExceptionFactory.asSpannerException(e); + } catch (ExecutionException e) { + throw SpannerExceptionFactory.asSpannerException(e.getCause()); + } catch (TimeoutException e) { + throw SpannerExceptionFactory.propagateTimeout(e); } catch (InterruptedException e) { throw SpannerExceptionFactory.propagateInterrupt(e); } diff --git a/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClient.java b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClient.java index afe498219139..49364615cd23 100644 --- a/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClient.java +++ b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClient.java @@ -179,7 +179,7 @@ private SharedChannelUsage(int numChannels) { private static final Map CHANNEL_USAGE = new HashMap<>(); - private static final EnumSet RETRYABLE_ERROR_CODES = + static final EnumSet RETRYABLE_ERROR_CODES = EnumSet.of(ErrorCode.DEADLINE_EXCEEDED, ErrorCode.RESOURCE_EXHAUSTED, ErrorCode.UNAVAILABLE); private final BitSet channelUsage; diff --git a/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java b/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java index 8acb0a7b725b..02a3b4b80172 100644 --- a/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java +++ b/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/DatabaseClientImplTest.java @@ -29,6 +29,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; @@ -115,6 +116,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; import java.util.logging.Level; import java.util.logging.Logger; @@ -3145,6 +3147,122 @@ public void testGetDialectDefaultPreloaded_FailsDirectlyIfDatabaseNotFound() { } } + @Test + public void testGetDialect_RecoversAfterTransientCreateSessionFailure() throws Exception { + mockSpanner.putStatementResult(StatementResult.detectDialectResult(Dialect.POSTGRESQL)); + mockSpanner.setCreateSessionExecutionTime( + SimulatedExecutionTime.ofException( + Status.DEADLINE_EXCEEDED + .withDescription("Simulated transient outage on createSession call #1") + .asRuntimeException())); + mockSpanner.freeze(); + try { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); + AtomicReference firstCallError = new AtomicReference<>(); + Thread firstCaller = + new Thread( + () -> { + try { + client.getDialect(); + } catch (Throwable t) { + firstCallError.set(t); + } + }); + firstCaller.start(); + // Wait (up to 10s) until firstCaller is blocked waiting on the initial multiplexed session. + Stopwatch watch = Stopwatch.createStarted(); + while (firstCaller.getState() != Thread.State.WAITING + && firstCaller.getState() != Thread.State.TIMED_WAITING + && firstCaller.getState() != Thread.State.TERMINATED + && watch.elapsed(TimeUnit.SECONDS) < 10) { + Thread.yield(); + } + mockSpanner.unfreeze(); + firstCaller.join(10_000L); + assertFalse(firstCaller.isAlive()); + + assertThat(firstCallError.get()).isInstanceOf(SpannerException.class); + SpannerException firstException = (SpannerException) firstCallError.get(); + assertEquals(ErrorCode.DEADLINE_EXCEEDED, firstException.getErrorCode()); + + // Wait for the retry CreateSession request to reach the server so multiplexedSessionReference + // has been updated to the new session future. + mockSpanner.waitForRequestsToContain( + message -> message instanceof CreateSessionRequest, 10_000L); + try (ResultSet rs = client.singleUse().executeQuery(SELECT1)) { + assertTrue(rs.next()); + } + + assertEquals(Dialect.POSTGRESQL, client.getDialect()); + assertEquals(Dialect.POSTGRESQL, client.getDialect()); + assertEquals( + Statement.newBuilder("select id from test where b=$1").bind("p1").to(true).build(), + client + .getStatementFactory() + .withUnnamedParameters("select id from test where b=?", true)); + } finally { + mockSpanner.unfreeze(); + mockSpanner.putStatementResult( + StatementResult.detectDialectResult(Dialect.GOOGLE_STANDARD_SQL)); + } + } + + @Test + public void testGetDialectAndStatementFactory_RecoverAfterTransientQueryFailure() { + mockSpanner.putStatementResult( + StatementResult.exception( + MultiplexedSessionDatabaseClient.DETERMINE_DIALECT_STATEMENT, + Status.DEADLINE_EXCEEDED + .withDescription("Transient dialect query timeout") + .asRuntimeException())); + try { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE)); + + SpannerException dialectException = assertThrows(SpannerException.class, client::getDialect); + assertEquals(ErrorCode.DEADLINE_EXCEEDED, dialectException.getErrorCode()); + + SpannerException factoryException = + assertThrows(SpannerException.class, client::getStatementFactory); + assertEquals(ErrorCode.DEADLINE_EXCEEDED, factoryException.getErrorCode()); + + mockSpanner.putStatementResult(StatementResult.detectDialectResult(Dialect.POSTGRESQL)); + + assertEquals(Dialect.POSTGRESQL, client.getDialect()); + assertEquals( + Statement.newBuilder("select id from test where b=$1").bind("p1").to(true).build(), + client + .getStatementFactory() + .withUnnamedParameters("select id from test where b=?", true)); + } finally { + mockSpanner.putStatementResult( + StatementResult.detectDialectResult(Dialect.GOOGLE_STANDARD_SQL)); + } + } + + @Test + public void testGetDialect_CachesNonRetryableQueryFailure() { + mockSpanner.putStatementResult( + StatementResult.exception( + MultiplexedSessionDatabaseClient.DETERMINE_DIALECT_STATEMENT, + Status.PERMISSION_DENIED + .withDescription("Permission denied on information_schema") + .asRuntimeException())); + try { + DatabaseClient client = + spanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, "non-retryable-db")); + + SpannerException e1 = assertThrows(SpannerException.class, client::getDialect); + assertEquals(ErrorCode.PERMISSION_DENIED, e1.getErrorCode()); + SpannerException e2 = assertThrows(SpannerException.class, client::getDialect); + assertSame(e1, e2); + } finally { + mockSpanner.putStatementResult( + StatementResult.detectDialectResult(Dialect.GOOGLE_STANDARD_SQL)); + } + } + @Test public void testUntypedNullParameters() { Statement statement = diff --git a/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/LazySpannerInitializerTest.java b/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/LazySpannerInitializerTest.java index aa4879875e32..a6e2af02ce29 100644 --- a/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/LazySpannerInitializerTest.java +++ b/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/LazySpannerInitializerTest.java @@ -100,4 +100,84 @@ public Spanner initialize() { assertThat(count.get()).isEqualTo(1); executor.shutdown(); } + + @Test + public void testGet_shouldRetryOnRetryableSpannerException() throws Exception { + final AtomicInteger attempts = new AtomicInteger(); + final Spanner expectedSpanner = mock(Spanner.class); + final LazySpannerInitializer initializer = + new LazySpannerInitializer() { + @Override + public Spanner initialize() { + int attempt = attempts.incrementAndGet(); + if (attempt == 1) { + throw SpannerExceptionFactory.newSpannerException( + ErrorCode.UNAVAILABLE, "Transient unavailable"); + } + if (attempt == 2) { + throw SpannerExceptionFactory.newSpannerException( + ErrorCode.DEADLINE_EXCEEDED, "Transient deadline exceeded"); + } + return expectedSpanner; + } + }; + SpannerException e1 = assertThrows(SpannerException.class, initializer::get); + assertThat(e1.getErrorCode()).isEqualTo(ErrorCode.UNAVAILABLE); + SpannerException e2 = assertThrows(SpannerException.class, initializer::get); + assertThat(e2.getErrorCode()).isEqualTo(ErrorCode.DEADLINE_EXCEEDED); + + Spanner s1 = initializer.get(); + Spanner s2 = initializer.get(); + assertThat(s1).isSameInstanceAs(expectedSpanner); + assertThat(s2).isSameInstanceAs(expectedSpanner); + assertThat(attempts.get()).isEqualTo(3); + } + + @Test + public void testGet_concurrentRetryAfterRetryableFailure() throws InterruptedException { + final AtomicInteger count = new AtomicInteger(); + final Spanner expectedSpanner = mock(Spanner.class); + final LazySpannerInitializer initializer = + new LazySpannerInitializer() { + @Override + public Spanner initialize() { + if (count.incrementAndGet() == 1) { + throw SpannerExceptionFactory.newSpannerException( + ErrorCode.UNAVAILABLE, "Transient error"); + } + return expectedSpanner; + } + }; + final int threads = 16; + final CountDownLatch latch = new CountDownLatch(threads); + ListeningExecutorService executor = + MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(threads)); + List> futures = new ArrayList<>(threads); + for (int i = 0; i < threads; i++) { + futures.add( + executor.submit( + () -> { + latch.countDown(); + latch.await(10L, TimeUnit.SECONDS); + return initializer.get(); + })); + } + int failureCount = 0; + int successCount = 0; + for (ListenableFuture future : futures) { + try { + assertThat(future.get()).isSameInstanceAs(expectedSpanner); + successCount++; + } catch (ExecutionException e) { + assertThat(e.getCause()).isInstanceOf(SpannerException.class); + assertThat(((SpannerException) e.getCause()).getErrorCode()) + .isEqualTo(ErrorCode.UNAVAILABLE); + failureCount++; + } + } + assertThat(failureCount).isEqualTo(1); + assertThat(successCount).isEqualTo(threads - 1); + assertThat(count.get()).isEqualTo(2); + executor.shutdown(); + } } From be7b32d1349874f1e2c607c86a741798a7e3d82a Mon Sep 17 00:00:00 2001 From: Sakthivel Subramanian Date: Fri, 23 Oct 2026 15:47:31 +0000 Subject: [PATCH 2/5] Addressed comments --- .../com/google/cloud/spanner/AbstractLazyInitializer.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractLazyInitializer.java b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractLazyInitializer.java index 00883c1c3f77..43d85462b64f 100644 --- a/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractLazyInitializer.java +++ b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractLazyInitializer.java @@ -56,11 +56,6 @@ public T get() throws Exception { } private static boolean shouldCacheError(Exception e) { - if (e instanceof DatabaseNotFoundException - || e instanceof InstanceNotFoundException - || e instanceof SessionNotFoundException) { - return true; - } if (e instanceof SpannerException) { return !MultiplexedSessionDatabaseClient.RETRYABLE_ERROR_CODES.contains( ((SpannerException) e).getErrorCode()); From 39c34ba549c7518234beb3abfea5ca9d8371a393 Mon Sep 17 00:00:00 2001 From: Sakthivel Subramanian Date: Tue, 27 Oct 2026 07:06:47 +0000 Subject: [PATCH 3/5] Addressed comments --- .../spanner/AbstractLazyInitializer.java | 21 +++++++++++++------ .../cloud/spanner/DatabaseClientImpl.java | 5 +++++ .../MultiplexedSessionDatabaseClient.java | 12 +++++++++++ .../spanner/LazySpannerInitializerTest.java | 10 +++++++++ 4 files changed, 42 insertions(+), 6 deletions(-) diff --git a/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractLazyInitializer.java b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractLazyInitializer.java index 43d85462b64f..5e8d43423302 100644 --- a/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractLazyInitializer.java +++ b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractLazyInitializer.java @@ -18,7 +18,8 @@ /** * Generic {@link AbstractLazyInitializer} for any heavy-weight object that might throw an exception - * during initialization. The underlying object is initialized at most once. + * during initialization. The underlying object is initialized at most once unless an exception is + * thrown and {@link #shouldCacheError(Exception)} returns false. * * @param Object which is to be initialized lazily */ @@ -55,11 +56,19 @@ public T get() throws Exception { return object; } - private static boolean shouldCacheError(Exception e) { - if (e instanceof SpannerException) { - return !MultiplexedSessionDatabaseClient.RETRYABLE_ERROR_CODES.contains( - ((SpannerException) e).getErrorCode()); - } + /** + * Determines whether an exception thrown by {@link #initialize()} should be cached. If cached, + * subsequent calls to {@link #get()} will immediately rethrow the cached exception. If not + * cached, the exception is rethrown without marking this initializer as initialized, allowing + * subsequent calls to retry {@link #initialize()}. + * + *

Defaults to {@code true} for backward compatibility. Subclasses can override this method to + * avoid caching transient or retryable errors. + * + * @param e the exception thrown during initialization + * @return {@code true} if the exception should be cached, {@code false} otherwise + */ + protected boolean shouldCacheError(Exception e) { return true; } diff --git a/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java index 1679ebddc899..9d29e641152c 100644 --- a/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java +++ b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java @@ -99,6 +99,11 @@ protected StatementFactory initialize() { throw SpannerExceptionFactory.propagateInterrupt(e); } } + + @Override + protected boolean shouldCacheError(Exception e) { + return MultiplexedSessionDatabaseClient.shouldCacheDialectError(e); + } }; @Override diff --git a/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClient.java b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClient.java index 49364615cd23..00bfa9deb64e 100644 --- a/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClient.java +++ b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClient.java @@ -182,6 +182,13 @@ private SharedChannelUsage(int numChannels) { static final EnumSet RETRYABLE_ERROR_CODES = EnumSet.of(ErrorCode.DEADLINE_EXCEEDED, ErrorCode.RESOURCE_EXHAUSTED, ErrorCode.UNAVAILABLE); + static boolean shouldCacheDialectError(Exception e) { + if (e instanceof SpannerException) { + return !RETRYABLE_ERROR_CODES.contains(((SpannerException) e).getErrorCode()); + } + return true; + } + private final BitSet channelUsage; private final int numChannels; @@ -539,6 +546,11 @@ protected Dialect initialize() { // This should not really happen, but it is the safest fallback value. return Dialect.GOOGLE_STANDARD_SQL; } + + @Override + protected boolean shouldCacheError(Exception e) { + return shouldCacheDialectError(e); + } }; @Override diff --git a/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/LazySpannerInitializerTest.java b/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/LazySpannerInitializerTest.java index a6e2af02ce29..c7bb78d537fa 100644 --- a/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/LazySpannerInitializerTest.java +++ b/java-spanner/google-cloud-spanner/src/test/java/com/google/cloud/spanner/LazySpannerInitializerTest.java @@ -120,6 +120,11 @@ public Spanner initialize() { } return expectedSpanner; } + + @Override + protected boolean shouldCacheError(Exception e) { + return MultiplexedSessionDatabaseClient.shouldCacheDialectError(e); + } }; SpannerException e1 = assertThrows(SpannerException.class, initializer::get); assertThat(e1.getErrorCode()).isEqualTo(ErrorCode.UNAVAILABLE); @@ -147,6 +152,11 @@ public Spanner initialize() { } return expectedSpanner; } + + @Override + protected boolean shouldCacheError(Exception e) { + return MultiplexedSessionDatabaseClient.shouldCacheDialectError(e); + } }; final int threads = 16; final CountDownLatch latch = new CountDownLatch(threads); From 28c82b6cb07ab44d49b4f2df2dc4000e43012c9c Mon Sep 17 00:00:00 2001 From: Sakthivel Subramanian Date: Tue, 27 Oct 2026 07:34:22 +0000 Subject: [PATCH 4/5] Addressed comments --- .../google/cloud/spanner/MultiplexedSessionDatabaseClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClient.java b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClient.java index 00bfa9deb64e..03558aded997 100644 --- a/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClient.java +++ b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/MultiplexedSessionDatabaseClient.java @@ -179,7 +179,7 @@ private SharedChannelUsage(int numChannels) { private static final Map CHANNEL_USAGE = new HashMap<>(); - static final EnumSet RETRYABLE_ERROR_CODES = + private static final EnumSet RETRYABLE_ERROR_CODES = EnumSet.of(ErrorCode.DEADLINE_EXCEEDED, ErrorCode.RESOURCE_EXHAUSTED, ErrorCode.UNAVAILABLE); static boolean shouldCacheDialectError(Exception e) { From f82e2a9676f366d760785be2c0b8f900bbfaadaa Mon Sep 17 00:00:00 2001 From: Sakthivel Subramanian Date: Tue, 27 Oct 2026 09:38:55 +0000 Subject: [PATCH 5/5] Addressed comments --- .../spanner/AbstractLazyInitializer.java | 4 ++-- .../cloud/spanner/DatabaseClientImpl.java | 24 ++++--------------- 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractLazyInitializer.java b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractLazyInitializer.java index 5e8d43423302..710c2d0b4130 100644 --- a/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractLazyInitializer.java +++ b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/AbstractLazyInitializer.java @@ -62,8 +62,8 @@ public T get() throws Exception { * cached, the exception is rethrown without marking this initializer as initialized, allowing * subsequent calls to retry {@link #initialize()}. * - *

Defaults to {@code true} for backward compatibility. Subclasses can override this method to - * avoid caching transient or retryable errors. + *

Defaults to {@code true}. Subclasses can override this method to avoid caching transient or + * retryable errors. * * @param e the exception thrown during initialization * @return {@code true} if the exception should be cached, {@code false} otherwise diff --git a/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java index 9d29e641152c..66eea4a882ef 100644 --- a/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java +++ b/java-spanner/google-cloud-spanner/src/main/java/com/google/cloud/spanner/DatabaseClientImpl.java @@ -30,10 +30,6 @@ import io.opentelemetry.api.common.Attributes; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import javax.annotation.Nullable; @@ -88,21 +84,15 @@ public Dialect getDialect() { new AbstractLazyInitializer() { @Override protected StatementFactory initialize() { - try { - Dialect dialect = getDialectAsync().get(30, TimeUnit.SECONDS); - return new StatementFactory(dialect); - } catch (ExecutionException e) { - throw SpannerExceptionFactory.asSpannerException(e.getCause()); - } catch (TimeoutException e) { - throw SpannerExceptionFactory.propagateTimeout(e); - } catch (InterruptedException e) { - throw SpannerExceptionFactory.propagateInterrupt(e); - } + return new StatementFactory(getDialect()); } @Override protected boolean shouldCacheError(Exception e) { - return MultiplexedSessionDatabaseClient.shouldCacheDialectError(e); + // getDialect() already decides whether to cache or retry dialect errors. + // Never cache errors here so that statementFactorySupplier always delegates + // the retry/caching lifecycle entirely to getDialect(). + return false; } }; @@ -305,10 +295,6 @@ public long executePartitionedUpdate(final Statement stmt, final UpdateOption... return multiplexedSessionDatabaseClient.executePartitionedUpdate(stmt, options); } - private Future getDialectAsync() { - return multiplexedSessionDatabaseClient.getDialectAsync(); - } - boolean isValid() { return multiplexedSessionDatabaseClient.isValid(); }