Skip to content

Commit 26f33fe

Browse files
committed
2 parents a399f7e + dc2809d commit 26f33fe

File tree

3 files changed

+204
-3
lines changed

3 files changed

+204
-3
lines changed
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
using Microsoft.Data.SqlClient;
2+
using Simpleverse.Repository.Db.SqlServer;
3+
using StackExchange.Profiling.Data;
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using System.Threading.Tasks;
8+
using Xunit;
9+
using Xunit.Abstractions;
10+
11+
namespace Simpleverse.Repository.Db.Test.SqlServer
12+
{
13+
[Collection("SqlServerCollection")]
14+
public class AcquireLocksTests : DatabaseTestFixture
15+
{
16+
public AcquireLocksTests(DatabaseFixture fixture, ITestOutputHelper output)
17+
: base(fixture, output)
18+
{
19+
}
20+
21+
[Fact]
22+
public async Task TryGetAcquireLocks_AllLocksAcquired_ReturnsTrue()
23+
{
24+
using (var connection = _fixture.GetProfiledConnection())
25+
{
26+
connection.Open();
27+
var sqlConnection = (SqlConnection)((ProfiledDbConnection)connection).WrappedConnection;
28+
using (var transaction = sqlConnection.BeginTransaction())
29+
{
30+
var keys = new List<string> { "101", "102", "103" };
31+
32+
// act
33+
var result = await sqlConnection.TryGetAppLockAsync(keys, transaction: transaction);
34+
35+
// assert
36+
Assert.True(result);
37+
38+
transaction.Commit();
39+
}
40+
}
41+
}
42+
43+
[Fact]
44+
public async Task TryGetAcquireLocks_ParallelThreads_ContentionTest()
45+
{
46+
var keys = new List<string> { "201", "202", "203" };
47+
48+
async Task<bool> TryAcquireLocksAsync()
49+
{
50+
using var connection = _fixture.GetProfiledConnection();
51+
connection.Open();
52+
var sqlConnection = (SqlConnection)((ProfiledDbConnection)connection).WrappedConnection;
53+
using var transaction = sqlConnection.BeginTransaction();
54+
var result = await sqlConnection.TryGetAppLockAsync(keys, transaction: transaction, lockTimeout: TimeSpan.FromMilliseconds(500));
55+
if (result)
56+
transaction.Commit();
57+
else
58+
transaction.Rollback();
59+
return result;
60+
}
61+
62+
// Run two tasks in parallel
63+
var task1 = Task.Run(TryAcquireLocksAsync);
64+
var task2 = Task.Run(TryAcquireLocksAsync);
65+
66+
var results = await Task.WhenAll(task1, task2);
67+
68+
// Only one should succeed in acquiring all locks
69+
Assert.Equal(2, results.Count(r => r));
70+
}
71+
72+
[Fact]
73+
public async Task TryGetAcquireLocks_NullKeys_ThrowsArgumentException()
74+
{
75+
using var connection = _fixture.GetProfiledConnection();
76+
connection.Open();
77+
var sqlConnection = (SqlConnection)((ProfiledDbConnection)connection).WrappedConnection;
78+
using var transaction = sqlConnection.BeginTransaction();
79+
80+
await Assert.ThrowsAsync<ArgumentException>(async () =>
81+
{
82+
await sqlConnection.TryGetAppLockAsync(null, transaction: transaction);
83+
});
84+
85+
transaction.Rollback();
86+
}
87+
88+
[Fact]
89+
public async Task TryGetAcquireLocks_EmptyKeys()
90+
{
91+
using var connection = _fixture.GetProfiledConnection();
92+
connection.Open();
93+
var sqlConnection = (SqlConnection)((ProfiledDbConnection)connection).WrappedConnection;
94+
using var transaction = sqlConnection.BeginTransaction();
95+
96+
try
97+
{
98+
await sqlConnection.TryGetAppLockAsync(new List<string> { }, transaction: transaction);
99+
}
100+
catch (ArgumentException ex) when (ex.Message.Contains("Keys collection must not be null or empty.", StringComparison.OrdinalIgnoreCase))
101+
{
102+
transaction.Rollback();
103+
return;
104+
}
105+
}
106+
107+
[Fact]
108+
public async Task TryGetAcquireLocks_DuplicateKeys_ReturnsTrue()
109+
{
110+
using var connection = _fixture.GetProfiledConnection();
111+
connection.Open();
112+
var sqlConnection = (SqlConnection)((ProfiledDbConnection)connection).WrappedConnection;
113+
using var transaction = sqlConnection.BeginTransaction();
114+
115+
var keys = new List<string> { "301", "301", "302" };
116+
var result = await sqlConnection.TryGetAppLockAsync(keys, transaction: transaction);
117+
118+
Assert.True(result);
119+
120+
transaction.Commit();
121+
}
122+
123+
[Fact]
124+
public async Task TryGetAcquireLocks_LockTimeout_ReturnsFalse()
125+
{
126+
var keys = new List<string> { "401", "402" };
127+
128+
using var connection1 = _fixture.GetProfiledConnection();
129+
connection1.Open();
130+
var sqlConnection1 = (SqlConnection)((ProfiledDbConnection)connection1).WrappedConnection;
131+
using var transaction1 = sqlConnection1.BeginTransaction();
132+
await sqlConnection1.TryGetAppLockAsync(keys, transaction: transaction1);
133+
134+
using var connection2 = _fixture.GetProfiledConnection();
135+
connection2.Open();
136+
var sqlConnection2 = (SqlConnection)((ProfiledDbConnection)connection2).WrappedConnection;
137+
using var transaction2 = sqlConnection2.BeginTransaction();
138+
var result = await sqlConnection2.TryGetAppLockAsync(keys, transaction: transaction2, lockTimeout: TimeSpan.FromMilliseconds(100));
139+
140+
Assert.False(result);
141+
142+
transaction1.Rollback();
143+
transaction2.Rollback();
144+
}
145+
}
146+
}

