@@ -358,9 +358,40 @@ static Builder newBuilder() {
358358 @ GuardedBy ("txnLock" )
359359 private ByteString transactionId ;
360360
361+ /**
362+ * Future used to synchronize concurrent operations during transaction initialization:
363+ *
364+ * <ul>
365+ * <li>When using explicit {@code BeginTransaction} (e.g. via {@link #initTransaction()}), the
366+ * first caller creates this future, releases {@code txnLock}, and executes the RPC
367+ * outside the lock. Subsequent concurrent callers wait on this future outside the lock.
368+ * <li>When using inlined {@code BeginTransaction} options, the first query or read operation
369+ * creates this future and includes {@code Begin} in its {@link TransactionSelector}.
370+ * Concurrent queries wait on this future until the first operation's initial response
371+ * arrives and sets the transaction ID via {@link #onTransactionMetadata}.
372+ * </ul>
373+ */
361374 @ GuardedBy ("txnLock" )
362375 private SettableApiFuture <ByteString > transactionIdFuture ;
363376
377+ /**
378+ * Future completed when all in-flight asynchronous query initializations (tracked by {@link
379+ * #pendingStarts}) have finished and the read context has been closed.
380+ */
381+ @ GuardedBy ("txnLock" )
382+ private SettableApiFuture <Void > closeFuture ;
383+
384+ /**
385+ * Flag indicating whether the transaction has been closed or is currently closing. Once true,
386+ * no new reads or queries (synchronous or asynchronous) are permitted to start on this context.
387+ */
388+ @ GuardedBy ("txnLock" )
389+ private boolean isClosedOrClosing ;
390+
391+ /**
392+ * Counter tracking the number of asynchronous queries created on this transaction whose
393+ * delegate {@link ResultSet} suppliers have not yet been executed or closed.
394+ */
364395 private final AtomicInteger pendingStarts = new AtomicInteger (0 );
365396
366397 private static final long WAIT_FOR_INLINE_BEGIN_TIMEOUT_MILLIS = 60_000L ;
@@ -468,20 +499,53 @@ TransactionSelector getTransactionSelector() {
468499 }
469500 }
470501
502+ /**
503+ * Decrements the count of pending asynchronous query starts. When the count drops to 0, if the
504+ * transaction is in the process of closing (i.e. {@link #closeAsync()} was called), this method
505+ * closes the read context via {@code super.close()} and completes {@link #closeFuture}.
506+ */
471507 private void decrementPendingStartsAndSignal () {
472508 if (pendingStarts .decrementAndGet () == 0 ) {
509+ SettableApiFuture <Void > futureToComplete = null ;
510+ Throwable error = null ;
473511 txnLock .lock ();
474512 try {
475513 hasNoPendingStarts .signalAll ();
514+ if (closeFuture != null ) {
515+ try {
516+ super .close ();
517+ } catch (Throwable throwable ) {
518+ error = throwable ;
519+ }
520+ futureToComplete = closeFuture ;
521+ }
476522 } finally {
477523 txnLock .unlock ();
478524 }
525+ if (futureToComplete != null ) {
526+ if (error != null ) {
527+ futureToComplete .setException (error );
528+ } else {
529+ futureToComplete .set (null );
530+ }
531+ }
479532 }
480533 }
481534
535+ /**
536+ * Creates an {@link AsyncResultSetImpl} for an asynchronous read or query. Tracks the query in
537+ * {@link #pendingStarts} so that {@link #closeAsync()} will not close the read context until
538+ * the query initialization has completed.
539+ */
482540 private ListenableAsyncResultSet createAsyncResultSet (
483541 Supplier <ResultSet > resultSetSupplier , int bufferRows ) {
484- pendingStarts .incrementAndGet ();
542+ txnLock .lock ();
543+ try {
544+ checkState (!isClosedOrClosing , "Context has been closed" );
545+ pendingStarts .incrementAndGet ();
546+ } finally {
547+ txnLock .unlock ();
548+ }
485549 // Make sure that we decrement the counter exactly once, either
486550 // when the query is actually executed, or when the result set is closed,
487551 // or if something goes wrong when creating the result set.
@@ -518,6 +582,41 @@ public void close() {
518582 }
519583 }
520584
585+ /**
586+ * Ensures that the transaction is neither closed nor in the process of closing before
587+ * initiating a new synchronous read or query.
588+ */
589+ private void checkClosedOrClosing () {
590+ txnLock .lock ();
591+ try {
592+ checkState (!isClosedOrClosing , "Context has been closed" );
593+ } finally {
594+ txnLock .unlock ();
595+ }
596+ }
597+
598+ @ Override
599+ ResultSet readInternalWithOptions (
600+ String table ,
601+ @ Nullable String index ,
602+ KeySet keys ,
603+ Iterable <String > columns ,
604+ final Options readOptions ,
605+ ByteString partitionToken ) {
606+ checkClosedOrClosing ();
607+ return super .readInternalWithOptions (
608+ table , index , keys , columns , readOptions , partitionToken );
609+ }
610+
611+ @ Override
612+ ResultSet executeQueryInternal (
613+ final Statement statement ,
614+ final com .google .spanner .v1 .ExecuteSqlRequest .QueryMode queryMode ,
615+ final QueryOption ... options ) {
616+ checkClosedOrClosing ();
617+ return super .executeQueryInternal (statement , queryMode , options );
618+ }
619+
521620 @ Override
522621 public ListenableAsyncResultSet readAsync (
523622 String table , KeySet keys , Iterable <String > columns , ReadOption ... options ) {
@@ -527,7 +626,7 @@ public ListenableAsyncResultSet readAsync(
527626 ? readOptions .bufferRows ()
528627 : AsyncResultSetImpl .DEFAULT_BUFFER_SIZE ;
529628 return createAsyncResultSet (
530- () -> readInternal (table , null , keys , columns , options ), bufferRows );
629+ () -> super . readInternal (table , null , keys , columns , options ), bufferRows );
531630 }
532631
533632 @ Override
@@ -539,7 +638,7 @@ public ListenableAsyncResultSet readUsingIndexAsync(
539638 ? readOptions .bufferRows ()
540639 : AsyncResultSetImpl .DEFAULT_BUFFER_SIZE ;
541640 return createAsyncResultSet (
542- () -> readInternal (table , checkNotNull (index ), keys , columns , options ), bufferRows );
641+ () -> super . readInternal (table , checkNotNull (index ), keys , columns , options ), bufferRows );
543642 }
544643
545644 @ Override
@@ -551,7 +650,7 @@ public ListenableAsyncResultSet executeQueryAsync(Statement statement, QueryOpti
551650 : AsyncResultSetImpl .DEFAULT_BUFFER_SIZE ;
552651 return createAsyncResultSet (
553652 () ->
554- executeQueryInternal (
653+ super . executeQueryInternal (
555654 statement , com .google .spanner .v1 .ExecuteSqlRequest .QueryMode .NORMAL , options ),
556655 bufferRows );
557656 }
@@ -650,21 +749,37 @@ ByteString getTransactionId() {
650749 }
651750 }
652751
752+ /**
753+ * Closes the transaction asynchronously.
754+ *
755+ * <p>If there are no in-flight asynchronous query initializations ({@code pendingStarts == 0}),
756+ * the read context is closed immediately and a completed future is returned. If there are
757+ * pending starts, this method marks {@code isClosedOrClosing = true} and returns a {@link
758+ * SettableApiFuture} that will be completed by {@link #decrementPendingStartsAndSignal()} when
759+ * all pending query initializations finish.
760+ */
653761 @ Override
654- public void close () {
762+ public ApiFuture < Void > closeAsync () {
655763 txnLock .lock ();
656764 try {
657- while (pendingStarts .get () > 0 ) {
658- try {
659- hasNoPendingStarts .await ();
660- } catch (InterruptedException e ) {
661- throw SpannerExceptionFactory .propagateInterrupt (e );
662- }
765+ if (isClosedOrClosing ) {
766+ return closeFuture != null ? closeFuture : ApiFutures .immediateFuture (null );
767+ }
768+ isClosedOrClosing = true ;
769+ if (pendingStarts .get () == 0 ) {
770+ super .close ();
771+ return ApiFutures .immediateFuture (null );
663772 }
773+ closeFuture = SettableApiFuture .create ();
774+ return closeFuture ;
664775 } finally {
665776 txnLock .unlock ();
666777 }
667- super .close ();
778+ }
779+
780+ @ Override
781+ public void close () {
782+ SpannerApiFutures .get (closeAsync ());
668783 }
669784
670785 private TransactionOptions createReadOnlyTransactionOptions () {
@@ -686,47 +801,77 @@ private TransactionOptions createReadOnlyTransactionOptions() {
686801 * Multiplexed Session.
687802 */
688803 void initFallbackTransaction () {
804+ BeginTransactionRequest request ;
689805 txnLock .lock ();
690806 try {
691807 span .addAnnotation ("Creating Transaction" );
692- final BeginTransactionRequest request =
808+ request =
693809 BeginTransactionRequest .newBuilder ()
694810 .setSession (session .getName ())
695811 .setOptions (createReadOnlyTransactionOptions ())
696812 .build ();
697- initTransactionInternal (request );
698813 } finally {
699814 txnLock .unlock ();
700815 }
816+ initTransactionInternal (request );
701817 }
702818
819+ /**
820+ * Initializes the transaction by issuing a BeginTransaction RPC.
821+ *
822+ * <p>To prevent blocking concurrent operations (such as {@link #closeAsync()}) while a network
823+ * RPC is in-flight, {@code rpc.beginTransaction} is executed <b>outside</b> {@code txnLock}. A
824+ * leader/follower pattern using {@link #transactionIdFuture} is used: the first caller acquires
825+ * the lock, creates {@code transactionIdFuture}, releases the lock, and executes the RPC.
826+ * Subsequent concurrent callers retrieve {@code transactionIdFuture} and wait on it outside the
827+ * lock.
828+ */
703829 void initTransaction () {
704830 SessionImpl .throwIfTransactionsPending ();
705831
706- // Since we only support synchronous calls, just block on "txnLock" while the RPC is in
707- // flight. Note that we use the strategy of sending an explicit BeginTransaction() RPC,
708- // rather than using the first read in the transaction to begin it implicitly. The chosen
709- // strategy is sub-optimal in the case of the first read being fast, as it incurs an extra
710- // RTT, but optimal if the first read is slow. As the client library is now using streaming
711- // reads, a possible optimization could be to use the first read in the transaction to begin
712- // it implicitly.
832+ ApiFuture <ByteString > futureToWaitFor = null ;
833+ BeginTransactionRequest request = null ;
713834 txnLock .lock ();
714835 try {
715836 if (transactionId != null ) {
716837 return ;
717838 }
718- span .addAnnotation ("Creating Transaction" );
719- final BeginTransactionRequest request =
720- BeginTransactionRequest .newBuilder ()
721- .setSession (session .getName ())
722- .setOptions (createReadOnlyTransactionOptions ())
723- .build ();
724- initTransactionInternal (request );
839+ if (transactionIdFuture != null ) {
840+ futureToWaitFor = transactionIdFuture ;
841+ } else {
842+ transactionIdFuture = SettableApiFuture .create ();
843+ span .addAnnotation ("Creating Transaction" );
844+ request =
845+ BeginTransactionRequest .newBuilder ()
846+ .setSession (session .getName ())
847+ .setOptions (createReadOnlyTransactionOptions ())
848+ .build ();
849+ }
725850 } finally {
726851 txnLock .unlock ();
727852 }
853+
854+ if (futureToWaitFor != null ) {
855+ try {
856+ futureToWaitFor .get ();
857+ return ;
858+ } catch (ExecutionException executionException ) {
859+ throw SpannerExceptionFactory .asSpannerException (executionException .getCause ());
860+ } catch (InterruptedException interruptedException ) {
861+ Thread .currentThread ().interrupt ();
862+ throw SpannerExceptionFactory .newSpannerExceptionForCancellation (
863+ null , interruptedException );
864+ }
865+ }
866+
867+ initTransactionInternal (request );
728868 }
729869
870+ /**
871+ * Executes the BeginTransaction RPC outside {@code txnLock}, updates transaction state under
872+ * {@code txnLock}, and completes or fails {@link #transactionIdFuture} so waiting callers are
873+ * notified.
874+ */
730875 private void initTransactionInternal (BeginTransactionRequest request ) {
731876 try {
732877 Transaction transaction =
@@ -739,20 +884,41 @@ private void initTransactionInternal(BeginTransactionRequest request) {
739884 throw SpannerExceptionFactory .newSpannerException (
740885 ErrorCode .INTERNAL , "Missing expected transaction.id metadata field" );
741886 }
887+ Timestamp readTimestamp ;
742888 try {
743- timestamp = Timestamp .fromProto (transaction .getReadTimestamp ());
744- } catch (IllegalArgumentException e ) {
889+ readTimestamp = Timestamp .fromProto (transaction .getReadTimestamp ());
890+ } catch (IllegalArgumentException illegalArgumentException ) {
745891 throw SpannerExceptionFactory .newSpannerException (
746- ErrorCode .INTERNAL , "Bad value in transaction.read_timestamp metadata field" , e );
892+ ErrorCode .INTERNAL ,
893+ "Bad value in transaction.read_timestamp metadata field" ,
894+ illegalArgumentException );
895+ }
896+ txnLock .lock ();
897+ try {
898+ timestamp = readTimestamp ;
899+ transactionId = transaction .getId ();
900+ if (transactionIdFuture != null && !transactionIdFuture .isDone ()) {
901+ transactionIdFuture .set (transactionId );
902+ }
903+ } finally {
904+ txnLock .unlock ();
747905 }
748- transactionId = transaction .getId ();
749906 span .addAnnotation (
750907 "Transaction Creation Done" ,
751908 ImmutableMap .of (
752- "Id" , transaction .getId ().toStringUtf8 (), "Timestamp" , timestamp .toString ()));
753- } catch (SpannerException e ) {
754- span .addAnnotation ("Transaction Creation Failed" , e );
755- throw e ;
909+ "Id" , transaction .getId ().toStringUtf8 (), "Timestamp" , readTimestamp .toString ()));
910+ } catch (Throwable throwable ) {
911+ SpannerException spannerException = SpannerExceptionFactory .asSpannerException (throwable );
912+ span .addAnnotation ("Transaction Creation Failed" , spannerException );
913+ txnLock .lock ();
914+ try {
915+ if (transactionIdFuture != null && !transactionIdFuture .isDone ()) {
916+ transactionIdFuture .setException (spannerException );
917+ }
918+ } finally {
919+ txnLock .unlock ();
920+ }
921+ throw spannerException ;
756922 }
757923 }
758924 }
@@ -1206,6 +1372,12 @@ public void close() {
12061372 }
12071373 }
12081374
1375+ @ Override
1376+ public ApiFuture <Void > closeAsync () {
1377+ close ();
1378+ return ApiFutures .immediateFuture (null );
1379+ }
1380+
12091381 /**
12101382 * Returns the {@link TransactionSelector} that should be used for a statement that is executed on
12111383 * this read context. This could be a reference to an existing transaction ID, or it could be a
0 commit comments