@@ -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 () {
472- if (pendingStarts .decrementAndGet () == 0 ) {
473- txnLock .lock ();
474- try {
508+ SettableApiFuture <Void > futureToComplete = null ;
509+ Throwable error = null ;
510+ txnLock .lock ();
511+ try {
512+ if (pendingStarts .decrementAndGet () == 0 ) {
475513 hasNoPendingStarts .signalAll ();
476- } finally {
477- txnLock .unlock ();
514+ if (closeFuture != null ) {
515+ try {
516+ super .close ();
517+ } catch (Throwable throwable ) {
518+ error = throwable ;
519+ }
520+ futureToComplete = closeFuture ;
521+ }
522+ }
523+ } finally {
524+ txnLock .unlock ();
525+ }
526+ if (futureToComplete != null ) {
527+ if (error != null ) {
528+ futureToComplete .setException (error );
529+ } else {
530+ futureToComplete .set (null );
478531 }
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,65 @@ private TransactionOptions createReadOnlyTransactionOptions() {
686801 * Multiplexed Session.
687802 */
688803 void initFallbackTransaction () {
689- txnLock .lock ();
690- try {
691- span .addAnnotation ("Creating Transaction" );
692- final BeginTransactionRequest request =
693- BeginTransactionRequest .newBuilder ()
694- .setSession (session .getName ())
695- .setOptions (createReadOnlyTransactionOptions ())
696- .build ();
697- initTransactionInternal (request );
698- } finally {
699- txnLock .unlock ();
700- }
804+ initTransaction ();
701805 }
702806
807+ /**
808+ * Initializes the transaction by issuing a BeginTransaction RPC.
809+ *
810+ * <p>To prevent blocking concurrent operations (such as {@link #closeAsync()}) while a network
811+ * RPC is in-flight, {@code rpc.beginTransaction} is executed <b>outside</b> {@code txnLock}. A
812+ * leader/follower pattern using {@link #transactionIdFuture} is used: the first caller acquires
813+ * the lock, creates {@code transactionIdFuture}, releases the lock, and executes the RPC.
814+ * Subsequent concurrent callers retrieve {@code transactionIdFuture} and wait on it outside the
815+ * lock.
816+ */
703817 void initTransaction () {
704818 SessionImpl .throwIfTransactionsPending ();
705819
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.
820+ ApiFuture <ByteString > futureToWaitFor = null ;
821+ BeginTransactionRequest request = null ;
713822 txnLock .lock ();
714823 try {
715824 if (transactionId != null ) {
716825 return ;
717826 }
718- span .addAnnotation ("Creating Transaction" );
719- final BeginTransactionRequest request =
720- BeginTransactionRequest .newBuilder ()
721- .setSession (session .getName ())
722- .setOptions (createReadOnlyTransactionOptions ())
723- .build ();
724- initTransactionInternal (request );
827+ if (transactionIdFuture != null ) {
828+ futureToWaitFor = transactionIdFuture ;
829+ } else {
830+ transactionIdFuture = SettableApiFuture .create ();
831+ span .addAnnotation ("Creating Transaction" );
832+ request =
833+ BeginTransactionRequest .newBuilder ()
834+ .setSession (session .getName ())
835+ .setOptions (createReadOnlyTransactionOptions ())
836+ .build ();
837+ }
725838 } finally {
726839 txnLock .unlock ();
727840 }
841+
842+ if (futureToWaitFor != null ) {
843+ try {
844+ futureToWaitFor .get ();
845+ return ;
846+ } catch (ExecutionException executionException ) {
847+ throw SpannerExceptionFactory .asSpannerException (executionException .getCause ());
848+ } catch (InterruptedException interruptedException ) {
849+ Thread .currentThread ().interrupt ();
850+ throw SpannerExceptionFactory .newSpannerExceptionForCancellation (
851+ null , interruptedException );
852+ }
853+ }
854+
855+ initTransactionInternal (request );
728856 }
729857
858+ /**
859+ * Executes the BeginTransaction RPC outside {@code txnLock}, updates transaction state under
860+ * {@code txnLock}, and completes or fails {@link #transactionIdFuture} so waiting callers are
861+ * notified.
862+ */
730863 private void initTransactionInternal (BeginTransactionRequest request ) {
731864 try {
732865 Transaction transaction =
@@ -739,20 +872,41 @@ private void initTransactionInternal(BeginTransactionRequest request) {
739872 throw SpannerExceptionFactory .newSpannerException (
740873 ErrorCode .INTERNAL , "Missing expected transaction.id metadata field" );
741874 }
875+ Timestamp readTimestamp ;
742876 try {
743- timestamp = Timestamp .fromProto (transaction .getReadTimestamp ());
744- } catch (IllegalArgumentException e ) {
877+ readTimestamp = Timestamp .fromProto (transaction .getReadTimestamp ());
878+ } catch (IllegalArgumentException illegalArgumentException ) {
745879 throw SpannerExceptionFactory .newSpannerException (
746- ErrorCode .INTERNAL , "Bad value in transaction.read_timestamp metadata field" , e );
880+ ErrorCode .INTERNAL ,
881+ "Bad value in transaction.read_timestamp metadata field" ,
882+ illegalArgumentException );
883+ }
884+ txnLock .lock ();
885+ try {
886+ timestamp = readTimestamp ;
887+ transactionId = transaction .getId ();
888+ if (transactionIdFuture != null && !transactionIdFuture .isDone ()) {
889+ transactionIdFuture .set (transactionId );
890+ }
891+ } finally {
892+ txnLock .unlock ();
747893 }
748- transactionId = transaction .getId ();
749894 span .addAnnotation (
750895 "Transaction Creation Done" ,
751896 ImmutableMap .of (
752- "Id" , transaction .getId ().toStringUtf8 (), "Timestamp" , timestamp .toString ()));
753- } catch (SpannerException e ) {
754- span .addAnnotation ("Transaction Creation Failed" , e );
755- throw e ;
897+ "Id" , transaction .getId ().toStringUtf8 (), "Timestamp" , readTimestamp .toString ()));
898+ } catch (Throwable throwable ) {
899+ SpannerException spannerException = SpannerExceptionFactory .asSpannerException (throwable );
900+ span .addAnnotation ("Transaction Creation Failed" , spannerException );
901+ txnLock .lock ();
902+ try {
903+ if (transactionIdFuture != null && !transactionIdFuture .isDone ()) {
904+ transactionIdFuture .setException (spannerException );
905+ }
906+ } finally {
907+ txnLock .unlock ();
908+ }
909+ throw spannerException ;
756910 }
757911 }
758912 }
@@ -1206,6 +1360,12 @@ public void close() {
12061360 }
12071361 }
12081362
1363+ @ Override
1364+ public ApiFuture <Void > closeAsync () {
1365+ close ();
1366+ return ApiFutures .immediateFuture (null );
1367+ }
1368+
12091369 /**
12101370 * Returns the {@link TransactionSelector} that should be used for a statement that is executed on
12111371 * this read context. This could be a reference to an existing transaction ID, or it could be a
0 commit comments