Skip to content

Commit f96eaf9

Browse files
HolymaanZlatko Zajec
andauthored
Fix release app lock async (#15)
* Added Lock Owner as parameter in ReleaseAppLockAsync * Added Lock owner to the GetAppLockAsync --------- Co-authored-by: Zlatko Zajec <[email protected]>
1 parent 26f33fe commit f96eaf9

File tree

2 files changed

+204
-4
lines changed

2 files changed

+204
-4
lines changed
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
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.Threading.Tasks;
7+
using Xunit;
8+
using Xunit.Abstractions;
9+
10+
namespace Simpleverse.Repository.Db.Test.SqlServer
11+
{
12+
[Collection("SqlServerCollection")]
13+
public class ReleaseAppLockTests : DatabaseTestFixture
14+
{
15+
public ReleaseAppLockTests(DatabaseFixture fixture, ITestOutputHelper output)
16+
: base(fixture, output)
17+
{
18+
}
19+
20+
[Fact]
21+
public async Task ReleaseAppLockAsync_WithTransaction_SuccessfullyReleasesLock()
22+
{
23+
using var connection = _fixture.GetProfiledConnection();
24+
connection.Open();
25+
var sqlConnection = (SqlConnection)((ProfiledDbConnection)connection).WrappedConnection;
26+
using var transaction = sqlConnection.BeginTransaction();
27+
var key = "test_lock_transaction_501";
28+
29+
var lockAcquired = await sqlConnection.GetAppLockAsync(key, transaction: transaction);
30+
Assert.True(lockAcquired);
31+
32+
// Act
33+
var result = await sqlConnection.ReleaseAppLockAsync(key, transaction: transaction);
34+
35+
// Assert
36+
Assert.True(result);
37+
38+
transaction.Commit();
39+
}
40+
41+
[Fact]
42+
public async Task ReleaseAppLockAsync_WithSession_SuccessfullyReleasesLock()
43+
{
44+
using var connection = _fixture.GetProfiledConnection();
45+
connection.Open();
46+
var sqlConnection = (SqlConnection)((ProfiledDbConnection)connection).WrappedConnection;
47+
48+
var key = "test_lock_session_502";
49+
var lockAcquired = await sqlConnection.GetAppLockAsync(key, transaction: null);
50+
Assert.True(lockAcquired);
51+
52+
// Act
53+
var result = await sqlConnection.ReleaseAppLockAsync(key, transaction: null);
54+
55+
// Assert
56+
Assert.True(result);
57+
}
58+
59+
[Fact]
60+
public async Task ReleaseAppLockAsync_LockNotHeld_ThrowsSqlException()
61+
{
62+
using var connection = _fixture.GetProfiledConnection();
63+
connection.Open();
64+
var sqlConnection = (SqlConnection)((ProfiledDbConnection)connection).WrappedConnection;
65+
using var transaction = sqlConnection.BeginTransaction();
66+
var key = "test_lock_not_held_503";
67+
68+
// Act & Assert
69+
await Assert.ThrowsAsync<SqlException>(async () =>
70+
{
71+
await sqlConnection.ReleaseAppLockAsync(key, transaction: transaction);
72+
});
73+
74+
transaction.Rollback();
75+
}
76+
77+
[Fact]
78+
public async Task ReleaseAppLockAsync_WrongLockOwner_ThrowsSqlException()
79+
{
80+
using var connection = _fixture.GetProfiledConnection();
81+
await connection.OpenAsync();
82+
var sqlConnection = (SqlConnection)((ProfiledDbConnection)connection).WrappedConnection;
83+
var key = "test_lock_wrong_owner_504";
84+
85+
// Acquire lock at session level (without transaction)
86+
await sqlConnection.GetAppLockAsync(key, transaction: null);
87+
88+
// Now create a transaction and try to release the session-level lock
89+
using var transaction = await sqlConnection.BeginTransactionAsync();
90+
91+
// Act & Assert
92+
await Assert.ThrowsAsync<SqlException>(async () =>
93+
{
94+
await sqlConnection.ReleaseAppLockAsync(key, transaction: transaction);
95+
});
96+
97+
// Cleanup
98+
await Assert.ThrowsAsync<InvalidOperationException>(async () =>
99+
{
100+
await sqlConnection.ReleaseAppLockAsync(key, transaction: null);
101+
});
102+
103+
transaction.Rollback();
104+
}
105+
106+
[Fact]
107+
public async Task ReleaseAppLockAsync_KeyTooLong_ThrowsArgumentOutOfRangeException()
108+
{
109+
using var connection = _fixture.GetProfiledConnection();
110+
connection.Open();
111+
var sqlConnection = (SqlConnection)((ProfiledDbConnection)connection).WrappedConnection;
112+
using var transaction = sqlConnection.BeginTransaction();
113+
var key = new string('a', 256);
114+
115+
// Act & Assert
116+
await Assert.ThrowsAsync<ArgumentOutOfRangeException>(async () =>
117+
{
118+
await sqlConnection.ReleaseAppLockAsync(key, transaction: transaction);
119+
});
120+
121+
transaction.Rollback();
122+
}
123+
124+
[Fact]
125+
public async Task ReleaseAppLockAsync_MultipleKeys_SuccessfullyReleasesAll()
126+
{
127+
using var connection = _fixture.GetProfiledConnection();
128+
connection.Open();
129+
var sqlConnection = (SqlConnection)((ProfiledDbConnection)connection).WrappedConnection;
130+
using var transaction = sqlConnection.BeginTransaction();
131+
var keys = new List<string> { "test_lock_505", "test_lock_506", "test_lock_507" };
132+
133+
foreach (var key in keys)
134+
{
135+
await sqlConnection.GetAppLockAsync(key, transaction: transaction);
136+
}
137+
138+
// Act
139+
var result = await sqlConnection.ReleaseAppLockAsync(keys, transaction: transaction);
140+
141+
// Assert
142+
Assert.True(result);
143+
144+
transaction.Commit();
145+
}
146+
147+
[Fact]
148+
public async Task ReleaseAppLockAsync_MultipleKeys_SomeNotHeld_ThrowsSqlException()
149+
{
150+
using var connection = _fixture.GetProfiledConnection();
151+
connection.Open();
152+
var sqlConnection = (SqlConnection)((ProfiledDbConnection)connection).WrappedConnection;
153+
using var transaction = sqlConnection.BeginTransaction();
154+
var keys = new List<string> { "test_lock_508", "test_lock_not_held_509", "test_lock_510" };
155+
156+
// Acquire only two of the three locks
157+
await sqlConnection.GetAppLockAsync(keys[0], transaction: transaction);
158+
await sqlConnection.GetAppLockAsync(keys[2], transaction: transaction);
159+
160+
// Act & Assert - Should throw when trying to release lock that's not held
161+
await Assert.ThrowsAsync<SqlException>(async () =>
162+
{
163+
await sqlConnection.ReleaseAppLockAsync(keys, transaction: transaction);
164+
});
165+
166+
transaction.Rollback();
167+
}
168+
169+
[Fact]
170+
public async Task ReleaseAppLockAsync_AfterTransactionCommit_LockIsReleased()
171+
{
172+
using var connection = _fixture.GetProfiledConnection();
173+
connection.Open();
174+
var sqlConnection = (SqlConnection)((ProfiledDbConnection)connection).WrappedConnection;
175+
176+
var key = "test_lock_auto_release_511";
177+
178+
using (var transaction = sqlConnection.BeginTransaction())
179+
{
180+
await sqlConnection.GetAppLockAsync(key, transaction: transaction);
181+
transaction.Commit();
182+
}
183+
184+
// Act & Assert
185+
using (var transaction = sqlConnection.BeginTransaction())
186+
{
187+
await Assert.ThrowsAsync<SqlException>(async () =>
188+
{
189+
await sqlConnection.ReleaseAppLockAsync(key, transaction: transaction);
190+
});
191+
192+
transaction.Rollback();
193+
}
194+
}
195+
}
196+
}

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,15 @@ public static async Task<bool> GetAppLockAsync(this SqlConnection connection, st
5959
if (key.Length > 255)
6060
throw new ArgumentOutOfRangeException(nameof(key), "Length of the key used for locking must be less then 256 characters.");
6161

62+
var lockOwner = transaction == null ? "Session" : "Transaction";
63+
6264
var result = await connection.ExecuteScalarAsync<int>(
6365
@"
6466
declare @result int
65-
exec @result = sp_getapplock @Resource, @LockMode, @LockTimeout = @Timeout;
67+
exec @result = sp_getapplock @Resource, @LockMode, @LockOwner, @LockTimeout = @Timeout;
6668
select @result
6769
",
68-
new { Resource = key, LockMode = "Exclusive", Timeout = lockTimeout == null ? -1 : lockTimeout.Value.TotalMilliseconds },
70+
new { Resource = key, LockMode = "Exclusive", LockOwner = lockOwner, Timeout = lockTimeout == null ? -1 : lockTimeout.Value.TotalMilliseconds },
6971
transaction: transaction
7072
);
7173

@@ -77,13 +79,15 @@ public static async Task<bool> ReleaseAppLockAsync(this SqlConnection connection
7779
if (key.Length > 255)
7880
throw new ArgumentOutOfRangeException(nameof(key), "Length of the key used for locking must be less then 256 characters.");
7981

82+
var lockOwner = transaction == null ? "Session" : "Transaction";
83+
8084
var result = await connection.ExecuteScalarAsync<int>(
8185
@"
8286
declare @result int
83-
exec @result = sp_releaseapplock @Resource
87+
exec @result = sp_releaseapplock @Resource, @LockOwner
8488
select @result
8589
",
86-
new { Resource = key },
90+
new { Resource = key, LockOwner = lockOwner },
8791
transaction: transaction
8892
);
8993

0 commit comments

Comments
 (0)