src/Simpleverse.Repository.Db/Simpleverse.Repository.Db.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@
1313
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
1414
<PackageTags>Dapper, Bulk, Merge, Upsert, Delete, Insert, Update, Repository</PackageTags>
1515
<PackageLicenseFile>LICENSE</PackageLicenseFile>
16-
<Version>2.1.30</Version>
16+
<Version>2.1.31</Version>
1717
<Description>High performance operation for MS SQL Server built for Dapper ORM. Including bulk operations Insert, Update, Delete, Get as well as Upsert both single and bulk.</Description>
18-
<AssemblyVersion>2.1.30.0</AssemblyVersion>
19-
<FileVersion>2.1.30.0</FileVersion>
18+
<AssemblyVersion>2.1.31.0</AssemblyVersion>
19+
<FileVersion>2.1.31.0</FileVersion>
2020
<RepositoryUrl>https://github.com/lukaferlez/Simpleverse.Repository</RepositoryUrl>
2121
<PackageReadmeFile>README.md</PackageReadmeFile>
2222
<EmbedAllSources>true</EmbedAllSources>

src/Simpleverse.Repository.Db/SqlServer/SqlConnectionExtensions.cs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
using System;
44
using System.Collections.Generic;
55
using System.Data;
6+
using System.Linq;
67
using System.Reflection;
8+
using System.Threading;
79
using System.Threading.Tasks;
810

911
namespace Simpleverse.Repository.Db.SqlServer
@@ -88,6 +90,59 @@ select @result
8890
return result == 0;
8991
}
9092

93+
public static async Task<bool> ReleaseAppLockAsync(this SqlConnection connection, IEnumerable<string> keys, IDbTransaction transaction = null)
94+
{
95+
bool allReleased = true;
96+
97+
foreach (var key in keys)
98+
{
99+
var released = await connection.ReleaseAppLockAsync(key, transaction);
100+
if (!released)
101+
allReleased = false;
102+
}
103+
104+
return allReleased;
105+
}
106+
107+
public static async Task<bool> TryGetAppLockAsync(this SqlConnection connection, IEnumerable<string> keys, int retryTimeout = 100, int numberOfRetries = 3, IDbTransaction transaction = null, TimeSpan? lockTimeout = null)
108+
{
109+
if (keys == null || !keys.Any())
110+
throw new ArgumentException("Keys collection must not be null or empty.", nameof(keys));
111+
if (numberOfRetries < 1)
112+
throw new ArgumentOutOfRangeException(nameof(numberOfRetries), "Number of retries must be at least 1.");
113+
if (retryTimeout < 0)
114+
throw new ArgumentOutOfRangeException(nameof(retryTimeout), "Retry timeout must be non-negative.");
115+
116+
bool allLocked = true;
117+
118+
for (int retry = 0; retry < numberOfRetries; retry++)
119+
{
120+
int lastAttemptedIndex = -1;
121+
allLocked = true;
122+
123+
foreach (var key in keys)
124+
{
125+
var locked = await connection.GetAppLockAsync(key, transaction, lockTimeout);
126+
127+
if (!locked)
128+
{
129+
allLocked = false;
130+
break;
131+
}
132+
133+
lastAttemptedIndex++;
134+
}
135+
136+
if (allLocked)
137+
break;
138+
139+
await connection.ReleaseAppLockAsync(keys.Take(lastAttemptedIndex + 1), transaction);
140+
await Task.Delay(retryTimeout);
141+
}
142+
143+
return allLocked;
144+
}
145+
91146
public static Task<R> ExecuteWithAppLockAsync<R>(
92147
this SqlConnection conn,
93148
string resourceIdentifier,

0 commit comments

Comments
 (0)