Skip to content

Commit 2ceff50

Browse files
fix: Avoid blocking thread in AsyncResultSet
1 parent 11ead4e commit 2ceff50

14 files changed

+337
-71
lines changed

google-cloud-spanner/clirr-ignored-differences.xml

+5-1
Original file line numberDiff line numberDiff line change
@@ -790,5 +790,9 @@
790790
<className>com/google/cloud/spanner/connection/Connection</className>
791791
<method>boolean isAutoBatchDmlUpdateCountVerification()</method>
792792
</difference>
793-
793+
<difference>
794+
<differenceType>7012</differenceType>
795+
<className>com/google/cloud/spanner/ResultSet</className>
796+
<method>boolean initiateStreaming(com.google.cloud.spanner.AsyncResultSet$StreamMessageListener)</method>
797+
</difference>
794798
</differences>

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

+10-4
Original file line numberDiff line numberDiff line change
@@ -768,9 +768,12 @@ 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+
stream.registerListener(streamListener);
774777
if (partitionToken != null) {
775778
request.setPartitionToken(partitionToken);
776779
}
@@ -791,8 +794,8 @@ CloseableIterator<PartialResultSet> startStream(@Nullable ByteString resumeToken
791794
getTransactionChannelHint(),
792795
isRouteToLeader());
793796
session.markUsed(clock.instant());
794-
call.request(prefetchChunks);
795797
stream.setCall(call, request.getTransaction().hasBegin());
798+
call.request(prefetchChunks);
796799
return stream;
797800
}
798801

@@ -959,9 +962,12 @@ ResultSet readInternalWithOptions(
959962
rpc.getReadRetrySettings(),
960963
rpc.getReadRetryableCodes()) {
961964
@Override
962-
CloseableIterator<PartialResultSet> startStream(@Nullable ByteString resumeToken) {
965+
CloseableIterator<PartialResultSet> startStream(
966+
@Nullable ByteString resumeToken,
967+
AsyncResultSet.StreamMessageListener streamListener) {
963968
GrpcStreamIterator stream =
964969
new GrpcStreamIterator(prefetchChunks, cancelQueryWhenClientIsClosed);
970+
stream.registerListener(streamListener);
965971
TransactionSelector selector = null;
966972
if (resumeToken != null) {
967973
builder.setResumeToken(resumeToken);
@@ -980,8 +986,8 @@ CloseableIterator<PartialResultSet> startStream(@Nullable ByteString resumeToken
980986
getTransactionChannelHint(),
981987
isRouteToLeader());
982988
session.markUsed(clock.instant());
983-
call.request(prefetchChunks);
984989
stream.setCall(call, /* withBeginTransaction = */ builder.getTransaction().hasBegin());
990+
call.request(prefetchChunks);
985991
return stream;
986992
}
987993

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

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

152152
boolean isWithBeginTransaction();
153+
154+
default boolean initiateStreaming(AsyncResultSet.StreamMessageListener streamMessageListener) {
155+
return false;
156+
}
153157
}
154158

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

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

+21
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,24 @@ 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(
234+
PartialResultSet partialResultSet,
235+
int prefetchChunks,
236+
int currentBufferSize,
237+
StreamMessageRequestor streamMessageRequestor);
238+
}
239+
240+
/**
241+
* An interface to request more messages from the gRPC streaming call. It will be implemented by
242+
* the class which has access to SpannerRpc.StreamingCall object
243+
*/
244+
interface StreamMessageRequestor {
245+
void requestMessages(int numOfMessages);
246+
}
226247
}

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

+81-23
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,28 +28,25 @@
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;
37-
import java.util.concurrent.BlockingDeque;
38-
import java.util.concurrent.Callable;
39-
import java.util.concurrent.CountDownLatch;
40-
import java.util.concurrent.ExecutionException;
41-
import java.util.concurrent.Executor;
42-
import java.util.concurrent.Future;
43-
import java.util.concurrent.LinkedBlockingDeque;
37+
import java.util.concurrent.*;
4438
import java.util.logging.Level;
4539
import java.util.logging.Logger;
4640

