Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion DistributedLock.Core/DistributedLock.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
</PropertyGroup>

<PropertyGroup>
<Version>1.0.4</Version>
<Version>1.0.5</Version>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<Authors>Michael Adelson</Authors>
<Description>Core interfaces and utilities that support the DistributedLock.* family of packages</Description>
Expand All @@ -32,6 +32,9 @@
<TreatSpecificWarningsAsErrors />
<!-- see https://github.com/dotnet/sdk/issues/2679 -->
<DebugType>embedded</DebugType>
<!-- see https://mitchelsellers.com/blog/article/net-5-deterministic-builds-source-linking -->
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
Expand Down
11 changes: 6 additions & 5 deletions DistributedLock.Core/Internal/Data/ConnectionMonitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -253,9 +253,10 @@ private async ValueTask StopOrDisposeAsync(bool isDispose)
// 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

// If disposing, unsubscribe from state change tracking.
if (isDispose
&& this._stateChangedHandler != null
&& this._weakConnection.TryGetTarget(out var connection))
{
((DbConnection)connection.InnerConnection).StateChange -= this._stateChangedHandler;
Expand Down Expand Up @@ -424,7 +425,7 @@ public MonitoringHandle(ConnectionMonitor keepaliveHelper, CancellationToken can

private sealed class AlreadyCanceledHandle : IDatabaseConnectionMonitoringHandle
{
private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
private readonly CancellationTokenSource _cancellationTokenSource = new();

public AlreadyCanceledHandle()
{
Expand All @@ -438,7 +439,7 @@ public AlreadyCanceledHandle()

private sealed class NullHandle : IDatabaseConnectionMonitoringHandle
{
public static readonly NullHandle Instance = new NullHandle();
public static readonly NullHandle Instance = new();

private NullHandle() { }

Expand Down
3 changes: 3 additions & 0 deletions DistributedLock.FileSystem/DistributedLock.FileSystem.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
<TreatSpecificWarningsAsErrors />
<!-- see https://github.com/dotnet/sdk/issues/2679 -->
<DebugType>embedded</DebugType>
<!-- see https://mitchelsellers.com/blog/article/net-5-deterministic-builds-source-linking -->
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
Expand Down
5 changes: 4 additions & 1 deletion DistributedLock.MySql/DistributedLock.MySql.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
</PropertyGroup>

<PropertyGroup>
<Version>1.0.0</Version>
<Version>1.0.1</Version>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<Authors>Michael Adelson</Authors>
<Description>Provides a distributed lock implementation based on MySql</Description>
Expand All @@ -32,6 +32,9 @@
<TreatSpecificWarningsAsErrors />
<!-- see https://github.com/dotnet/sdk/issues/2679 -->
<DebugType>embedded</DebugType>
<!-- see https://mitchelsellers.com/blog/article/net-5-deterministic-builds-source-linking -->
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
Expand Down
5 changes: 4 additions & 1 deletion DistributedLock.Oracle/DistributedLock.Oracle.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
</PropertyGroup>

<PropertyGroup>
<Version>1.0.0</Version>
<Version>1.0.1</Version>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<Authors>Michael Adelson</Authors>
<Description>Provides a distributed lock implementation based on Oracle Database</Description>
Expand All @@ -32,6 +32,9 @@
<TreatSpecificWarningsAsErrors />
<!-- see https://github.com/dotnet/sdk/issues/2679 -->
<DebugType>embedded</DebugType>
<!-- see https://mitchelsellers.com/blog/article/net-5-deterministic-builds-source-linking -->
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
Expand Down
5 changes: 4 additions & 1 deletion DistributedLock.Postgres/DistributedLock.Postgres.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
</PropertyGroup>

<PropertyGroup>
<Version>1.0.2</Version>
<Version>1.0.3</Version>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<Authors>Michael Adelson</Authors>
<Description>Provides a distributed lock implementation based on Postgresql</Description>
Expand All @@ -32,6 +32,9 @@
<TreatSpecificWarningsAsErrors />
<!-- see https://github.com/dotnet/sdk/issues/2679 -->
<DebugType>embedded</DebugType>
<!-- see https://mitchelsellers.com/blog/article/net-5-deterministic-builds-source-linking -->
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
Expand Down
5 changes: 4 additions & 1 deletion DistributedLock.Redis/DistributedLock.Redis.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
</PropertyGroup>

<PropertyGroup>
<Version>1.0.1</Version>
<Version>1.0.2</Version>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<Authors>Michael Adelson</Authors>
<Description>Provides distributed locking primitives based on Redis</Description>
Expand All @@ -32,6 +32,9 @@
<TreatSpecificWarningsAsErrors />
<!-- see https://github.com/dotnet/sdk/issues/2679 -->
<DebugType>embedded</DebugType>
<!-- see https://mitchelsellers.com/blog/article/net-5-deterministic-builds-source-linking -->
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,12 +145,15 @@ internal static RedisDistributedLockOptions GetOptions(Action<RedisDistributedSy
TimeoutValue extensionCadence;
if (options?._extensionCadence is { } specifiedExtensionCadence)
{
// Note: we do not allow for disabling auto-extension here because it leads to traps
// where people might abandon the handle and then have it be closed due to GC.
// See discussion here: https://github.com/madelson/DistributedLock/issues/130.
if (specifiedExtensionCadence.CompareTo(minValidityTime) >= 0)
{
throw new ArgumentOutOfRangeException(
nameof(extensionCadence),
specifiedExtensionCadence.TimeSpan,
$"{nameof(extensionCadence)} must be less than {nameof(expiry)} ({expiry.TimeSpan}). To disable auto-extension, specify {nameof(Timeout)}.{nameof(Timeout.InfiniteTimeSpan)}"
$"{nameof(extensionCadence)} must be less than {nameof(expiry)} ({expiry.TimeSpan})"
);
}
extensionCadence = specifiedExtensionCadence;
Expand Down
5 changes: 4 additions & 1 deletion DistributedLock.SqlServer/DistributedLock.SqlServer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
</PropertyGroup>

<PropertyGroup>
<Version>1.0.1</Version>
<Version>1.0.2</Version>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<Authors>Michael Adelson</Authors>
<Description>Provides a distributed lock implementation based on SQL Server</Description>
Expand All @@ -32,6 +32,9 @@
<TreatSpecificWarningsAsErrors />
<!-- see https://github.com/dotnet/sdk/issues/2679 -->
<DebugType>embedded</DebugType>
<!-- see https://mitchelsellers.com/blog/article/net-5-deterministic-builds-source-linking -->
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
Expand Down
38 changes: 38 additions & 0 deletions DistributedLock.Tests/Tests/Core/Data/DatabaseConnectionTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
using Medallion.Threading.Internal.Data;
using Medallion.Threading.SqlServer;
using Medallion.Threading.Tests.SqlServer;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Medallion.Threading.Tests.Core.Data
{
public class DatabaseConnectionTest
{
/// <summary>
/// Reproduces the root cause of https://github.com/madelson/DistributedLock/issues/133
/// </summary>
[Test]
public async Task TestConnectionMonitorStaysSubscribedAfterClose()
{
var db = new TestingSqlServerDb { ApplicationName = nameof(TestConnectionMonitorStaysSubscribedAfterClose) };

await using var connection = new SqlDatabaseConnection(db.ConnectionString);

await connection.OpenAsync(CancellationToken.None);
connection.ConnectionMonitor.GetMonitoringHandle().Dispose(); // initialize monitoring
await connection.CloseAsync();

await connection.OpenAsync(CancellationToken.None);
using var handle = connection.ConnectionMonitor.GetMonitoringHandle();
Assert.IsFalse(handle.ConnectionLostToken.IsCancellationRequested);
await db.KillSessionsAsync(db.ApplicationName, idleSince: null);
Assert.IsTrue(await TestHelper.WaitForAsync(() => new(handle.ConnectionLostToken.IsCancellationRequested), timeout: TimeSpan.FromSeconds(5)));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

namespace Medallion.Threading.Tests.MySql
{
[Category("CI")]
public class MySqlConnectionOptionsBuilderTest
{
[Test]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

namespace Medallion.Threading.Tests.Oracle
{
[Category("CI")]
public class OracleConnectionOptionsBuilderTest
{
[Test]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

namespace Medallion.Threading.Tests.Postgres
{
[Category("CI")]
public class PostgresConnectionOptionsBuilderTest
{
[Test]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

namespace Medallion.Threading.Tests.SqlServer
{
[Category("CI")]
public class SqlConnectionOptionsBuilderTest
{
[Test]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Medallion.Threading.Tests.WaitHandles
Expand Down Expand Up @@ -47,7 +48,7 @@ public void TestMaxLengthNames()
public async Task TestGarbageCollection()
{
var @lock = CreateAsLock("gc_test", NameStyle.AddPrefix);
WeakReference AbandonLock() => new WeakReference(@lock.Acquire());
WeakReference AbandonLock() => new(@lock.Acquire());

var weakHandle = AbandonLock();
GC.Collect();
Expand Down Expand Up @@ -78,8 +79,70 @@ public void TestGetSafeLockNameCompat()
.ShouldEqual(@"Global\xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxsrCnXZ1XHiT//dOSBfAU0iC4Gtnlr0dQACBUK8Ev2OdRYJ9jcvbiqVCv/rjyPemTW9AvOonkdr0B2bG04gmeYA==");
}

/// <summary>
/// Attempts to reproduce https://github.com/madelson/DistributedLock/issues/120.
///
/// NOTE: in practice this race condition is so slim that to reproduce with any reliability requires
/// adding a call to Thread.Sleep(1) at the start of WaitHandleExtensions.Resignal().
/// </summary>
[Test]
public async Task TestCancellationDoesNotLeadToLostSignal([Values] bool async)
{
var semaphore = new WaitHandleDistributedSemaphore(nameof(this.TestCancellationDoesNotLeadToLostSignal), 2);
await using var _ = await semaphore.AcquireAsync(TimeSpan.FromSeconds(1));

Random random = new();
for (var i = 0; i < 50; ++i)
{
using var blockingHandle = semaphore.TryAcquire(TimeSpan.Zero); // claim the last slot on the semaphore
Assert.IsNotNull(blockingHandle);

using CancellationTokenSource source = new();

using SemaphoreSlim acquiringEvent = new(initialCount: 0, maxCount: 1);
var acquireTask = Task.Run(async () =>
{
try
{
if (async)
{
var acquireHandleTask = semaphore.AcquireAsync(TimeSpan.FromSeconds(30), source.Token);
acquiringEvent.Release();
(await acquireHandleTask).Dispose();
}
else
{
acquiringEvent.Release();
semaphore.Acquire(TimeSpan.FromSeconds(30), source.Token).Dispose();
}
}
catch (OperationCanceledException) { }
});
await acquiringEvent.WaitAsync();
Assert.IsFalse(acquireTask.IsCompleted);

using Barrier barrier = new(participantCount: 2);
var releaseTask = Task.Run(() =>
{
barrier.SignalAndWait();
blockingHandle!.Dispose();
});
var cancelTask = Task.Run(() =>
{
barrier.SignalAndWait();
var yieldCount = random.Next(5, 25);
for (var i = 0; i < yieldCount; ++i) { Thread.Yield(); }
source.Cancel();
});
await Task.WhenAll(acquireTask, releaseTask, cancelTask);
}

await using var handle = await semaphore.TryAcquireAsync();
Assert.IsNotNull(handle); // if we lost even a single signal due to cancellation in the loop above, this will fail
}

private static WaitHandleDistributedSemaphore CreateAsLock(string name, NameStyle nameStyle) =>
new WaitHandleDistributedSemaphore(
new(
nameStyle == NameStyle.AddPrefix ? DistributedWaitHandleHelpers.GlobalPrefix + name : name,
maxCount: 1,
abandonmentCheckCadence: TimeSpan.FromSeconds(.3),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
</PropertyGroup>

<PropertyGroup>
<Version>1.0.0</Version>
<Version>1.0.1</Version>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<Authors>Michael Adelson</Authors>
<Description>Provides a distributed lock implementation based on global WaitHandle objects in Windows</Description>
Expand All @@ -32,6 +32,9 @@
<TreatSpecificWarningsAsErrors />
<!-- see https://github.com/dotnet/sdk/issues/2679 -->
<DebugType>embedded</DebugType>
<!-- see https://mitchelsellers.com/blog/article/net-5-deterministic-builds-source-linking -->
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
Expand Down
Loading