Skip to content

Commit 18fa8a5

Browse files
committed
Added readerwriter lock class based on sp_getapplock
1 parent 702dab3 commit 18fa8a5

21 files changed

+967
-72
lines changed

DistributedLock.Tests/DistributedLock.Tests.csproj

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@
3535
<WarningLevel>4</WarningLevel>
3636
</PropertyGroup>
3737
<ItemGroup>
38-
<Reference Include="MedallionShell, Version=1.0.2.0, Culture=neutral, processorArchitecture=MSIL">
39-
<HintPath>..\packages\MedallionShell.1.0.2\lib\net45\MedallionShell.dll</HintPath>
38+
<Reference Include="MedallionShell, Version=1.2.1.0, Culture=neutral, processorArchitecture=MSIL">
39+
<HintPath>..\packages\MedallionShell.1.2.1\lib\net45\MedallionShell.dll</HintPath>
4040
<Private>True</Private>
4141
</Reference>
4242
<Reference Include="System" />
@@ -59,6 +59,8 @@
5959
<Compile Include="Properties\AssemblyInfo.cs" />
6060
<Compile Include="Sql\SqlConnectionScopedDistributedLockTest.cs" />
6161
<Compile Include="Sql\SqlDistributedLockTest.cs" />
62+
<Compile Include="Sql\SqlDistributedReaderWriterLockTests.cs" />
63+
<Compile Include="Sql\SqlDistributedReaderWriterLockTestBase.cs" />
6264
<Compile Include="Sql\SqlOptimisticConnectionPoolingDistributedLockTest.cs" />
6365
<Compile Include="Sql\SqlOwnedConnectionDistributedLockTest.cs" />
6466
<Compile Include="Sql\SqlOwnedTransactionDistributedLockTest.cs" />

DistributedLock.Tests/DistributedLockTestBase.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public void BasicTest()
2323

2424
using (var nestedHandle = @lock.TryAcquire())
2525
{
26-
(nestedHandle == null).ShouldEqual(!this.IsReentrant);
26+
(nestedHandle == null).ShouldEqual(!this.IsReentrant, this.GetType() + ": reentrancy mis-stated");
2727
}
2828

2929
using (var nestedHandle2 = lock2.TryAcquire())
@@ -211,7 +211,7 @@ public void TestCrossProcess()
211211
var type = this.CreateLock("a").GetType().Name.Replace("DistributedLock", string.Empty).ToLowerInvariant();
212212

213213
var command = this.RunLockTaker(type, "cpl");
214-
command.Task.Wait(TimeSpan.FromSeconds(.5)).ShouldEqual(false);
214+
command.Task.Wait(TimeSpan.FromSeconds(.5)).ShouldEqual(false, this.GetType().ToString());
215215

216216
var @lock = this.CreateLock("cpl");
217217
@lock.TryAcquire().ShouldEqual(null);
@@ -255,7 +255,7 @@ private void CrossProcessAbandonmentHelper(bool asyncWait, bool kill)
255255

256256
var name = "cpl-" + asyncWait + "-" + kill;
257257
var command = this.RunLockTaker(type, name);
258-
command.Task.Wait(TimeSpan.FromSeconds(.5)).ShouldEqual(false);
258+
command.Task.Wait(TimeSpan.FromSeconds(.5)).ShouldEqual(false, this.GetType().ToString());
259259

260260
var @lock = this.CreateLock(name);
261261

@@ -282,7 +282,7 @@ private void CrossProcessAbandonmentHelper(bool asyncWait, bool kill)
282282

