perf(spanner): make internal AsyncResultSet row production non-blocking - #14268
perf(spanner): make internal AsyncResultSet row production non-blocking#14268olavloite wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a non-blocking isDataAvailable() check across Spanner result set classes to determine if data can be read immediately without blocking on network I/O. It refactors AsyncResultSetImpl to use a state-machine-based scheduling mechanism for producers and callbacks, replacing the previous blocking latch-based synchronization. Additionally, comprehensive unit tests are added to verify these changes. The review feedback highlights that catching Throwable in ProduceRowsRunnable can swallow InterruptedException, and recommends restoring the thread's interrupted status if an InterruptedException is caught.
c74722a to
a2fc337
Compare
|
/gemini review |
1 similar comment
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request refactors the asynchronous result set implementation (AsyncResultSetImpl) to support non-blocking streaming data checks via a new isDataAvailable() method. It replaces the previous latch-based synchronization mechanism with a state-driven scheduling model for producers and callbacks, and introduces strict null-safety annotations (@NullMarked and @Nullable). The review feedback highlights three critical issues in AsyncResultSetImpl where the nullable fields callback, executor, and result are dereferenced directly without local capture or null checks, which could lead to NullPointerExceptions and violates the newly introduced null-safety rules.
a2fc337 to
4e93618
Compare
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces a non-blocking data availability check (isDataAvailable()) across various result set and iterator classes in the Google Cloud Spanner library. It also refactors the concurrency model in AsyncResultSetImpl by replacing several CountDownLatches with state flags and explicit scheduling methods to improve thread safety and prevent blocking. The feedback suggests replacing the synchronized blocks in AsyncResultSetImpl with explicit ReentrantLocks to avoid carrier thread pinning when using virtual threads.
Refactors AsyncResultSetImpl to produce rows cooperatively without holding executor threads. Uses isDataAvailable on underlying streaming iterators to avoid blocking on network I/O or full buffers, eliminating thread-per-query pinning and improving throughput under concurrency.
4e93618 to
2052c6c
Compare
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces non-blocking data availability checks (isDataAvailable()) across various result set and iterator implementations, including AsyncResultSetImpl, GrpcStreamIterator, GrpcValueIterator, and ResumableStreamIterator. Additionally, AsyncResultSetImpl is refactored to replace thread-blocking latches with state flags to coordinate producer and consumer threads asynchronously. The review feedback highlights critical race conditions in ResumableStreamIterator.java where gRPC streams could be leaked or started after the iterator is closed, and suggests introducing a closed flag to safely manage the lifecycle of the iterator and its streams.
| public void close(@Nullable String message) { | ||
| if (stream != null) { | ||
| stream.close(message); | ||
| span.end(); | ||
| stream = null; | ||
| } |
There was a problem hiding this comment.
To prevent race conditions and avoid starting new gRPC streams after the iterator has been closed (for example, if a retry is scheduled or in progress when close() is called), we should introduce a private volatile boolean closed; field to track the closed state of the iterator.
public void close(@Nullable String message) {
closed = true;
if (stream != null) {
stream.close(message);
span.end();
stream = null;
}
}References
- When using lazily initialized resources, ensure that teardown or close methods perform explicit null checks before invoking methods on them to prevent NullPointerException.
| if (stream == null) { | ||
| stream = streamIterator; | ||
| } |
There was a problem hiding this comment.
There is a potential race condition here:
startStreamis called, which internally callssetStream(stream)to setthis.stream = streamIterator.- Before
startGrpcStreaming()continues, another thread callsclose(), which closes the stream and setsthis.stream = null. startGrpcStreaming()resumes, seesthis.stream == null, and setsthis.stream = streamIterator(which is the already-closed stream).
This leaks the closed stream and leaves this.stream non-null, so it will never be closed again. By checking the closed flag, we can safely close the newly created stream if the iterator was closed concurrently, and avoid leaking it.
if (!closed) {
stream = streamIterator;
} else {
streamIterator.close(null);
}
Refactors AsyncResultSetImpl to produce rows cooperatively without holding executor threads. Uses isDataAvailable on underlying streaming iterators to avoid blocking on network I/O or full buffers, eliminating thread-per-query pinning and improving throughput under concurrency.