2828import com .google .cloud .spanner .Options .UpdateOption ;
2929import com .google .cloud .spanner .SessionClient .SessionConsumer ;
3030import com .google .cloud .spanner .SpannerException .ResourceNotFoundException ;
31+ import com .google .cloud .spanner .spi .v1 .SpannerRpc .ChannelPrimeSessionSource ;
3132import com .google .common .annotations .VisibleForTesting ;
3233import com .google .common .base .Preconditions ;
34+ import com .google .common .util .concurrent .Futures ;
3335import com .google .spanner .v1 .BatchWriteResponse ;
3436import java .time .Clock ;
3537import java .time .Duration ;
3840import java .util .EnumSet ;
3941import java .util .HashMap ;
4042import java .util .Map ;
43+ import java .util .concurrent .CancellationException ;
4144import java .util .concurrent .ExecutionException ;
4245import java .util .concurrent .Executors ;
4346import java .util .concurrent .Future ;
4851import java .util .concurrent .atomic .AtomicInteger ;
4952import java .util .concurrent .atomic .AtomicLong ;
5053import java .util .concurrent .atomic .AtomicReference ;
54+ import javax .annotation .Nullable ;
5155
5256/**
5357 * {@link DatabaseClient} implementation that uses a single multiplexed session to execute
5458 * transactions.
5559 */
56- final class MultiplexedSessionDatabaseClient extends AbstractMultiplexedSessionDatabaseClient {
60+ final class MultiplexedSessionDatabaseClient extends AbstractMultiplexedSessionDatabaseClient
61+ implements ChannelPrimeSessionSource {
5762 /**
5863 * The maximum number of attempts that the client will try to execute CreateSession for the
5964 * initial multiplexed session. This value is only used for the very first multiplexed session
@@ -186,7 +191,7 @@ private SharedChannelUsage(int numChannels) {
186191 */
187192 private final AtomicInteger numCurrentSingleUseTransactions = new AtomicInteger ();
188193
189- private boolean isClosed ;
194+ private volatile boolean isClosed ;
190195
191196 /** The duration before we try to replace the multiplexed session. The default is 7 days. */
192197 private final Duration sessionExpirationDuration ;
@@ -253,15 +258,23 @@ private SharedChannelUsage(int numChannels) {
253258 final SettableApiFuture <SessionReference > initialSessionReferenceFuture =
254259 SettableApiFuture .create ();
255260 this .multiplexedSessionReference = new AtomicReference <>(initialSessionReferenceFuture );
261+ spanner .getRpc ().registerChannelPrimeSessionSource (this );
256262
257- Duration waitDuration =
258- sessionClient .getSpanner ().getOptions ().getSessionPoolOptions ().getWaitForMinSessions ();
259- int initialAttempts =
260- waitDuration == null || waitDuration .isZero () ? MAX_INITIAL_CREATE_SESSION_ATTEMPTS : 1 ;
261- asyncCreateMultiplexedSession (initialSessionReferenceFuture , initialAttempts );
262- maybeWaitForSessionCreation (
263- sessionClient .getSpanner ().getOptions ().getSessionPoolOptions (),
264- initialSessionReferenceFuture );
263+ try {
264+ Duration waitDuration =
265+ sessionClient .getSpanner ().getOptions ().getSessionPoolOptions ().getWaitForMinSessions ();
266+ int initialAttempts =
267+ waitDuration == null || waitDuration .isZero () ? MAX_INITIAL_CREATE_SESSION_ATTEMPTS : 1 ;
268+ asyncCreateMultiplexedSession (initialSessionReferenceFuture , initialAttempts );
269+ maybeWaitForSessionCreation (
270+ sessionClient .getSpanner ().getOptions ().getSessionPoolOptions (),
271+ initialSessionReferenceFuture );
272+ } catch (Throwable t ) {
273+ // The caller never receives this client, so it will never be closed; close() therefore undoes
274+ // the registrations.
275+ close ();
276+ throw t ;
277+ }
265278 }
266279
267280 private void asyncCreateMultiplexedSession (
@@ -270,10 +283,19 @@ private void asyncCreateMultiplexedSession(
270283 new SessionConsumer () {
271284 @ Override
272285 public void onSessionReady (SessionImpl session ) {
286+ synchronized (MultiplexedSessionDatabaseClient .this ) {
287+ if (isClosed ) {
288+ // The client was closed while the session was being created. Ignore the session:
289+ // it must neither be handed to waiters of a closed client nor keep a maintainer
290+ // running for a client that no longer exists.
291+ sessionReferenceFuture .setException (newClosedException ());
292+ return ;
293+ }
294+ // only start the maintainer if we actually managed to create a session in the first
295+ // place. Starting it under the lock guarantees that a concurrent close() stops it.
296+ maintainer .start ();
297+ }
273298 sessionReferenceFuture .set (session .getSessionReference ());
274- // only start the maintainer if we actually managed to create a session in the first
275- // place.
276- maintainer .start ();
277299 if (sessionClient
278300 .getSpanner ()
279301 .getOptions ()
@@ -296,9 +318,11 @@ public void onSessionCreateFailure(Throwable t, int createFailureForSessionCount
296318 (ResourceNotFoundException ) spannerException );
297319 }
298320 // Set the exception to trigger an error for all waiters.
299- // Then retry the session creation if the error is (potentially) transient.
321+ // Then retry the session creation if the error is (potentially) transient and the
322+ // client has not been closed in the meantime.
300323 sessionReferenceFuture .setException (t );
301324 if (remainingAttempts > 1
325+ && !isClientClosed ()
302326 && RETRYABLE_ERROR_CODES .contains (spannerException .getErrorCode ())) {
303327 final SettableApiFuture <SessionReference > future = SettableApiFuture .create ();
304328 MultiplexedSessionDatabaseClient .this .multiplexedSessionReference .set (future );
@@ -363,6 +387,15 @@ boolean isValid() {
363387 return resourceNotFoundException .get () == null ;
364388 }
365389
390+ private boolean isClientClosed () {
391+ return isClosed ;
392+ }
393+
394+ private static SpannerException newClosedException () {
395+ return SpannerExceptionFactory .newSpannerException (
396+ ErrorCode .FAILED_PRECONDITION , "This client has been closed" );
397+ }
398+
366399 AtomicLong getNumSessionsAcquired () {
367400 return this .numSessionsAcquired ;
368401 }
@@ -371,6 +404,24 @@ AtomicLong getNumSessionsReleased() {
371404 return this .numSessionsReleased ;
372405 }
373406
407+ @ Override
408+ @ Nullable
409+ public String getChannelPrimeSessionName () {
410+ if (isClosed || !isValid ()) {
411+ return null ;
412+ }
413+ ApiFuture <SessionReference > future = multiplexedSessionReference .get ();
414+ if (!future .isDone () || future .isCancelled ()) {
415+ return null ;
416+ }
417+ try {
418+ SessionReference session = Futures .getDone (future );
419+ return isClosed || !isValid () ? null : session .getName ();
420+ } catch (CancellationException | ExecutionException e ) {
421+ return null ;
422+ }
423+ }
424+
374425 void close () {
375426 boolean releaseChannelUsage = false ;
376427 synchronized (this ) {
@@ -381,6 +432,9 @@ void close() {
381432 }
382433 }
383434 if (releaseChannelUsage ) {
435+ // The multiplexed session is no longer maintained, so this client must no longer be asked
436+ // for a session when priming dynamic channel pool channels.
437+ spanner .getRpc ().unregisterChannelPrimeSessionSource (this );
384438 synchronized (CHANNEL_USAGE ) {
385439 SharedChannelUsage sharedChannelUsage = CHANNEL_USAGE .get (this .spanner );
386440 if (sharedChannelUsage != null ) {
@@ -659,12 +713,19 @@ void maintain() {
659713 new SessionConsumer () {
660714 @ Override
661715 public void onSessionReady (SessionImpl session ) {
662- multiplexedSessionReference .set (
663- ApiFutures .immediateFuture (session .getSessionReference ()));
664- expirationDate .set (
665- clock
666- .instant ()
667- .plus (MultiplexedSessionDatabaseClient .this .sessionExpirationDuration ));
716+ synchronized (MultiplexedSessionDatabaseClient .this ) {
717+ if (isClosed ) {
718+ // The client was closed while the session was being refreshed. The refreshed
719+ // session belongs to a client that no longer exists and is ignored.
720+ return ;
721+ }
722+ multiplexedSessionReference .set (
723+ ApiFutures .immediateFuture (session .getSessionReference ()));
724+ expirationDate .set (
725+ clock
726+ .instant ()
727+ .plus (MultiplexedSessionDatabaseClient .this .sessionExpirationDuration ));
728+ }
668729 }
669730
670731 @ Override
0 commit comments