283283
private Command RunLockTaker(params string[] args)
284284
{
285-
var command = Command.Run("DistributedLockTaker", args);
285+
var command = Command.Run("DistributedLockTaker", args, o => o.ThrowOnError(true));
286286
this.AddCleanupAction(() =>
287287
{
288288
if (!command.Task.IsCompleted)

DistributedLock.Tests/Sql/SqlConnectionScopedDistributedLockTest.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ internal override IDistributedLock CreateLock(string name)
5959
SqlConnection.ClearAllPools();
6060

6161
var connection = new SqlConnection(SqlDistributedLockTest.ConnectionString);
62-
connection.Open();;
62+
connection.Open();
6363
this.AddCleanupAction(connection.Dispose);
6464
return new SqlDistributedLock(name, connection);
6565
}
@@ -69,6 +69,8 @@ internal override string GetSafeLockName(string name)
6969
return SqlDistributedLock.GetSafeLockName(name);
7070
}
7171

72-
internal override bool IsReentrant { get { return true; } }
72+
internal override bool IsReentrant => true;
73+
// from my testing, it appears that abandoning a SqlConnection does not cause it to be released
74+
internal override bool SupportsInProcessAbandonment => false;
7375
}
7476
}

DistributedLock.Tests/Sql/SqlDistributedLockTest.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ public void TestBadConstructorArguments()
3131
TestHelper.AssertThrows<ArgumentNullException>(() => new SqlDistributedLock("a", default(string)));
3232
TestHelper.AssertThrows<ArgumentNullException>(() => new SqlDistributedLock("a", default(DbTransaction)));
3333
TestHelper.AssertThrows<ArgumentNullException>(() => new SqlDistributedLock("a", default(DbConnection)));
34+
TestHelper.AssertThrows<ArgumentException>(() => new SqlDistributedLock("a", ConnectionString, (SqlDistributedLockConnectionStrategy)(-1)));
35+
TestHelper.AssertThrows<ArgumentException>(() => new SqlDistributedLock("a", ConnectionString, (SqlDistributedLockConnectionStrategy)4));
3436
TestHelper.AssertThrows<FormatException>(() => new SqlDistributedLock(new string('a', SqlDistributedLock.MaxLockNameLength + 1), ConnectionString));
3537
TestHelper.AssertDoesNotThrow(() => new SqlDistributedLock(new string('a', SqlDistributedLock.MaxLockNameLength), ConnectionString));
3638
}
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
using Medallion.Threading.Sql;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using System.Text;
6+
using System.Threading.Tasks;
7+
using System.Threading;
8+
using Microsoft.VisualStudio.TestTools.UnitTesting;
9+
10+
namespace Medallion.Threading.Tests.Sql
11+
{
12+
public abstract class SqlDistributedReaderWriterLockTestBase : DistributedLockTestBase
13+
{
14+
[TestMethod]
15+
public void TestMultipleReadersSingleWriter()
16+
{
17+
var @lock = this.CreateReaderWriterLock(nameof(TestMultipleReadersSingleWriter));
18+
19+
var readHandle1 = this.CreateReaderWriterLock(nameof(TestMultipleReadersSingleWriter)).TryAcquireReadLockAsync().Result;
20+
Assert.IsNotNull(readHandle1, this.GetType().ToString());
21+
var readHandle2 = this.CreateReaderWriterLock(nameof(TestMultipleReadersSingleWriter)).TryAcquireReadLockAsync().Result;
22+
Assert.IsNotNull(readHandle2, this.GetType().ToString());
23+
24+
using (var handle = @lock.TryAcquireUpgradeableReadLock())
25+
{
26+
Assert.IsNotNull(handle);
27+
28+
var readHandle3 = this.CreateReaderWriterLock(nameof(TestMultipleReadersSingleWriter)).TryAcquireReadLock();
29+
Assert.IsNotNull(readHandle3);
30+
31+
this.CreateReaderWriterLock(nameof(TestMultipleReadersSingleWriter)).TryAcquireUpgradeableReadLock().ShouldEqual(null);
32+
this.CreateReaderWriterLock(nameof(TestMultipleReadersSingleWriter)).TryAcquireWriteLock().ShouldEqual(null);
33+
34+
readHandle3.Dispose();
35+
}
36+
37+
readHandle1.Dispose();
38+
readHandle2.Dispose();
39+
40+
using (var writeHandle = this.CreateReaderWriterLock(nameof(TestMultipleReadersSingleWriter)).TryAcquireUpgradeableReadLock())
41+
{
42+
Assert.IsNotNull(writeHandle);
43+
}
44+
}
45+
46+
[TestMethod]
47+
public void TestUpgradeToWriteLock()
48+
{
49+
var @lock = this.CreateReaderWriterLock(nameof(TestUpgradeToWriteLock));
50+
51+
var readHandle = this.CreateReaderWriterLock(nameof(TestUpgradeToWriteLock)).AcquireReadLock();
52+
53+
Task<IDisposable> readTask;
54+
using (var upgradeableHandle = @lock.AcquireUpgradeableReadLockAsync().Result)
55+
{
56+
upgradeableHandle.TryUpgradeToWriteLock().ShouldEqual(false); // read lock still held
57+
58+
readHandle.Dispose();
59+
60+
upgradeableHandle.TryUpgradeToWriteLock().ShouldEqual(true);
61+
62+
readTask = this.CreateReaderWriterLock(nameof(TestUpgradeToWriteLock)).AcquireReadLockAsync();
63+
readTask.Wait(TimeSpan.FromSeconds(.1)).ShouldEqual(false, "write lock held");
64+
}
65+
66+
readTask.Wait(TimeSpan.FromSeconds(10)).ShouldEqual(true, "write lock released");
67+
readTask.Result.Dispose();
68+
}
69+
70+
[TestMethod]
71+
public void TestReaderWriterLockBadArguments()
72+
{
73+
var @lock = this.CreateReaderWriterLock(Guid.NewGuid().ToString());
74+
TestHelper.AssertThrows<ArgumentOutOfRangeException>(() => @lock.AcquireReadLock(TimeSpan.FromSeconds(-2)));
75+
TestHelper.AssertThrows<ArgumentOutOfRangeException>(() => @lock.AcquireReadLockAsync(TimeSpan.FromSeconds(-2)));
76+
TestHelper.AssertThrows<ArgumentOutOfRangeException>(() => @lock.TryAcquireReadLock(TimeSpan.FromSeconds(-2)));
77+
TestHelper.AssertThrows<ArgumentOutOfRangeException>(() => @lock.TryAcquireReadLockAsync(TimeSpan.FromSeconds(-2)));
78+
TestHelper.AssertThrows<ArgumentOutOfRangeException>(() => @lock.AcquireReadLock(TimeSpan.FromSeconds(int.MaxValue)));
79+
TestHelper.AssertThrows<ArgumentOutOfRangeException>(() => @lock.AcquireReadLockAsync(TimeSpan.FromSeconds(int.MaxValue)));
80+
TestHelper.AssertThrows<ArgumentOutOfRangeException>(() => @lock.TryAcquireReadLock(TimeSpan.FromSeconds(int.MaxValue)));
81+
TestHelper.AssertThrows<ArgumentOutOfRangeException>(() => @lock.TryAcquireReadLockAsync(TimeSpan.FromSeconds(int.MaxValue)));
82+
83+
TestHelper.AssertThrows<ArgumentOutOfRangeException>(() => @lock.AcquireUpgradeableReadLock(TimeSpan.FromSeconds(-2)));
84+
TestHelper.AssertThrows<ArgumentOutOfRangeException>(() => @lock.AcquireUpgradeableReadLockAsync(TimeSpan.FromSeconds(-2)));
85+
TestHelper.AssertThrows<ArgumentOutOfRangeException>(() => @lock.TryAcquireUpgradeableReadLock(TimeSpan.FromSeconds(-2)));
86+
TestHelper.AssertThrows<ArgumentOutOfRangeException>(() => @lock.TryAcquireUpgradeableReadLockAsync(TimeSpan.FromSeconds(-2)));
87+
TestHelper.AssertThrows<ArgumentOutOfRangeException>(() => @lock.AcquireUpgradeableReadLock(TimeSpan.FromSeconds(int.MaxValue)));
88+
TestHelper.AssertThrows<ArgumentOutOfRangeException>(() => @lock.AcquireUpgradeableReadLockAsync(TimeSpan.FromSeconds(int.MaxValue)));
89+
TestHelper.AssertThrows<ArgumentOutOfRangeException>(() => @lock.TryAcquireUpgradeableReadLock(TimeSpan.FromSeconds(int.MaxValue)));
90+
TestHelper.AssertThrows<ArgumentOutOfRangeException>(() => @lock.TryAcquireUpgradeableReadLockAsync(TimeSpan.FromSeconds(int.MaxValue)));
91+
92+
TestHelper.AssertThrows<ArgumentOutOfRangeException>(() => @lock.AcquireWriteLock(TimeSpan.FromSeconds(-2)));
93+
TestHelper.AssertThrows<ArgumentOutOfRangeException>(() => @lock.AcquireWriteLockAsync(TimeSpan.FromSeconds(-2)));
94+
TestHelper.AssertThrows<ArgumentOutOfRangeException>(() => @lock.TryAcquireWriteLock(TimeSpan.FromSeconds(-2)));
95+
TestHelper.AssertThrows<ArgumentOutOfRangeException>(() => @lock.TryAcquireWriteLockAsync(TimeSpan.FromSeconds(-2)));
96+
TestHelper.AssertThrows<ArgumentOutOfRangeException>(() => @lock.AcquireWriteLock(TimeSpan.FromSeconds(int.MaxValue)));
97+
TestHelper.AssertThrows<ArgumentOutOfRangeException>(() => @lock.AcquireWriteLockAsync(TimeSpan.FromSeconds(int.MaxValue)));
98+
TestHelper.AssertThrows<ArgumentOutOfRangeException>(() => @lock.TryAcquireWriteLock(TimeSpan.FromSeconds(int.MaxValue)));
99+
TestHelper.AssertThrows<ArgumentOutOfRangeException>(() => @lock.TryAcquireWriteLockAsync(TimeSpan.FromSeconds(int.MaxValue)));
100+
101+
using (var upgradeableHandle = @lock.AcquireUpgradeableReadLock())
102+
{
103+
TestHelper.AssertThrows<ArgumentOutOfRangeException>(() => upgradeableHandle.UpgradeToWriteLock(TimeSpan.FromSeconds(-2)));
104+
TestHelper.AssertThrows<ArgumentOutOfRangeException>(() => upgradeableHandle.UpgradeToWriteLockAsync(TimeSpan.FromSeconds(-2)));
105+
TestHelper.AssertThrows<ArgumentOutOfRangeException>(() => upgradeableHandle.TryUpgradeToWriteLock(TimeSpan.FromSeconds(-2)));
106+
TestHelper.AssertThrows<ArgumentOutOfRangeException>(() => upgradeableHandle.TryUpgradeToWriteLockAsync(TimeSpan.FromSeconds(-2)));
107+
TestHelper.AssertThrows<ArgumentOutOfRangeException>(() => upgradeableHandle.UpgradeToWriteLock(TimeSpan.FromSeconds(int.MaxValue)));
108+
TestHelper.AssertThrows<ArgumentOutOfRangeException>(() => upgradeableHandle.UpgradeToWriteLockAsync(TimeSpan.FromSeconds(int.MaxValue)));
109+
TestHelper.AssertThrows<ArgumentOutOfRangeException>(() => upgradeableHandle.TryUpgradeToWriteLock(TimeSpan.FromSeconds(int.MaxValue)));
110+
TestHelper.AssertThrows<ArgumentOutOfRangeException>(() => upgradeableHandle.TryUpgradeToWriteLockAsync(TimeSpan.FromSeconds(int.MaxValue)));
111+
}
112+
}
113+
114+
[TestMethod]
115+
public void TestUpgradeableHandleDisposal()
116+
{
117+
var @lock = this.CreateReaderWriterLock(nameof(TestUpgradeableHandleDisposal));
118+
119+
var handle = @lock.AcquireUpgradeableReadLock();
120+
handle.Dispose();
121+
TestHelper.AssertDoesNotThrow(() => handle.Dispose());
122+
TestHelper.AssertThrows<ObjectDisposedException>(() => handle.TryUpgradeToWriteLock());
123+
TestHelper.AssertThrows<ObjectDisposedException>(() => handle.TryUpgradeToWriteLockAsync());
124+
TestHelper.AssertThrows<ObjectDisposedException>(() => handle.UpgradeToWriteLock());
125+
TestHelper.AssertThrows<ObjectDisposedException>(() => handle.UpgradeToWriteLockAsync());
126+
}
127+
128+
[TestMethod]
129+
public void TestUpgradeableHandleMultipleUpgrades()
130+
{
131+
var @lock = this.CreateReaderWriterLock(nameof(TestUpgradeableHandleMultipleUpgrades));
132+
133+
using (var upgradeHandle = @lock.AcquireUpgradeableReadLock())
134+
{
135+
upgradeHandle.UpgradeToWriteLock();
136+
TestHelper.AssertThrows<InvalidOperationException>(() => upgradeHandle.TryUpgradeToWriteLock());
137+
}
138+
}
139+
140+
internal virtual bool UseWriteLockAsExclusive() => true;
141+
142+
internal abstract SqlDistributedReaderWriterLock CreateReaderWriterLock(string name);
143+
144+
internal sealed override IDistributedLock CreateLock(string name) => new SqlReaderWriterLockDistributedLock(this, name);
145+
146+
internal sealed override string GetSafeLockName(string name) => SqlDistributedReaderWriterLock.GetSafeLockName(name);
147+
148+
private sealed class SqlReaderWriterLockDistributedLock : IDistributedLock
149+
{
150+
private readonly SqlDistributedReaderWriterLock @lock;
151+
private readonly SqlDistributedReaderWriterLockTestBase test;
152+
153+
public SqlReaderWriterLockDistributedLock(SqlDistributedReaderWriterLockTestBase test, string name)
154+
{
155+
this.@lock = test.CreateReaderWriterLock(name);
156+
this.test = test;
157+
}
158+
159+
public IDisposable Acquire(TimeSpan? timeout = default(TimeSpan?), CancellationToken cancellationToken = default(CancellationToken))
160+
{
161+
return test.UseWriteLockAsExclusive()
162+
? this.@lock.AcquireWriteLock(timeout, cancellationToken)
163+
: this.@lock.AcquireUpgradeableReadLock(timeout, cancellationToken);
164+
}
165+
166+
public Task<IDisposable> AcquireAsync(TimeSpan? timeout = default(TimeSpan?), CancellationToken cancellationToken = default(CancellationToken))
167+
{
168+
return test.UseWriteLockAsExclusive()
169+
? this.@lock.AcquireWriteLockAsync(timeout, cancellationToken)
170+
: CastToDisposable(this.@lock.AcquireUpgradeableReadLockAsync(timeout, cancellationToken));
171+
}
172+
173+
public IDisposable TryAcquire(TimeSpan timeout = default(TimeSpan), CancellationToken cancellationToken = default(CancellationToken))
174+
{
175+
return test.UseWriteLockAsExclusive()
176+
? this.@lock.TryAcquireWriteLock(timeout, cancellationToken)
177+
: this.@lock.TryAcquireUpgradeableReadLock(timeout, cancellationToken);
178+
}
179+
180+
public Task<IDisposable> TryAcquireAsync(TimeSpan timeout = default(TimeSpan), CancellationToken cancellationToken = default(CancellationToken))
181+
{
182+
return test.UseWriteLockAsExclusive()
183+
? this.@lock.TryAcquireWriteLockAsync(timeout, cancellationToken)
184+
: CastToDisposable(this.@lock.TryAcquireUpgradeableReadLockAsync(timeout, cancellationToken));
185+
}
186+
187+
private static async Task<IDisposable> CastToDisposable<T>(Task<T> task) where T : IDisposable
188+
{
189+
return await task.ConfigureAwait(false);
190+
}
191+
}
192+
}
193+
}

0 commit comments

Comments
 (0)