Skip to content

Commit 75fd5bf

Browse files
committed
Improve disposal processes safety as Locks are released when Connection is closed and/or disposed of in C#. So we can rely on that, and do not need to throw warning exceptions. Release & Disposal are now safer, and more streamlined. Added explicit Release() and ReleaseAsync() methods for improved api, and unit tests to validate these for Sync & Async.
1 parent 9cfe6f5 commit 75fd5bf

4 files changed

Lines changed: 307 additions & 31 deletions

File tree

SqlAppLockHelper.Common/SqlServerAppLock.cs

Lines changed: 50 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ namespace SqlAppLockHelper
77
{
88
public class SqlServerAppLock : IDisposable, IAsyncDisposable
99
{
10+
//NOTE: Using Delegates here allows this class to be independent of Microsoft.Data/System.Data
11+
// namespaces, reducing duplication.
1012
private Func<ValueTask> _releaseActionAsync = null;
1113
private Action _releaseAction = null;
1214

@@ -31,6 +33,8 @@ public SqlServerAppLock(
3133
LockAcquisitionResult = lockAcquisitionResult;
3234

3335
//Initialize Sync & Async callbacks for Disposal!
36+
//NOTE: Using Delegates here allows this class to be independent of Microsoft.Data/System.Data
37+
// namespaces, reducing duplication.
3438
_releaseAction = releaseAction;
3539
_releaseActionAsync = releaseActionAsync;
3640
}
@@ -40,32 +44,62 @@ public SqlServerAppLock(
4044
public bool IsLockAcquired => LockAcquisitionResult == SqlServerAppLockAcquisitionResult.AcquiredImmediately
4145
|| LockAcquisitionResult == SqlServerAppLockAcquisitionResult.AcquiredAfterRelease;
4246

43-
public void Dispose()
47+
/// <summary>
48+
/// Explicitly release the Lock on demand asynchronously; also called when disposed asynchronously.
49+
/// This may error if no Lock is currently acquired, however if a lock is acquired then this method is
50+
/// idempotent and safe to call multiple times.
51+
/// </summary>
52+
/// <returns></returns>
53+
public async Task ReleaseAsync()
4454
{
45-
if (!IsDisposed && _releaseAction != null)
55+
if (_releaseActionAsync != null)
4656
{
47-
if (_releaseAction != null && IsLockAcquired)
48-
{
49-
_releaseAction.Invoke();
50-
_releaseAction = null;
51-
}
57+
await _releaseActionAsync.Invoke();
58+
_releaseActionAsync = null;
59+
}
60+
}
61+
62+
/// <summary>
63+
/// Explicitly release the Lock on demand; also called when disposed.
64+
/// This may error if no Lock is currently acquired, however if a lock is acquired then this method is
65+
/// idempotent and safe to call multiple times.
66+
/// </summary>
67+
public void Release()
68+
{
69+
_releaseAction?.Invoke();
70+
_releaseAction = null;
71+
}
72+
73+
/// <summary>
74+
/// Safely Dispose and release the lock.
75+
/// </summary>
76+
public void Dispose()
77+
{
78+
if (IsDisposed) return;
5279

53-
IsDisposed = true;
80+
if (IsLockAcquired)
81+
{
82+
//NOTE: This is Safe/Idempotent to call...
83+
Release();
5484
}
85+
86+
IsDisposed = true;
5587
}
5688

89+
/// <summary>
90+
/// Safely Dispose and release the lock asynchronously.
91+
/// </summary>
5792
public async ValueTask DisposeAsync()
5893
{
59-
if (!IsDisposed && _releaseActionAsync != null)
60-
{
61-
if (_releaseAction != null && IsLockAcquired)
62-
{
63-
await _releaseActionAsync.Invoke();
64-
_releaseActionAsync = null;
65-
}
94+
if (IsDisposed) return;
6695

67-
IsDisposed = true;
96+
if (IsLockAcquired)
97+
{
98+
//NOTE: This is Safe/Idempotent to call...
99+
await ReleaseAsync();
68100
}
101+
102+
IsDisposed = true;
69103
}
70104
}
71105
}

SqlAppLockHelper.SystemDataNS/SqlAppLockCommandBuilder.cs

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -67,20 +67,27 @@ public static SqlCommand CreateReleaseLockSqlCommand(
6767
SqlServerAppLockScope sqlAppLockScope
6868
)
6969
{
70-
var sqlConn = sqlConnection ?? throw new ArgumentException(
71-
$"The SqlConnection cannot be null; this is CRITICAL error during Dispose(), and may result in an" +
72-
$" abandoned {nameof(SqlServerAppLock)} on the server.",
73-
nameof(sqlConnection)
74-
);
75-
76-
if (sqlConn.State != ConnectionState.Open)
77-
{
78-
throw new ArgumentException(
79-
$"The SqlConnection must be [Open]; current state is [{sqlConn.State}]. This is CRITICAL error during" +
80-
$" Dispose(), and may result in an abandoned {nameof(SqlServerAppLock)} on the server.",
81-
nameof(sqlConnection)
82-
);
83-
}
70+
//Short Circuit if the Connection is not valid and/or not open; as this means that Sql Server
71+
// has likely already released the lock so we just return null.
72+
if (sqlConnection == null || sqlConnection.State != ConnectionState.Open)
73+
return null;
74+
75+
var sqlConn = sqlConnection;
76+
77+
//var sqlConn = sqlConnection ?? throw new ArgumentException(
78+
// $"The SqlConnection cannot be null; this is CRITICAL error during Dispose(), and may result in an" +
79+
// $" abandoned {nameof(SqlServerAppLock)} on the server.",
80+
// nameof(sqlConnection)
81+
//);
82+
83+
//if (sqlConn.State != ConnectionState.Open)
84+
//{
85+
// throw new ArgumentException(
86+
// $"The SqlConnection must be [Open]; current state is [{sqlConn.State}]. This is CRITICAL error during" +
87+
// $" Dispose(), and may result in an abandoned {nameof(SqlServerAppLock)} on the server.",
88+
// nameof(sqlConnection)
89+
// );
90+
//}
8491

8592
var lockScope = SqlAppLockValidation.GetLockOwnerFromScope(sqlAppLockScope);
8693

@@ -132,6 +139,10 @@ SqlServerAppLockScope sqlAppLockScope
132139
{
133140
await using var sqlCmd = CreateReleaseLockSqlCommand(sqlConnection, lockName, sqlAppLockScope);
134141

142+
//NOTE: Short Circuit if no Command was provided because this means that the Connection was null or already closed, and
143+
// Sql Server has likely already released the lock... so we can continue.
144+
if (sqlCmd == null) return;
145+
135146
//Execute the Release process...
136147
await sqlCmd.ExecuteNonQueryAsync();
137148

@@ -164,6 +175,10 @@ SqlServerAppLockScope sqlAppLockScope
164175
{
165176
using var sqlCmd = CreateReleaseLockSqlCommand(sqlConnection, lockName, sqlAppLockScope);
166177

178+
//NOTE: Short Circuit if no Command was provided because this means that the Connection was null or already closed, and
179+
// Sql Server has likely already released the lock... so we can continue.
180+
if (sqlCmd == null) return;
181+
167182
//Execute the Release process...
168183
sqlCmd.ExecuteNonQuery();
169184

SqlAppLockHelper.Tests/MicrosoftDataConnectionAppLockTests.cs

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,5 +110,109 @@ public async Task TestAsyncConnectionAppLockExplicitRelease()
110110

111111
Assert.IsTrue(appLock.IsDisposed);
112112
}
113+
114+
[TestMethod]
115+
public async Task TestAsyncConnectionAppLockExplicitReleaseAsync()
116+
{
117+
await using var sqlConn = TestHelper.CreateMicrosoftDataSqlConnection();
118+
await sqlConn.OpenAsync();
119+
120+
//Acquire the Lock & Validate
121+
await using var appLock = await sqlConn.AcquireAppLockAsync(nameof(TestSystemDataAppLock));
122+
123+
Assert.IsNotNull(appLock);
124+
Assert.AreEqual(appLock.LockAcquisitionResult, SqlServerAppLockAcquisitionResult.AcquiredImmediately);
125+
Assert.IsFalse(string.IsNullOrWhiteSpace(appLock.LockName));
126+
127+
//Explicitly Release the AppLock & Validate
128+
await appLock.ReleaseAsync();
129+
130+
await using var sqlConnAfterRelease = TestHelper.CreateMicrosoftDataSqlConnection();
131+
await sqlConnAfterRelease.OpenAsync();
132+
133+
//Acquire the Lock & Validate
134+
await using var appLockAfterRelease = await sqlConnAfterRelease.AcquireAppLockAsync(nameof(TestSystemDataAppLock));
135+
136+
Assert.IsNotNull(appLockAfterRelease);
137+
Assert.AreEqual(appLockAfterRelease.LockAcquisitionResult, SqlServerAppLockAcquisitionResult.AcquiredImmediately);
138+
Assert.IsFalse(string.IsNullOrWhiteSpace(appLockAfterRelease.LockName));
139+
}
140+
141+
[TestMethod]
142+
public void TestAsyncConnectionAppLockExplicitReleaseSync()
143+
{
144+
using var sqlConn = TestHelper.CreateMicrosoftDataSqlConnection();
145+
sqlConn.Open();
146+
147+
//Acquire the Lock & Validate
148+
using var appLock = sqlConn.AcquireAppLock(nameof(TestSystemDataAppLock));
149+
150+
Assert.IsNotNull(appLock);
151+
Assert.AreEqual(appLock.LockAcquisitionResult, SqlServerAppLockAcquisitionResult.AcquiredImmediately);
152+
Assert.IsFalse(string.IsNullOrWhiteSpace(appLock.LockName));
153+
154+
//Explicitly Release the AppLock & Validate
155+
appLock.Release();
156+
157+
using var sqlConnAfterRelease = TestHelper.CreateMicrosoftDataSqlConnection();
158+
sqlConnAfterRelease.Open();
159+
160+
//Acquire the Lock & Validate
161+
using var appLockAfterRelease = sqlConnAfterRelease.AcquireAppLock(nameof(TestSystemDataAppLock));
162+
163+
Assert.IsNotNull(appLockAfterRelease);
164+
Assert.AreEqual(appLockAfterRelease.LockAcquisitionResult, SqlServerAppLockAcquisitionResult.AcquiredImmediately);
165+
Assert.IsFalse(string.IsNullOrWhiteSpace(appLockAfterRelease.LockName));
166+
}
167+
168+
[TestMethod]
169+
public async Task TestAsyncConnectionAppLockReleaseWithConnectionDisposalWithUsing()
170+
{
171+
await using (var sqlConn = TestHelper.CreateMicrosoftDataSqlConnection())
172+
{
173+
await sqlConn.OpenAsync();
174+
175+
//Acquire the Lock & Validate but DO NOT DISPOSE of it in the current Scope!
176+
//await using var appLock = await sqlConn.AcquireAppLockAsync(
177+
var appLock = await sqlConn.AcquireAppLockAsync(
178+
nameof(TestSystemDataAppLock),
179+
acquisitionTimeoutSeconds: 1,
180+
throwsException: false
181+
);
182+
183+
Assert.IsNotNull(appLock);
184+
Assert.AreEqual(appLock.LockAcquisitionResult, SqlServerAppLockAcquisitionResult.AcquiredImmediately);
185+
Assert.IsFalse(string.IsNullOrWhiteSpace(appLock.LockName));
186+
187+
////Attempt Acquisition from SECOND Connection Once Locked & Validate...
188+
await using var sqlConnWhileLocked = TestHelper.CreateMicrosoftDataSqlConnection();
189+
await sqlConnWhileLocked.OpenAsync();
190+
191+
await using var appLockFailWhileLocked = await sqlConnWhileLocked.AcquireAppLockAsync(
192+
nameof(TestSystemDataAppLock),
193+
acquisitionTimeoutSeconds: 1,
194+
throwsException: false
195+
);
196+
197+
Assert.IsNotNull(appLockFailWhileLocked);
198+
Assert.AreEqual(appLockFailWhileLocked.LockAcquisitionResult,
199+
SqlServerAppLockAcquisitionResult.FailedDueToTimeout);
200+
Assert.IsFalse(string.IsNullOrWhiteSpace(appLockFailWhileLocked.LockName));
201+
}
202+
203+
//Attempt Reacquisition of the Lock Once Released via Sql Connection Disposal (from using{} scope) above!
204+
//Get a new Transaction to test re-acquisition!
205+
await using var sqlConnAfterRelease = TestHelper.CreateMicrosoftDataSqlConnection();
206+
await sqlConnAfterRelease.OpenAsync();
207+
208+
await using var appLockAfterRelease = await sqlConnAfterRelease.AcquireAppLockAsync(
209+
nameof(TestSystemDataAppLock),
210+
throwsException: false
211+
);
212+
213+
Assert.IsNotNull(appLockAfterRelease);
214+
Assert.AreEqual(appLockAfterRelease.LockAcquisitionResult, SqlServerAppLockAcquisitionResult.AcquiredImmediately);
215+
Assert.IsFalse(string.IsNullOrWhiteSpace(appLockAfterRelease.LockName));
216+
}
113217
}
114218
}

0 commit comments

Comments
 (0)