Skip to content

Commit 4029de2

Browse files
authored
chore(spanner): wait until queries have started before closing ReadContext (#12727)
Prevents a ReadContext from being closed before all queries have actually started. This extra check is needed now that queries in read-only transactions are started using the background executor when the async API is used. Follow-up for #12715
1 parent ab29069 commit 4029de2

File tree

4 files changed

+144
-15
lines changed

4 files changed

+144
-15
lines changed

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

Lines changed: 92 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import com.google.cloud.spanner.spi.v1.SpannerRpc.Option;
4040
import com.google.common.annotations.VisibleForTesting;
4141
import com.google.common.base.Preconditions;
42+
import com.google.common.base.Supplier;
4243
import com.google.common.collect.ImmutableMap;
4344
import com.google.common.util.concurrent.MoreExecutors;
4445
import com.google.protobuf.ByteString;
@@ -59,7 +60,11 @@
5960
import java.util.EnumMap;
6061
import java.util.Map;
6162
import java.util.concurrent.ThreadLocalRandom;
63+
import java.util.concurrent.atomic.AtomicBoolean;
64+
import java.util.concurrent.atomic.AtomicInteger;
6265
import java.util.concurrent.atomic.AtomicLong;
66+
import java.util.concurrent.locks.Condition;
67+
import java.util.concurrent.locks.ReentrantLock;
6368
import java.util.logging.Logger;
6469
import javax.annotation.Nullable;
6570
import javax.annotation.concurrent.GuardedBy;
@@ -345,14 +350,17 @@ static Builder newBuilder() {
345350
}
346351

347352
private TimestampBound bound;
348-
private final Object txnLock = new Object();
353+
private final ReentrantLock txnLock = new ReentrantLock();
354+
private final Condition hasNoPendingStarts = txnLock.newCondition();
349355

350356
@GuardedBy("txnLock")
351357
private Timestamp timestamp;
352358

353359
@GuardedBy("txnLock")
354360
private ByteString transactionId;
355361

362+
private final AtomicInteger pendingStarts = new AtomicInteger(0);
363+
356364
private final Map<SpannerRpc.Option, ?> channelHint;
357365

358366
MultiUseReadOnlyTransaction(Builder builder) {
@@ -407,6 +415,56 @@ TransactionSelector getTransactionSelector() {
407415
return selector;
408416
}
409417

418+
private void decrementPendingStartsAndSignal() {
419+
if (pendingStarts.decrementAndGet() == 0) {
420+
txnLock.lock();
421+
try {
422+
hasNoPendingStarts.signalAll();
423+
} finally {
424+
txnLock.unlock();
425+
}
426+
}
427+
}
428+
429+
private ListenableAsyncResultSet createAsyncResultSet(
430+
Supplier<ResultSet> resultSetSupplier, int bufferRows) {
431+
pendingStarts.incrementAndGet();
432+
// Make sure that we decrement the counter exactly once, either
433+
// when the query is actually executed, or when the result set is closed,
434+
// or if something goes wrong when creating the result set.
435+
final AtomicBoolean decremented = new AtomicBoolean(false);
436+
try {
437+
return new AsyncResultSetImpl(
438+
executorProvider,
439+
() -> {
440+
try {
441+
return resultSetSupplier.get();
442+
} finally {
443+
if (decremented.compareAndSet(false, true)) {
444+
decrementPendingStartsAndSignal();
445+
}
446+
}
447+
},
448+
bufferRows) {
449+
@Override
450+
public void close() {
451+
try {
452+
super.close();
453+
} finally {
454+
if (!isUsed() && decremented.compareAndSet(false, true)) {
455+
decrementPendingStartsAndSignal();
456+
}
457+
}
458+
}
459+
};
460+
} catch (Throwable t) {
461+
if (decremented.compareAndSet(false, true)) {
462+
decrementPendingStartsAndSignal();
463+
}
464+
throw t;
465+
}
466+
}
467+
410468
@Override
411469
public ListenableAsyncResultSet readAsync(
412470
String table, KeySet keys, Iterable<String> columns, ReadOption... options) {
@@ -415,8 +473,8 @@ public ListenableAsyncResultSet readAsync(
415473
readOptions.hasBufferRows()
416474
? readOptions.bufferRows()
417475
: AsyncResultSetImpl.DEFAULT_BUFFER_SIZE;
418-
return new AsyncResultSetImpl(
419-
executorProvider, () -> readInternal(table, null, keys, columns, options), bufferRows);
476+
return createAsyncResultSet(
477+
() -> readInternal(table, null, keys, columns, options), bufferRows);
420478
}
421479

422480
@Override
@@ -427,10 +485,8 @@ public ListenableAsyncResultSet readUsingIndexAsync(
427485
readOptions.hasBufferRows()
428486
? readOptions.bufferRows()
429487
: AsyncResultSetImpl.DEFAULT_BUFFER_SIZE;
430-
return new AsyncResultSetImpl(
431-
executorProvider,
432-
() -> readInternal(table, checkNotNull(index), keys, columns, options),
433-
bufferRows);
488+
return createAsyncResultSet(
489+
() -> readInternal(table, checkNotNull(index), keys, columns, options), bufferRows);
434490
}
435491

436492
@Override
@@ -440,8 +496,7 @@ public ListenableAsyncResultSet executeQueryAsync(Statement statement, QueryOpti
440496
readOptions.hasBufferRows()
441497
? readOptions.bufferRows()
442498
: AsyncResultSetImpl.DEFAULT_BUFFER_SIZE;
443-
return new AsyncResultSetImpl(
444-
executorProvider,
499+
return createAsyncResultSet(
445500
() ->
446501
executeQueryInternal(
447502
statement, com.google.spanner.v1.ExecuteSqlRequest.QueryMode.NORMAL, options),
@@ -450,20 +505,38 @@ public ListenableAsyncResultSet executeQueryAsync(Statement statement, QueryOpti
450505

451506
@Override
452507
public Timestamp getReadTimestamp() {
453-
synchronized (txnLock) {
508+
txnLock.lock();
509+
try {
454510
assertTimestampAvailable(timestamp != null);
455511
return timestamp;
512+
} finally {
513+
txnLock.unlock();
456514
}
457515
}
458516

459517
ByteString getTransactionId() {
460-
synchronized (txnLock) {
518+
txnLock.lock();
519+
try {
461520
return transactionId;
521+
} finally {
522+
txnLock.unlock();
462523
}
463524
}
464525

465526
@Override
466527
public void close() {
528+
txnLock.lock();
529+
try {
530+
while (pendingStarts.get() > 0) {
531+
try {
532+
hasNoPendingStarts.await();
533+
} catch (InterruptedException e) {
534+
throw SpannerExceptionFactory.propagateInterrupt(e);
535+
}
536+
}
537+
} finally {
538+
txnLock.unlock();
539+
}
467540
ByteString id = getTransactionId();
468541
if (id != null && !id.isEmpty()) {
469542
rpc.clearTransactionAndChannelAffinity(id, Option.CHANNEL_HINT.getLong(channelHint));
@@ -477,7 +550,8 @@ public void close() {
477550
* Multiplexed Session.
478551
*/
479552
void initFallbackTransaction() {
480-
synchronized (txnLock) {
553+
txnLock.lock();
554+
try {
481555
span.addAnnotation("Creating Transaction");
482556
TransactionOptions.Builder options = TransactionOptions.newBuilder();
483557
if (timestamp != null) {
@@ -494,6 +568,8 @@ void initFallbackTransaction() {
494568
.setOptions(options)
495569
.build();
496570
initTransactionInternal(request);
571+
} finally {
572+
txnLock.unlock();
497573
}
498574
}
499575

@@ -507,7 +583,8 @@ void initTransaction() {
507583
// RTT, but optimal if the first read is slow. As the client library is now using streaming
508584
// reads, a possible optimization could be to use the first read in the transaction to begin
509585
// it implicitly.
510-
synchronized (txnLock) {
586+
txnLock.lock();
587+
try {
511588
if (transactionId != null) {
512589
return;
513590
}
@@ -520,6 +597,8 @@ void initTransaction() {
520597
.setOptions(options)
521598
.build();
522599
initTransactionInternal(request);
600+
} finally {
601+
txnLock.unlock();
523602
}
524603
}
525604

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,12 @@ private AsyncResultSetImpl(
176176
this.buffer = new LinkedBlockingDeque<>(bufferSize);
177177
}
178178

179+
boolean isUsed() {
180+
synchronized (monitor) {
181+
return state != State.INITIALIZED;
182+
}
183+
}
184+
179185
/**
180186
* Closes the {@link AsyncResultSet}. {@link #close()} is non-blocking and may be called multiple
181187
* times without side effects. An {@link AsyncResultSet} may be closed before all rows have been

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@
4747
/** Base class for {@link AsyncRunnerTest} and {@link AsyncTransactionManagerTest}. */
4848
public abstract class AbstractAsyncTransactionTest {
4949
static MockSpannerServiceImpl mockSpanner;
50-
private static Server server;
51-
private static InetSocketAddress address;
50+
static Server server;
51+
static InetSocketAddress address;
5252
static ExecutorService executor;
5353

5454
Spanner spanner;

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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,20 @@
1717
package com.google.cloud.spanner;
1818

1919
import static com.google.cloud.spanner.MockSpannerTestUtil.READ_ONE_KEY_VALUE_STATEMENT;
20+
import static com.google.cloud.spanner.MockSpannerTestUtil.TEST_DATABASE;
21+
import static com.google.cloud.spanner.MockSpannerTestUtil.TEST_INSTANCE;
22+
import static com.google.cloud.spanner.MockSpannerTestUtil.TEST_PROJECT;
2023
import static com.google.common.truth.Truth.assertThat;
24+
import static org.junit.Assert.assertEquals;
25+
import static org.junit.Assert.assertThrows;
2126
import static org.junit.Assert.assertTrue;
27+
import static org.mockito.Mockito.mock;
28+
import static org.mockito.Mockito.when;
2229

30+
import com.google.cloud.NoCredentials;
2331
import com.google.spanner.v1.BeginTransactionRequest;
2432
import com.google.spanner.v1.ExecuteSqlRequest;
33+
import io.grpc.ManagedChannelBuilder;
2534
import java.util.concurrent.CountDownLatch;
2635
import java.util.concurrent.TimeUnit;
2736
import org.junit.Test;
@@ -148,4 +157,39 @@ public void testMultipleQueriesOnlyCallsBeginTransactionOnce() throws Exception
148157
BeginTransactionRequest.class, ExecuteSqlRequest.class, ExecuteSqlRequest.class);
149158
}
150159
}
160+
161+
@Test(timeout = 5000)
162+
public void createAsyncResultSet_handlesExceptionCorrectly() throws Exception {
163+
SpannerOptions.CloseableExecutorProvider mockExecutorProvider =
164+
mock(SpannerOptions.CloseableExecutorProvider.class);
165+
when(mockExecutorProvider.getExecutor())
166+
.thenThrow(new RuntimeException("Failed to get executor"));
167+
168+
String endpoint = address.getHostString() + ":" + server.getPort();
169+
SpannerOptions options =
170+
SpannerOptions.newBuilder()
171+
.setProjectId(TEST_PROJECT)
172+
.setChannelConfigurator(ManagedChannelBuilder::usePlaintext)
173+
.setHost("http://" + endpoint)
174+
.setCredentials(NoCredentials.getInstance())
175+
.setAsyncExecutorProvider(mockExecutorProvider)
176+
.setSessionPoolOption(
177+
SessionPoolOptions.newBuilder()
178+
.setFailOnSessionLeak()
179+
.setWaitForMinSessions(org.threeten.bp.Duration.ofSeconds(2))
180+
.build())
181+
.build();
182+
183+
try (Spanner testSpanner = options.getService()) {
184+
DatabaseClient client =
185+
testSpanner.getDatabaseClient(DatabaseId.of(TEST_PROJECT, TEST_INSTANCE, TEST_DATABASE));
186+
try (ReadOnlyTransaction transaction = client.readOnlyTransaction()) {
187+
RuntimeException e =
188+
assertThrows(
189+
RuntimeException.class,
190+
() -> transaction.executeQueryAsync(READ_ONE_KEY_VALUE_STATEMENT));
191+
assertEquals("Failed to get executor", e.getMessage());
192+
}
193+
}
194+
}
151195
}

0 commit comments

Comments
 (0)