From 19c0e6418c11553a73c86d9f6680f56304a78a16 Mon Sep 17 00:00:00 2001 From: Michael Adelson Date: Sat, 24 Apr 2021 10:27:16 -0400 Subject: [PATCH 1/6] Rename private method in MultiplexedConnectionLockPool for clarity --- .../Internal/Data/MultiplexedConnectionLockPool.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DistributedLock.Core/Internal/Data/MultiplexedConnectionLockPool.cs b/DistributedLock.Core/Internal/Data/MultiplexedConnectionLockPool.cs index a8fc6281..0e9c58a8 100644 --- a/DistributedLock.Core/Internal/Data/MultiplexedConnectionLockPool.cs +++ b/DistributedLock.Core/Internal/Data/MultiplexedConnectionLockPool.cs @@ -51,7 +51,7 @@ public MultiplexedConnectionLockPool(Func connection { // opportunistic phase: see if we can use a connection that is already holding a lock // to acquire the current lock - var existingLock = await this.GetOrCreateLockAsync(connectionString).ConfigureAwait(false); + var existingLock = await this.GetExistingLockOrDefaultAsync(connectionString).ConfigureAwait(false); if (existingLock != null) { var canSafelyDisposeExistingLock = false; @@ -101,7 +101,7 @@ public MultiplexedConnectionLockPool(Func connection @lock.TryAcquireAsync(name, timeout, strategy, keepaliveCadence, cancellationToken, opportunistic); } - private async ValueTask GetOrCreateLockAsync(string connectionString) + private async ValueTask GetExistingLockOrDefaultAsync(string connectionString) { using var _ = await this._lock.AcquireAsync(CancellationToken.None).ConfigureAwait(false); From 99afbe42558a3bc13748e6f74098165405fdfc4a Mon Sep 17 00:00:00 2001 From: Michael Adelson Date: Sat, 24 Apr 2021 13:25:39 -0400 Subject: [PATCH 2/6] Fix #85 and fix #61. Make all SqlServer tests use the default keepalive behavior by default. --- .../Internal/Data/ConnectionMonitor.cs | 25 ++++++++++++--- .../DistributedLock.Postgres.csproj | 2 +- .../Data/ConnectionStringStrategyTestCases.cs | 25 +++++++++++++++ .../Infrastructure/Data/ConnectionOptions.cs | 4 +-- .../Postgres/TestingPostgresProviders.cs | 7 +++-- .../SqlServer/TestingSqlServerProviders.cs | 7 +++-- .../Tests/Postgres/PostgresBehaviorTest.cs | 31 +++---------------- 7 files changed, 62 insertions(+), 39 deletions(-) diff --git a/DistributedLock.Core/Internal/Data/ConnectionMonitor.cs b/DistributedLock.Core/Internal/Data/ConnectionMonitor.cs index 67418930..e1a754cd 100644 --- a/DistributedLock.Core/Internal/Data/ConnectionMonitor.cs +++ b/DistributedLock.Core/Internal/Data/ConnectionMonitor.cs @@ -248,8 +248,12 @@ private async ValueTask StopOrDisposeAsync(bool isDispose) this.CloseOrCancelMonitoringHandleRegistrationsNoLock(isCancel: false); task = this._monitoringWorkerTask; - this._monitorStateChangedTokenSource?.Cancel(); + // Note: synchronous cancel here should be safe because we've already set + // the state to disposed above which the monitoring loop will check if it + // takes over the Cancel() thread. + this._monitorStateChangedTokenSource?.Cancel(); + // unsubscribe from state change tracking if (this._stateChangedHandler != null && this._weakConnection.TryGetTarget(out var connection)) @@ -304,7 +308,7 @@ private bool StartMonitorWorkerIfNeededNoLock() // skip if there's nothing to do if (this._keepaliveCadence.IsInfinite && !this.HasRegisteredMonitoringHandlesNoLock) { return false; } - + this._monitorStateChangedTokenSource = new CancellationTokenSource(); // Set up the task as a continuation on the previous task to avoid concurrency in the case where the previous // one is spinning down. If we change states in rapid succession we could end up with multiple tasks queued up @@ -318,9 +322,20 @@ private bool StartMonitorWorkerIfNeededNoLock() private void FireStateChangedNoLock() { - this._monitorStateChangedTokenSource!.Cancel(); - this._monitorStateChangedTokenSource.Dispose(); + var monitorStateChangedTokenSource = this._monitorStateChangedTokenSource!; this._monitorStateChangedTokenSource = new CancellationTokenSource(); + // Canceling asynchronously is important because the Cancel() thread can end up + // running continuations inside the monitoring loop (e. g. see + // https://github.com/madelson/DistributedLock/issues/85). Now that we set the new + // token source before canceling the old one we should avoid that particular issue, but + // it is still safer and easier to reason about not to have that happen. This also ensures + // that FireStateChangedNoLock() always returns quickly, even if the monitoring loop + // were to do some synchronous work on the continuation thread. + Task.Run(() => + { + try { monitorStateChangedTokenSource.Cancel(); } + finally { monitorStateChangedTokenSource.Dispose(); } + }); } private async Task MonitorWorkerLoop() @@ -357,7 +372,7 @@ private async Task DoMonitoringAsync(CancellationToken cancellationToken) using var _ = await this._connectionLock.AcquireAsync(CancellationToken.None).ConfigureAwait(false); // 1-min increments is kind of an arbitrary choice. We want to avoid this being too short since each time - // we "come up to breathe" that's a waste of resource. We also want to avoid this being too long since + // we "come up to breathe" that's a waste of resources. We also want to avoid this being too long since // in case people have some kind of monitoring set up for hanging queries await connection.SleepAsync( sleepTime: TimeSpan.FromMinutes(1), diff --git a/DistributedLock.Postgres/DistributedLock.Postgres.csproj b/DistributedLock.Postgres/DistributedLock.Postgres.csproj index 1ac6b661..7c4565a0 100644 --- a/DistributedLock.Postgres/DistributedLock.Postgres.csproj +++ b/DistributedLock.Postgres/DistributedLock.Postgres.csproj @@ -41,7 +41,7 @@ - + diff --git a/DistributedLock.Tests/AbstractTestCases/Data/ConnectionStringStrategyTestCases.cs b/DistributedLock.Tests/AbstractTestCases/Data/ConnectionStringStrategyTestCases.cs index 84b42607..0e943542 100644 --- a/DistributedLock.Tests/AbstractTestCases/Data/ConnectionStringStrategyTestCases.cs +++ b/DistributedLock.Tests/AbstractTestCases/Data/ConnectionStringStrategyTestCases.cs @@ -3,6 +3,7 @@ using System; using System.Diagnostics; using System.Threading; +using System.Threading.Tasks; namespace Medallion.Threading.Tests.Data { @@ -95,5 +96,29 @@ public void TestKeepaliveDoesNotCreateRaceCondition() } }); } + + // replicates issue from https://github.com/madelson/DistributedLock/issues/85 + [Test] + public async Task TestAccessingHandleLostTokenWhileKeepaliveActiveDoesNotBlock() + { + this._lockProvider.Strategy.KeepaliveCadence = TimeSpan.FromMinutes(5); + + var @lock = this._lockProvider.CreateLock(string.Empty); + var handle = await @lock.TryAcquireAsync(); + if (handle != null) + { + var accessHandleLostTokenTask = Task.Run(() => + { + if (handle.HandleLostToken.CanBeCanceled) + { + handle.HandleLostToken.Register(() => { }); + } + }); + Assert.IsTrue(await accessHandleLostTokenTask.WaitAsync(TimeSpan.FromSeconds(5))); + + // do this only on success; on failure we're likely deadlocked and dispose will hang + await handle.DisposeAsync(); + } + } } } diff --git a/DistributedLock.Tests/Infrastructure/Data/ConnectionOptions.cs b/DistributedLock.Tests/Infrastructure/Data/ConnectionOptions.cs index 7044ebe4..811680e4 100644 --- a/DistributedLock.Tests/Infrastructure/Data/ConnectionOptions.cs +++ b/DistributedLock.Tests/Infrastructure/Data/ConnectionOptions.cs @@ -20,13 +20,13 @@ public sealed class TestingDbConnectionOptions public DbTransaction? Transaction { get; set; } public T Create( - Func fromConnectionString, + Func fromConnectionString, Func fromConnection, Func fromTransaction) { if (this.ConnectionString != null) { - return fromConnectionString(this.ConnectionString, (this.ConnectionStringUseMultiplexing, this.ConnectionStringUseTransaction, this.ConnectionStringKeepaliveCadence ?? Timeout.InfiniteTimeSpan)); + return fromConnectionString(this.ConnectionString, (this.ConnectionStringUseMultiplexing, this.ConnectionStringUseTransaction, this.ConnectionStringKeepaliveCadence)); } if (this.Connection != null) diff --git a/DistributedLock.Tests/Infrastructure/Postgres/TestingPostgresProviders.cs b/DistributedLock.Tests/Infrastructure/Postgres/TestingPostgresProviders.cs index 3dfd6b78..121c4861 100644 --- a/DistributedLock.Tests/Infrastructure/Postgres/TestingPostgresProviders.cs +++ b/DistributedLock.Tests/Infrastructure/Postgres/TestingPostgresProviders.cs @@ -23,8 +23,11 @@ public override IDistributedLock CreateLockWithExactName(string name) => public override string GetSafeName(string name) => new PostgresAdvisoryLockKey(name, allowHashing: true).ToString(); - internal static Action ToPostgresOptions((bool useMultiplexing, bool useTransaction, TimeSpan keepaliveCadence) options) => - o => o.UseMultiplexing(options.useMultiplexing).KeepaliveCadence(options.keepaliveCadence); + internal static Action ToPostgresOptions((bool useMultiplexing, bool useTransaction, TimeSpan? keepaliveCadence) options) => o => + { + o.UseMultiplexing(options.useMultiplexing); + if (options.keepaliveCadence is { } keepaliveCadence) { o.KeepaliveCadence(keepaliveCadence); } + }; } public sealed class TestingPostgresDistributedReaderWriterLockProvider : TestingReaderWriterLockProvider diff --git a/DistributedLock.Tests/Infrastructure/SqlServer/TestingSqlServerProviders.cs b/DistributedLock.Tests/Infrastructure/SqlServer/TestingSqlServerProviders.cs index f0848237..e4151b46 100644 --- a/DistributedLock.Tests/Infrastructure/SqlServer/TestingSqlServerProviders.cs +++ b/DistributedLock.Tests/Infrastructure/SqlServer/TestingSqlServerProviders.cs @@ -19,8 +19,11 @@ public override IDistributedLock CreateLockWithExactName(string name) => public override string GetSafeName(string name) => SqlDistributedLock.GetSafeName(name); - internal static Action ToSqlOptions((bool useMultiplexing, bool useTransaction, TimeSpan keepaliveCadence) options) => - o => o.UseMultiplexing(options.useMultiplexing).UseTransaction(options.useTransaction).KeepaliveCadence(options.keepaliveCadence); + internal static Action ToSqlOptions((bool useMultiplexing, bool useTransaction, TimeSpan? keepaliveCadence) options) => o => + { + o.UseMultiplexing(options.useMultiplexing).UseTransaction(options.useTransaction); + if (options.keepaliveCadence is { } keepaliveCadence) { o.KeepaliveCadence(keepaliveCadence); } + }; } public sealed class TestingSqlDistributedReaderWriterLockProvider : TestingUpgradeableReaderWriterLockProvider diff --git a/DistributedLock.Tests/Tests/Postgres/PostgresBehaviorTest.cs b/DistributedLock.Tests/Tests/Postgres/PostgresBehaviorTest.cs index cb46af58..34ce2866 100644 --- a/DistributedLock.Tests/Tests/Postgres/PostgresBehaviorTest.cs +++ b/DistributedLock.Tests/Tests/Postgres/PostgresBehaviorTest.cs @@ -89,7 +89,8 @@ async Task RunTransactionWithAbortAsync(bool useSavePoint) if (useTimeout) { command.CommandText = "SET LOCAL statement_timeout = 100; " + command.CommandText; } else { cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(.5)); } - Assert.ThrowsAsync(() => command.ExecuteNonQueryAsync(cancellationTokenSource.Token)); + var exception = Assert.CatchAsync(() => command.ExecuteNonQueryAsync(cancellationTokenSource.Token)); + Assert.IsInstanceOf(useTimeout ? typeof(PostgresException) : typeof(OperationCanceledException), exception); if (useSavePoint) { @@ -143,31 +144,7 @@ public async Task TestDoesNotDetectConnectionBreakViaState() Assert.IsTrue(stateChangedEvent.Wait(TimeSpan.FromSeconds(5))); } - // replicates https://github.com/npgsql/npgsql/issues/2912 - [Test] - public async Task TestPrepareThrowsNullReferenceExceptionOnTerminatedConnection() - { - using var connection = new NpgsqlConnection(TestingPostgresDb.ConnectionString); - await connection.OpenAsync(); - - using var getPidCommand = connection.CreateCommand(); - getPidCommand.CommandText = "SELECT pg_backend_pid()"; - var pid = (int)(await getPidCommand.ExecuteScalarAsync()); - - // kill the connection from the back end - using var killingConnection = new NpgsqlConnection(TestingPostgresDb.ConnectionString); - await killingConnection.OpenAsync(); - using var killCommand = killingConnection.CreateCommand(); - killCommand.CommandText = $"SELECT pg_terminate_backend({pid})"; - await killCommand.ExecuteNonQueryAsync(); - - await Task.Delay(10); - - Assert.ThrowsAsync(() => getPidCommand.PrepareAsync()); - } - - // See https://github.com/npgsql/npgsql/issues/3442. Until that's resolved, connection monitoring - // is broken in 5.x + // Effective test for https://github.com/npgsql/npgsql/issues/3442, which broke monitoring [Test] public async Task TestExecutingQueryOnKilledConnectionFiresStateChanged() { @@ -193,7 +170,7 @@ public async Task TestExecutingQueryOnKilledConnectionFiresStateChanged() Assert.ThrowsAsync(() => getPidCommand.ExecuteScalarAsync()); Assert.AreNotEqual(ConnectionState.Open, connection.State); - Assert.IsTrue(stateChangedEvent.Wait(TimeSpan.FromSeconds(5))); // assertion passes in 4.1.4, fails in 5.0.1.1 + Assert.IsTrue(stateChangedEvent.Wait(TimeSpan.FromSeconds(5))); } } } From 1d4a526847f31665045a3e59b01bcce6bd4bf0cf Mon Sep 17 00:00:00 2001 From: Michael Adelson Date: Sat, 24 Apr 2021 15:26:38 -0400 Subject: [PATCH 3/6] Ensure that broken connection does not pollute the multiplexed connection pool. Fix #83 --- .../Internal/Data/DatabaseConnection.cs | 16 ++++---- .../Data/MultiplexedConnectionLock.cs | 39 +++++++++++++++---- .../Data/MultiplexedConnectionLockPool.cs | 1 + ...MultiplexingConnectionStrategyTestCases.cs | 27 +++++++++++++ .../Postgres/TestingPostgresDb.cs | 2 +- 5 files changed, 69 insertions(+), 16 deletions(-) diff --git a/DistributedLock.Core/Internal/Data/DatabaseConnection.cs b/DistributedLock.Core/Internal/Data/DatabaseConnection.cs index 2bfbe254..00a320c4 100644 --- a/DistributedLock.Core/Internal/Data/DatabaseConnection.cs +++ b/DistributedLock.Core/Internal/Data/DatabaseConnection.cs @@ -112,14 +112,14 @@ private async ValueTask DisposeOrCloseAsync(bool isDispose) finally { #if NETSTANDARD2_1 - if (!SyncViaAsync.IsSynchronous && this.InnerConnection is DbConnection dbConnection) - { - await (isDispose ? dbConnection.DisposeAsync() : dbConnection.CloseAsync().AsValueTask()).ConfigureAwait(false); - } - else - { - SyncDisposeConnection(); - } + if (!SyncViaAsync.IsSynchronous && this.InnerConnection is DbConnection dbConnection) + { + await (isDispose ? dbConnection.DisposeAsync() : dbConnection.CloseAsync().AsValueTask()).ConfigureAwait(false); + } + else + { + SyncDisposeConnection(); + } #elif NETSTANDARD2_0 || NET461 SyncDisposeConnection(); #else diff --git a/DistributedLock.Core/Internal/Data/MultiplexedConnectionLock.cs b/DistributedLock.Core/Internal/Data/MultiplexedConnectionLock.cs index 991c42b9..153fd7c4 100644 --- a/DistributedLock.Core/Internal/Data/MultiplexedConnectionLock.cs +++ b/DistributedLock.Core/Internal/Data/MultiplexedConnectionLock.cs @@ -18,12 +18,20 @@ internal sealed class MultiplexedConnectionLock : IAsyncDisposable private readonly AsyncLock _mutex = AsyncLock.Create(); private readonly Dictionary _heldLocksToKeepaliveCadences = new Dictionary(); private readonly DatabaseConnection _connection; + /// + /// Tracks whether we've successfully opened the connection. We track this explicity instead of just looking at + /// because we want to make sure we close() explicitly for every + /// open() and also we want to make sure we do not try to re-open a broken connection. + /// + private bool _connectionOpened; public MultiplexedConnectionLock(DatabaseConnection connection) { this._connection = connection; } + private bool IsConnectionBrokenNoLock => this._connectionOpened && !this._connection.CanExecuteQueries; + public async ValueTask TryAcquireAsync( string name, TimeoutValue timeout, @@ -33,8 +41,8 @@ public async ValueTask TryAcquireAsync( bool opportunistic) where TLockCookie : class { - using var mutextHandle = await this._mutex.TryAcquireAsync(opportunistic ? TimeSpan.Zero : Timeout.InfiniteTimeSpan, cancellationToken).ConfigureAwait(false); - if (mutextHandle == null) + using var mutexHandle = await this._mutex.TryAcquireAsync(opportunistic ? TimeSpan.Zero : Timeout.InfiniteTimeSpan, cancellationToken).ConfigureAwait(false); + if (mutexHandle == null) { // mutex wasn't free, so just give up Invariant.Require(opportunistic); @@ -43,6 +51,10 @@ public async ValueTask TryAcquireAsync( return new Result(MultiplexedConnectionLockRetry.Retry, canSafelyDispose: false); } + // This is technically redundant with the similar catch block below, but avoids needing to have + // to attempt a query on a connection that we know is broken. + if (opportunistic && this.IsConnectionBrokenNoLock) { return this.GetAlreadyBrokenResultNoLock(); } + try { if (this._heldLocksToKeepaliveCadences.ContainsKey(name)) @@ -53,9 +65,10 @@ public async ValueTask TryAcquireAsync( return this.GetFailureResultNoLock(isAlreadyHeld: true, opportunistic, timeout); } - if (!this._connection.CanExecuteQueries) + if (!this._connectionOpened) { await this._connection.OpenAsync(cancellationToken).ConfigureAwait(false); + this._connectionOpened = true; } var lockCookie = await strategy.TryAcquireAsync(this._connection, name, opportunistic ? TimeSpan.Zero : timeout, cancellationToken).ConfigureAwait(false); @@ -71,6 +84,11 @@ public async ValueTask TryAcquireAsync( // shortened the timeout return this.GetFailureResultNoLock(isAlreadyHeld: false, opportunistic, timeout); } + // never punish for the connection being broken already (see https://github.com/madelson/DistributedLock/issues/83) + catch when (opportunistic && this.IsConnectionBrokenNoLock) + { + return this.GetAlreadyBrokenResultNoLock(); + } finally { await this.CloseConnectionIfNeededNoLockAsync().ConfigureAwait(false); @@ -90,6 +108,11 @@ public async ValueTask GetIsInUseAsync() return mutexHandle == null || this._heldLocksToKeepaliveCadences.Count != 0; } + private Result GetAlreadyBrokenResultNoLock() => + // Retry on any already-broken connection to avoid "leaking" the killing or death of connections. We want there to be no observable + // results (other than perf) of multiplexing vs. not. + new Result(MultiplexedConnectionLockRetry.Retry, canSafelyDispose: this._heldLocksToKeepaliveCadences.Count == 0); + private Result GetFailureResultNoLock(bool isAlreadyHeld, bool opportunistic, TimeoutValue timeout) { // only opportunistic acquisitions trigger retries @@ -151,11 +174,13 @@ private async ValueTask ReleaseAsync(IDbSynchronizationStrategy connection try { result = await TryAcquireAsync(@lock, opportunistic: false).ConfigureAwait(false); + Invariant.Require(result!.Value.Retry == MultiplexedConnectionLockRetry.NoRetry, "Acquire on fresh lock should not recommend a retry"); } finally { diff --git a/DistributedLock.Tests/AbstractTestCases/Data/MultiplexingConnectionStrategyTestCases.cs b/DistributedLock.Tests/AbstractTestCases/Data/MultiplexingConnectionStrategyTestCases.cs index 924f3da5..1c7aeae0 100644 --- a/DistributedLock.Tests/AbstractTestCases/Data/MultiplexingConnectionStrategyTestCases.cs +++ b/DistributedLock.Tests/AbstractTestCases/Data/MultiplexingConnectionStrategyTestCases.cs @@ -2,6 +2,7 @@ using NUnit.Framework; using System; using System.Collections.Generic; +using System.Data.Common; using System.Diagnostics; using System.Runtime.CompilerServices; using System.Threading; @@ -122,5 +123,31 @@ async Task Test() string MakeLockName(int i) => $"{nameof(TestHighConcurrencyWithSmallPool)}_{i}"; } + + [Test] + public async Task TestBrokenConnectionDoesNotCorruptPool() + { + // This makes sure that for the Semaphore5 lock initial 4 tickets are taken with the default + // application name and therefore won't be killed + this._lockProvider.CreateLock("1"); + this._lockProvider.CreateLock("2"); + var applicationName = this._lockProvider.Strategy.SetUniqueApplicationName(); + + var lock1 = this._lockProvider.CreateLock("1"); + await using var handle1 = await lock1.AcquireAsync(); + + // kill the session + await this._lockProvider.Strategy.Db.KillSessionsAsync(applicationName); + + var lock2 = this._lockProvider.CreateLock("2"); + Assert.DoesNotThrowAsync(async () => await (await lock2.AcquireAsync()).DisposeAsync()); + + await using var handle2 = await lock2.AcquireAsync(); + Assert.DoesNotThrow(() => lock2.TryAcquire()?.Dispose()); + + Assert.Catch(() => handle1.Dispose()); + + Assert.DoesNotThrowAsync(async () => await (await lock1.AcquireAsync()).DisposeAsync()); + } } } diff --git a/DistributedLock.Tests/Infrastructure/Postgres/TestingPostgresDb.cs b/DistributedLock.Tests/Infrastructure/Postgres/TestingPostgresDb.cs index 74d90019..c4d221b3 100644 --- a/DistributedLock.Tests/Infrastructure/Postgres/TestingPostgresDb.cs +++ b/DistributedLock.Tests/Infrastructure/Postgres/TestingPostgresDb.cs @@ -43,7 +43,7 @@ public int CountActiveSessions(string applicationName) using var command = connection.CreateCommand(); command.CommandText = "SELECT COUNT(*)::int FROM pg_stat_activity WHERE application_name = @applicationName"; command.Parameters.AddWithValue("applicationName", applicationName); - return (int)command.ExecuteScalar(); + return (int)command.ExecuteScalar()!; } public IsolationLevel GetIsolationLevel(DbConnection connection) From f581dd2c74cca7ded847d769cc6d3628653f1fd5 Mon Sep 17 00:00:00 2001 From: Michael Adelson Date: Sat, 24 Apr 2021 15:34:19 -0400 Subject: [PATCH 4/6] 2.0.2 version bumps --- DistributedLock.Core/DistributedLock.Core.csproj | 2 +- DistributedLock.Postgres/DistributedLock.Postgres.csproj | 2 +- DistributedLock.SqlServer/DistributedLock.SqlServer.csproj | 2 +- DistributedLock/DistributedLock.csproj | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/DistributedLock.Core/DistributedLock.Core.csproj b/DistributedLock.Core/DistributedLock.Core.csproj index 54a90171..e5c76502 100644 --- a/DistributedLock.Core/DistributedLock.Core.csproj +++ b/DistributedLock.Core/DistributedLock.Core.csproj @@ -10,7 +10,7 @@ - 1.0.0 + 1.0.1 1.0.0.0 Michael Adelson Core interfaces and utilities that support the DistributedLock.* family of packages diff --git a/DistributedLock.Postgres/DistributedLock.Postgres.csproj b/DistributedLock.Postgres/DistributedLock.Postgres.csproj index 7c4565a0..c16ea55e 100644 --- a/DistributedLock.Postgres/DistributedLock.Postgres.csproj +++ b/DistributedLock.Postgres/DistributedLock.Postgres.csproj @@ -10,7 +10,7 @@ - 1.0.0 + 1.0.1 1.0.0.0 Michael Adelson Provides a distributed lock implementation based on Postgresql diff --git a/DistributedLock.SqlServer/DistributedLock.SqlServer.csproj b/DistributedLock.SqlServer/DistributedLock.SqlServer.csproj index 2ce82baa..5eac9c35 100644 --- a/DistributedLock.SqlServer/DistributedLock.SqlServer.csproj +++ b/DistributedLock.SqlServer/DistributedLock.SqlServer.csproj @@ -10,7 +10,7 @@ - 1.0.0 + 1.0.1 1.0.0.0 Michael Adelson Provides a distributed lock implementation based on SQL Server diff --git a/DistributedLock/DistributedLock.csproj b/DistributedLock/DistributedLock.csproj index 2660947c..644e26ad 100644 --- a/DistributedLock/DistributedLock.csproj +++ b/DistributedLock/DistributedLock.csproj @@ -10,7 +10,7 @@ - 2.0.1 + 2.0.2 2.0.0.0 Michael Adelson Provides easy-to-use mutexes, reader-writer locks, and semaphores that can synchronize across processes and machines. This is an umbrella package that brings in the entire family of DistributedLock.* packages (e. g. DistributedLock.SqlServer) as references. Those packages can also be installed individually. From 3925748babc0abf5498c8a739bcfd9e8616c32ce Mon Sep 17 00:00:00 2001 From: Michael Adelson Date: Sat, 24 Apr 2021 16:31:26 -0400 Subject: [PATCH 5/6] Update release notes for 2.0.2 --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 53778db3..6ba35b18 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,10 @@ public class SomeService Contributions are welcome! If you are interested in contributing towards a new or existing issue, please let me know via comments on the issue so that I can help you get started and avoid wasted effort on your part. ## Release notes +- 2.0.2 + - Fixed bug where `HandleLostToken` would hang when accessed on a SqlServer or Postgres lock handle that used keepalive ([#85](https://github.com/madelson/DistributedLock/issues/85), DistributedLock.Core 1.0.1) + - Fixed bug where broken database connections could result in future lock attempts failing when using SqlServer or Postgres locks with multiplexing ([#83](https://github.com/madelson/DistributedLock/issues/83), DistributedLock.Core 1.0.1) + - Updated Npgsql dependency to 5.x to take advantage of various bugfixes ([#61](https://github.com/madelson/DistributedLock/issues/61), DistributedLock.Postgres 1.0.1) - 2.0.1 - Fixed Redis lock behavior when using a database with `WithKeyPrefix` (#66, DistributedLock.Redis 1.0.1). Thanks @skomis-mm for contributing! - 2.0.0 (see also [Migrating from 1.x to 2.x](docs/Migrating%20from%201.x%20to%202.x.md#migrating-from-1x-to-2x)) From a60febb71821e557f305a3d91aeb5ecc58bbc3f6 Mon Sep 17 00:00:00 2001 From: Michael Adelson Date: Sat, 24 Apr 2021 16:33:06 -0400 Subject: [PATCH 6/6] Minor readme fix --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6ba35b18..9a264159 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ DistributedLock is a .NET library that provides robust and easy-to-use distribut With DistributedLock, synchronizing access to a region of code across multiple applications/machines is as simple as: ```C# -using (await myDistributedLock.AcquireAsync()) +await using (await myDistributedLock.AcquireAsync()) { // I hold the lock here }