Skip to content

Commit 7c82f1c

Browse files
fix(spanner): Avoid blocking thread in AsyncResultSet (#3446)
* fix: Avoid blocking thread in AsyncResultSet * Addressed comments * Addressed comments * Addressed comments * Addressed comments * Addressed comments
1 parent fa18894 commit 7c82f1c

13 files changed

+333
-67
lines changed

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

+14-4
Original file line numberDiff line numberDiff line change
@@ -768,9 +768,14 @@ ResultSet executeQueryInternalWithOptions(
768768
rpc.getExecuteQueryRetrySettings(),
769769
rpc.getExecuteQueryRetryableCodes()) {
770770
@Override
771-
CloseableIterator<PartialResultSet> startStream(@Nullable ByteString resumeToken) {
771+
CloseableIterator<PartialResultSet> startStream(
772+
@Nullable ByteString resumeToken,
773+
AsyncResultSet.StreamMessageListener streamListener) {
772774
GrpcStreamIterator stream =
773775
new GrpcStreamIterator(statement, prefetchChunks, cancelQueryWhenClientIsClosed);
776+
if (streamListener != null) {
777+
stream.registerListener(streamListener);
778+
}
774779
if (partitionToken != null) {
775780
request.setPartitionToken(partitionToken);
776781
}
@@ -791,8 +796,8 @@ CloseableIterator<PartialResultSet> startStream(@Nullable ByteString resumeToken
791796
getTransactionChannelHint(),
792797
isRouteToLeader());
793798
session.markUsed(clock.instant());
794-
call.request(prefetchChunks);
795799
stream.setCall(call, request.getTransaction().hasBegin());
800+
call.request(prefetchChunks);
796801
return stream;
797802
}
798803

@@ -959,9 +964,14 @@ ResultSet readInternalWithOptions(
959964
rpc.getReadRetrySettings(),
960965
rpc.getReadRetryableCodes()) {
961966
@Override
962-
CloseableIterator<PartialResultSet> startStream(@Nullable ByteString resumeToken) {
967+
CloseableIterator<PartialResultSet> startStream(
968+
@Nullable ByteString resumeToken,
969+
AsyncResultSet.StreamMessageListener streamListener) {
963970
GrpcStreamIterator stream =
964971
new GrpcStreamIterator(prefetchChunks, cancelQueryWhenClientIsClosed);
972+
if (streamListener != null) {
973+
stream.registerListener(streamListener);
974+
}
965975
TransactionSelector selector = null;
966976
if (resumeToken != null) {
967977
builder.setResumeToken(resumeToken);
@@ -980,8 +990,8 @@ CloseableIterator<PartialResultSet> startStream(@Nullable ByteString resumeToken
980990
getTransactionChannelHint(),
981991
isRouteToLeader());
982992
session.markUsed(clock.instant());
983-
call.request(prefetchChunks);
984993
stream.setCall(call, /* withBeginTransaction = */ builder.getTransaction().hasBegin());
994+
call.request(prefetchChunks);
985995
return stream;
986996
}
987997

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

+8
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,14 @@ interface CloseableIterator<T> extends Iterator<T> {
150150
void close(@Nullable String message);
151151

152152
boolean isWithBeginTransaction();
153+
154+
/**
155+
* @param streamMessageListener A class object which implements StreamMessageListener
156+
* @return true if streaming is supported by the iterator, otherwise false
157+
*/
158+
default boolean initiateStreaming(AsyncResultSet.StreamMessageListener streamMessageListener) {
159+
return false;
160+
}
153161
}
154162

155163
static double valueProtoToFloat64(com.google.protobuf.Value proto) {

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

+9
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import com.google.api.core.ApiFuture;
2020
import com.google.common.base.Function;
21+
import com.google.spanner.v1.PartialResultSet;
2122
import java.util.List;
2223
import java.util.concurrent.ExecutionException;
2324
import java.util.concurrent.Executor;
@@ -223,4 +224,12 @@ interface ReadyCallback {
223224
* @param transformer function which will be used to transform the row. It should not return null.
224225
*/
225226
<T> List<T> toList(Function<StructReader, T> transformer) throws SpannerException;
227+
228+
/**
229+
* An interface to register the listener for streaming gRPC request. It will be called when a
230+
* chunk is received from gRPC streaming call.
231+
*/
232+
interface StreamMessageListener {
233+
void onStreamMessage(PartialResultSet partialResultSet, boolean bufferIsFull);
234+
}
226235
}

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

+80-21
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818

1919
import com.google.api.core.ApiFuture;
2020
import com.google.api.core.ApiFutures;
21-
import com.google.api.core.ListenableFutureToApiFuture;
2221
import com.google.api.core.SettableApiFuture;
2322
import com.google.api.gax.core.ExecutorProvider;
2423
import com.google.cloud.spanner.AbstractReadContext.ListenableAsyncResultSet;
@@ -29,13 +28,13 @@
2928
import com.google.common.collect.ImmutableList;
3029
import com.google.common.util.concurrent.ListeningScheduledExecutorService;
3130
import com.google.common.util.concurrent.MoreExecutors;
31+
import com.google.spanner.v1.PartialResultSet;
3232
import com.google.spanner.v1.ResultSetMetadata;
3333
import com.google.spanner.v1.ResultSetStats;
3434
import java.util.Collection;
3535
import java.util.LinkedList;
3636
import java.util.List;
3737
import java.util.concurrent.BlockingDeque;
38-
import java.util.concurrent.Callable;
3938
import java.util.concurrent.CountDownLatch;
4039
import java.util.concurrent.ExecutionException;
4140
import java.util.concurrent.Executor;
@@ -45,12 +44,14 @@
4544
import java.util.logging.Logger;
4645

4746
/** Default implementation for {@link AsyncResultSet}. */
48-
class AsyncResultSetImpl extends ForwardingStructReader implements ListenableAsyncResultSet {
47+
class AsyncResultSetImpl extends ForwardingStructReader
48+
implements ListenableAsyncResultSet, AsyncResultSet.StreamMessageListener {
4949
private static final Logger log = Logger.getLogger(AsyncResultSetImpl.class.getName());
5050

5151
/** State of an {@link AsyncResultSetImpl}. */
5252
private enum State {
5353
INITIALIZED,
54+
STREAMING_INITIALIZED,
5455
/** SYNC indicates that the {@link ResultSet} is used in sync pattern. */
5556
SYNC,
5657
CONSUMING,
@@ -115,12 +116,15 @@ private enum State {
115116

116117
private State state = State.INITIALIZED;
117118

119+
/** This variable indicates that produce rows thread is initiated */
120+
private volatile boolean produceRowsInitiated;
121+
118122
/**
119123
* This variable indicates whether all the results from the underlying result set have been read.
120124
*/
121125
private volatile boolean finished;
122126

123-
private volatile ApiFuture<Void> result;
127+
private volatile SettableApiFuture<Void> result;
124128

125129
/**
126130
* This variable indicates whether {@link #tryNext()} has returned {@link CursorState#DONE} or a
@@ -329,12 +333,12 @@ public void run() {
329333
private final CallbackRunnable callbackRunnable = new CallbackRunnable();
330334

331335
/**
332-
* {@link ProduceRowsCallable} reads data from the underlying {@link ResultSet}, places these in
336+
* {@link ProduceRowsRunnable} reads data from the underlying {@link ResultSet}, places these in
333337
* the buffer and dispatches the {@link CallbackRunnable} when data is ready to be consumed.
334338
*/
335-
private class ProduceRowsCallable implements Callable<Void> {
339+
private class ProduceRowsRunnable implements Runnable {
336340
@Override
337-
public Void call() throws Exception {
341+
public void run() {
338342
boolean stop = false;
339343
boolean hasNext = false;
340344
try {
@@ -393,12 +397,17 @@ public Void call() throws Exception {
393397
}
394398
// Call the callback if there are still rows in the buffer that need to be processed.
395399
while (!stop) {
396-
waitIfPaused();
397-
startCallbackIfNecessary();
398-
// Make sure we wait until the callback runner has actually finished.
399-
consumingLatch.await();
400-
synchronized (monitor) {
401-
stop = cursorReturnedDoneOrException;
400+
try {
401+
waitIfPaused();
402+
startCallbackIfNecessary();
403+
// Make sure we wait until the callback runner has actually finished.
404+
consumingLatch.await();
405+
synchronized (monitor) {
406+
stop = cursorReturnedDoneOrException;
407+
}
408+
} catch (Throwable e) {
409+
result.setException(e);
410+
return;
402411
}
403412
}
404413
} finally {
@@ -410,14 +419,14 @@ public Void call() throws Exception {
410419
}
411420
synchronized (monitor) {
412421
if (executionException != null) {
413-
throw executionException;
414-
}
415-
if (state == State.CANCELLED) {
416-
throw CANCELLED_EXCEPTION;
422+
result.setException(executionException);
423+
} else if (state == State.CANCELLED) {
424+
result.setException(CANCELLED_EXCEPTION);
425+
} else {
426+
result.set(null);
417427
}
418428
}
419429
}
420-
return null;
421430
}
422431

423432
private void waitIfPaused() throws InterruptedException {
@@ -449,6 +458,26 @@ private void startCallbackWithBufferLatchIfNecessary(int bufferLatch) {
449458
}
450459
}
451460

461+
private class InitiateStreamingRunnable implements Runnable {
462+
463+
@Override
464+
public void run() {
465+
try {
466+
// This method returns true if the underlying result set is a streaming result set (e.g. a
467+
// GrpcResultSet).
468+
// Those result sets will trigger initiateProduceRows() when the first results are received.
469+
// Non-streaming result sets do not trigger this callback, and for those result sets, we
470+
// need to eagerly start the ProduceRowsRunnable.
471+
if (!initiateStreaming(AsyncResultSetImpl.this)) {
472+
initiateProduceRows();
473+
}
474+
} catch (Throwable exception) {
475+
executionException = SpannerExceptionFactory.asSpannerException(exception);
476+
initiateProduceRows();
477+
}
478+
}
479+
}
480+
452481
/** Sets the callback for this {@link AsyncResultSet}. */
453482
@Override
454483
public ApiFuture<Void> setCallback(Executor exec, ReadyCallback cb) {
@@ -458,16 +487,24 @@ public ApiFuture<Void> setCallback(Executor exec, ReadyCallback cb) {
458487
this.state == State.INITIALIZED, "callback may not be set multiple times");
459488

460489
// Start to fetch data and buffer these.
461-
this.result =
462-
new ListenableFutureToApiFuture<>(this.service.submit(new ProduceRowsCallable()));
490+
this.result = SettableApiFuture.create();
491+
this.state = State.STREAMING_INITIALIZED;
492+
this.service.execute(new InitiateStreamingRunnable());
463493
this.executor = MoreExecutors.newSequentialExecutor(Preconditions.checkNotNull(exec));
464494
this.callback = Preconditions.checkNotNull(cb);
465-
this.state = State.RUNNING;
466495
pausedLatch.countDown();
467496
return result;
468497
}
469498
}
470499

500+
private void initiateProduceRows() {
501+
if (this.state == State.STREAMING_INITIALIZED) {
502+
this.state = State.RUNNING;
503+
}
504+
produceRowsInitiated = true;
505+
this.service.execute(new ProduceRowsRunnable());
506+
}
507+
471508
Future<Void> getResult() {
472509
return result;
473510
}
@@ -578,6 +615,10 @@ public ResultSetMetadata getMetadata() {
578615
return delegateResultSet.get().getMetadata();
579616
}
580617

618+
boolean initiateStreaming(StreamMessageListener streamMessageListener) {
619+
return StreamingUtil.initiateStreaming(delegateResultSet.get(), streamMessageListener);
620+
}
621+
581622
@Override
582623
protected void checkValidState() {
583624
synchronized (monitor) {
@@ -593,4 +634,22 @@ public Struct getCurrentRowAsStruct() {
593634
checkValidState();
594635
return currentRow;
595636
}
637+
638+
@Override
639+
public void onStreamMessage(PartialResultSet partialResultSet, boolean bufferIsFull) {
640+
synchronized (monitor) {
641+
if (produceRowsInitiated) {
642+
return;
643+
}
644+
// if PartialResultSet contains a resume token or buffer size is full, or
645+
// we have reached the end of the stream, we can start the thread.
646+
boolean startJobThread =
647+
!partialResultSet.getResumeToken().isEmpty()
648+
|| bufferIsFull
649+
|| partialResultSet == GrpcStreamIterator.END_OF_STREAM;
650+
if (startJobThread || state != State.STREAMING_INITIALIZED) {
651+
initiateProduceRows();
652+
}
653+
}
654+
}
596655
}

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

+9-1
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,16 @@
1616

1717
package com.google.cloud.spanner;
1818

19+
import com.google.api.core.InternalApi;
1920
import com.google.common.base.Preconditions;
2021
import com.google.common.base.Supplier;
2122
import com.google.common.base.Suppliers;
2223
import com.google.spanner.v1.ResultSetMetadata;
2324
import com.google.spanner.v1.ResultSetStats;
2425

2526
/** Forwarding implementation of ResultSet that forwards all calls to a delegate. */
26-
public class ForwardingResultSet extends ForwardingStructReader implements ProtobufResultSet {
27+
public class ForwardingResultSet extends ForwardingStructReader
28+
implements ProtobufResultSet, StreamingResultSet {
2729

2830
private Supplier<? extends ResultSet> delegate;
2931

@@ -102,4 +104,10 @@ public ResultSetStats getStats() {
102104
public ResultSetMetadata getMetadata() {
103105
return delegate.get().getMetadata();
104106
}
107+
108+
@Override
109+
@InternalApi
110+
public boolean initiateStreaming(AsyncResultSet.StreamMessageListener streamMessageListener) {
111+
return StreamingUtil.initiateStreaming(delegate.get(), streamMessageListener);
112+
}
105113
}

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

+9-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import static com.google.cloud.spanner.SpannerExceptionFactory.newSpannerException;
2020
import static com.google.common.base.Preconditions.checkState;
2121

22+
import com.google.api.core.InternalApi;
2223
import com.google.common.annotations.VisibleForTesting;
2324
import com.google.protobuf.Value;
2425
import com.google.spanner.v1.PartialResultSet;
@@ -30,7 +31,8 @@
3031
import javax.annotation.Nullable;
3132

3233
@VisibleForTesting
33-
class GrpcResultSet extends AbstractResultSet<List<Object>> implements ProtobufResultSet {
34+
class GrpcResultSet extends AbstractResultSet<List<Object>>
35+
implements ProtobufResultSet, StreamingResultSet {
3436
private final GrpcValueIterator iterator;
3537
private final Listener listener;
3638
private final DecodeMode decodeMode;
@@ -123,6 +125,12 @@ public ResultSetMetadata getMetadata() {
123125
return metadata;
124126
}
125127

128+
@Override
129+
@InternalApi
130+
public boolean initiateStreaming(AsyncResultSet.StreamMessageListener streamMessageListener) {
131+
return iterator.initiateStreaming(streamMessageListener);
132+
}
133+
126134
@Override
127135
public void close() {
128136
synchronized (this) {

0 commit comments

Comments
 (0)