4741
/** Default implementation for {@link AsyncResultSet}. */
48-
class AsyncResultSetImpl extends ForwardingStructReader implements ListenableAsyncResultSet {
42+
class AsyncResultSetImpl extends ForwardingStructReader
43+
implements ListenableAsyncResultSet, AsyncResultSet.StreamMessageListener {
4944
private static final Logger log = Logger.getLogger(AsyncResultSetImpl.class.getName());
5045

5146
/** State of an {@link AsyncResultSetImpl}. */
5247
private enum State {
5348
INITIALIZED,
49+
IN_PROGRESS,
5450
/** SYNC indicates that the {@link ResultSet} is used in sync pattern. */
5551
SYNC,
5652
CONSUMING,
@@ -115,12 +111,15 @@ private enum State {
115111

116112
private State state = State.INITIALIZED;
117113

114+
/** This variable indicates that produce rows thread is initiated */
115+
private volatile boolean produceRowsInitiated;
116+
118117
/**
119118
* This variable indicates whether all the results from the underlying result set have been read.
120119
*/
121120
private volatile boolean finished;
122121

123-
private volatile ApiFuture<Void> result;
122+
private volatile SettableApiFuture<Void> result;
124123

125124
/**
126125
* This variable indicates whether {@link #tryNext()} has returned {@link CursorState#DONE} or a
@@ -329,12 +328,12 @@ public void run() {
329328
private final CallbackRunnable callbackRunnable = new CallbackRunnable();
330329

331330
/**
332-
* {@link ProduceRowsCallable} reads data from the underlying {@link ResultSet}, places these in
331+
* {@link ProduceRowsRunnable} reads data from the underlying {@link ResultSet}, places these in
333332
* the buffer and dispatches the {@link CallbackRunnable} when data is ready to be consumed.
334333
*/
335-
private class ProduceRowsCallable implements Callable<Void> {
334+
private class ProduceRowsRunnable implements Runnable {
336335
@Override
337-
public Void call() throws Exception {
336+
public void run() {
338337
boolean stop = false;
339338
boolean hasNext = false;
340339
try {
@@ -393,12 +392,17 @@ public Void call() throws Exception {
393392
}
394393
// Call the callback if there are still rows in the buffer that need to be processed.
395394
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;
395+
try {
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;
402+
}
403+
} catch (InterruptedException e) {
404+
result.setException(e);
405+
return;
402406
}
403407
}
404408
} finally {
@@ -410,14 +414,16 @@ public Void call() throws Exception {
410414
}
411415
synchronized (monitor) {
412416
if (executionException != null) {
417+
result.setException(executionException);
413418
throw executionException;
414419
}
415420
if (state == State.CANCELLED) {
421+
result.setException(CANCELLED_EXCEPTION);
416422
throw CANCELLED_EXCEPTION;
417423
}
418424
}
419425
}
420-
return null;
426+
result.set(null);
421427
}
422428

423429
private void waitIfPaused() throws InterruptedException {
@@ -449,6 +455,21 @@ private void startCallbackWithBufferLatchIfNecessary(int bufferLatch) {
449455
}
450456
}
451457

458+
private class InitiateStreamingRunnable implements Runnable {
459+
460+
@Override
461+
public void run() {
462+
try {
463+
if (!initiateStreaming(AsyncResultSetImpl.this)) {
464+
initiateProduceRows();
465+
}
466+
} catch (SpannerException e) {
467+
executionException = e;
468+
initiateProduceRows();
469+
}
470+
}
471+
}
472+
452473
/** Sets the callback for this {@link AsyncResultSet}. */
453474
@Override
454475
public ApiFuture<Void> setCallback(Executor exec, ReadyCallback cb) {
@@ -458,16 +479,24 @@ public ApiFuture<Void> setCallback(Executor exec, ReadyCallback cb) {
458479
this.state == State.INITIALIZED, "callback may not be set multiple times");
459480

460481
// Start to fetch data and buffer these.
461-
this.result =
462-
new ListenableFutureToApiFuture<>(this.service.submit(new ProduceRowsCallable()));
482+
this.result = SettableApiFuture.create();
483+
this.state = State.IN_PROGRESS;
484+
this.service.execute(new InitiateStreamingRunnable());
463485
this.executor = MoreExecutors.newSequentialExecutor(Preconditions.checkNotNull(exec));
464486
this.callback = Preconditions.checkNotNull(cb);
465-
this.state = State.RUNNING;
466487
pausedLatch.countDown();
467488
return result;
468489
}
469490
}
470491

492+
private void initiateProduceRows() {
493+
this.service.execute(new ProduceRowsRunnable());
494+
if (this.state == State.IN_PROGRESS) {
495+
this.state = State.RUNNING;
496+
}
497+
produceRowsInitiated = true;
498+
}
499+
471500
Future<Void> getResult() {
472501
return result;
473502
}
@@ -578,6 +607,11 @@ public ResultSetMetadata getMetadata() {
578607
return delegateResultSet.get().getMetadata();
579608
}
580609

610+
@Override
611+
public boolean initiateStreaming(StreamMessageListener streamMessageListener) {
612+
return delegateResultSet.get().initiateStreaming(streamMessageListener);
613+
}
614+
581615
@Override
582616
protected void checkValidState() {
583617
synchronized (monitor) {
@@ -593,4 +627,28 @@ public Struct getCurrentRowAsStruct() {
593627
checkValidState();
594628
return currentRow;
595629
}
630+
631+
@Override
632+
public void onStreamMessage(
633+
PartialResultSet partialResultSet,
634+
int prefetchChunks,
635+
int currentBufferSize,
636+
StreamMessageRequestor streamMessageRequestor) {
637+
synchronized (monitor) {
638+
if (produceRowsInitiated) {
639+
return;
640+
}
641+
// if PartialResultSet contains resume token or buffer size is more than configured size or
642+
// we have reached end of stream, we can start the thread
643+
boolean startJobThread =
644+
!partialResultSet.getResumeToken().isEmpty()
645+
|| currentBufferSize >= prefetchChunks
646+
|| partialResultSet == GrpcStreamIterator.END_OF_STREAM;
647+
if (startJobThread || state != State.IN_PROGRESS) {
648+
initiateProduceRows();
649+
} else {
650+
streamMessageRequestor.requestMessages(1);
651+
}
652+
}
653+
}
596654
}

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

+5
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,9 @@ public ResultSetStats getStats() {
102102
public ResultSetMetadata getMetadata() {
103103
return delegate.get().getMetadata();
104104
}
105+
106+
@Override
107+
public boolean initiateStreaming(AsyncResultSet.StreamMessageListener streamMessageListener) {
108+
return delegate.get().initiateStreaming(streamMessageListener);
109+
}
105110
}

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

+5
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,11 @@ public ResultSetMetadata getMetadata() {
123123
return metadata;
124124
}
125125

126+
@Override
127+
public boolean initiateStreaming(AsyncResultSet.StreamMessageListener streamMessageListener) {
128+
return iterator.initiateStreaming(streamMessageListener);
129+
}
130+
126131
@Override
127132
public void close() {
128133
synchronized (this) {

0 commit comments

Comments
 (0)