@@ -18,12 +18,20 @@ internal sealed class MultiplexedConnectionLock : IAsyncDisposable
1818 private readonly AsyncLock _mutex = AsyncLock . Create ( ) ;
1919 private readonly Dictionary < string , TimeoutValue > _heldLocksToKeepaliveCadences = new Dictionary < string , TimeoutValue > ( ) ;
2020 private readonly DatabaseConnection _connection ;
21+ /// <summary>
22+ /// Tracks whether we've successfully opened the connection. We track this explicity instead of just looking at
23+ /// <see cref="DatabaseConnection.CanExecuteQueries"/> because we want to make sure we close() explicitly for every
24+ /// open() and also we want to make sure we do not try to re-open a broken connection.
25+ /// </summary>
26+ private bool _connectionOpened ;
2127
2228 public MultiplexedConnectionLock ( DatabaseConnection connection )
2329 {
2430 this . _connection = connection ;
2531 }
2632
33+ private bool IsConnectionBrokenNoLock => this . _connectionOpened && ! this . _connection . CanExecuteQueries ;
34+
2735 public async ValueTask < Result > TryAcquireAsync < TLockCookie > (
2836 string name ,
2937 TimeoutValue timeout ,
@@ -33,8 +41,8 @@ public async ValueTask<Result> TryAcquireAsync<TLockCookie>(
3341 bool opportunistic )
3442 where TLockCookie : class
3543 {
36- using var mutextHandle = await this . _mutex . TryAcquireAsync ( opportunistic ? TimeSpan . Zero : Timeout . InfiniteTimeSpan , cancellationToken ) . ConfigureAwait ( false ) ;
37- if ( mutextHandle == null )
44+ using var mutexHandle = await this . _mutex . TryAcquireAsync ( opportunistic ? TimeSpan . Zero : Timeout . InfiniteTimeSpan , cancellationToken ) . ConfigureAwait ( false ) ;
45+ if ( mutexHandle == null )
3846 {
3947 // mutex wasn't free, so just give up
4048 Invariant . Require ( opportunistic ) ;
@@ -43,6 +51,10 @@ public async ValueTask<Result> TryAcquireAsync<TLockCookie>(
4351 return new Result ( MultiplexedConnectionLockRetry . Retry , canSafelyDispose : false ) ;
4452 }
4553
54+ // This is technically redundant with the similar catch block below, but avoids needing to have
55+ // to attempt a query on a connection that we know is broken.
56+ if ( opportunistic && this . IsConnectionBrokenNoLock ) { return this . GetAlreadyBrokenResultNoLock ( ) ; }
57+
4658 try
4759 {
4860 if ( this . _heldLocksToKeepaliveCadences . ContainsKey ( name ) )
@@ -53,9 +65,10 @@ public async ValueTask<Result> TryAcquireAsync<TLockCookie>(
5365 return this . GetFailureResultNoLock ( isAlreadyHeld : true , opportunistic , timeout ) ;
5466 }
5567
56- if ( ! this . _connection . CanExecuteQueries )
68+ if ( ! this . _connectionOpened )
5769 {
5870 await this . _connection . OpenAsync ( cancellationToken ) . ConfigureAwait ( false ) ;
71+ this . _connectionOpened = true ;
5972 }
6073
6174 var lockCookie = await strategy . TryAcquireAsync ( this . _connection , name , opportunistic ? TimeSpan . Zero : timeout , cancellationToken ) . ConfigureAwait ( false ) ;
@@ -71,6 +84,11 @@ public async ValueTask<Result> TryAcquireAsync<TLockCookie>(
7184 // shortened the timeout
7285 return this . GetFailureResultNoLock ( isAlreadyHeld : false , opportunistic , timeout ) ;
7386 }
87+ // never punish for the connection being broken already (see https://github.com/madelson/DistributedLock/issues/83)
88+ catch when ( opportunistic && this . IsConnectionBrokenNoLock )
89+ {
90+ return this . GetAlreadyBrokenResultNoLock ( ) ;
91+ }
7492 finally
7593 {
7694 await this . CloseConnectionIfNeededNoLockAsync ( ) . ConfigureAwait ( false ) ;
@@ -90,6 +108,11 @@ public async ValueTask<bool> GetIsInUseAsync()
90108 return mutexHandle == null || this . _heldLocksToKeepaliveCadences . Count != 0 ;
91109 }
92110
111+ private Result GetAlreadyBrokenResultNoLock ( ) =>
112+ // Retry on any already-broken connection to avoid "leaking" the killing or death of connections. We want there to be no observable
113+ // results (other than perf) of multiplexing vs. not.
114+ new Result ( MultiplexedConnectionLockRetry . Retry , canSafelyDispose : this . _heldLocksToKeepaliveCadences . Count == 0 ) ;
115+
93116 private Result GetFailureResultNoLock ( bool isAlreadyHeld , bool opportunistic , TimeoutValue timeout )
94117 {
95118 // only opportunistic acquisitions trigger retries
@@ -151,11 +174,13 @@ private async ValueTask ReleaseAsync<TLockCookie>(IDbSynchronizationStrategy<TLo
151174 }
152175 }
153176
154- private ValueTask CloseConnectionIfNeededNoLockAsync ( )
177+ private async ValueTask CloseConnectionIfNeededNoLockAsync ( )
155178 {
156- return this . _heldLocksToKeepaliveCadences . Count == 0 && this . _connection . CanExecuteQueries
157- ? this . _connection . CloseAsync ( )
158- : default ;
179+ if ( this . _connectionOpened && this . _heldLocksToKeepaliveCadences . Count == 0 )
180+ {
181+ await this . _connection . CloseAsync ( ) . ConfigureAwait ( false ) ;
182+ this . _connectionOpened = false ;
183+ }
159184 }
160185
161186 private void SetKeepaliveCadenceNoLock ( )
0 commit comments