Skip to content

Commit 3b96fb0

Browse files
committed
Added support for IDbConnection/IDbTransaction interfaces (#3)
1 parent 18fa8a5 commit 3b96fb0

File tree

8 files changed

+108
-49
lines changed

8 files changed

+108
-49
lines changed

DistributedLock/Sql/ConnectionPooling/SharedConnectionLock.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public SharedConnectionLock(string connectionString)
5858

5959
for (var i = 0; i < purgedLockNames.Count; ++i)
6060
{
61-
DbParameter ignored; // note: we don't check the exit code; this is just best-effort cleanup
61+
IDbDataParameter ignored; // note: we don't check the exit code; this is just best-effort cleanup
6262
using (var releaseCommand = SqlApplicationLock.CreateReleaseCommand(this.connection, purgedLockNames[i], out ignored))
6363
{
6464
releaseCommand.ExecuteNonQuery();
@@ -103,10 +103,10 @@ public SharedConnectionLock(string connectionString)
103103

104104
for (var i = 0; i < purgedLockNames.Count; ++i)
105105
{
106-
DbParameter ignored; // note: we don't check the exit code; this is just best-effort cleanup
106+
IDbDataParameter ignored; // note: we don't check the exit code; this is just best-effort cleanup
107107
using (var releaseCommand = SqlApplicationLock.CreateReleaseCommand(this.connection, purgedLockNames[i], out ignored))
108108
{
109-
await releaseCommand.ExecuteNonQueryAsync().ConfigureAwait(false);
109+
await releaseCommand.ExecuteNonQueryAsync(CancellationToken.None).ConfigureAwait(false);
110110
}
111111
}
112112

DistributedLock/Sql/InternalLocks/ConnectionScopedSqlDistributedLock.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ namespace Medallion.Threading.Sql
1212
internal sealed class ConnectionScopedSqlDistributedLock : IInternalSqlDistributedLock
1313
{
1414
private readonly string lockName;
15-
private readonly DbConnection connection;
15+
private readonly IDbConnection connection;
1616

17-
public ConnectionScopedSqlDistributedLock(string lockName, DbConnection connection)
17+
public ConnectionScopedSqlDistributedLock(string lockName, IDbConnection connection)
1818
{
1919
this.lockName = lockName;
2020
this.connection = connection;
@@ -24,7 +24,7 @@ public IDisposable TryAcquire(int timeoutMillis, SqlApplicationLock.Mode mode, I
2424
{
2525
this.CheckConnection();
2626

27-
return SqlApplicationLock.ExecuteAcquireCommand(this.connection, this.lockName, timeoutMillis, mode)
27+
return SqlApplicationLock.ExecuteAcquireCommand(new ConnectionOrTransaction(this.connection), this.lockName, timeoutMillis, mode)
2828
? new LockScope(this)
2929
: null;
3030
}
@@ -33,7 +33,7 @@ public async Task<IDisposable> TryAcquireAsync(int timeoutMillis, SqlApplication
3333
{
3434
this.CheckConnection();
3535

36-
return await SqlApplicationLock.ExecuteAcquireCommandAsync(this.connection, this.lockName, timeoutMillis, mode, cancellationToken).ConfigureAwait(false)
36+
return await SqlApplicationLock.ExecuteAcquireCommandAsync(new ConnectionOrTransaction(this.connection), this.lockName, timeoutMillis, mode, cancellationToken).ConfigureAwait(false)
3737
? new LockScope(this)
3838
: null;
3939
}
@@ -52,7 +52,7 @@ private void Release()
5252
return;
5353
}
5454

55-
SqlApplicationLock.ExecuteReleaseCommand(this.connection, this.lockName);
55+
SqlApplicationLock.ExecuteReleaseCommand(new ConnectionOrTransaction(this.connection), this.lockName);
5656
}
5757

5858
private sealed class LockScope : IDisposable

DistributedLock/Sql/InternalLocks/TransactionScopedSqlDistributedLock.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ namespace Medallion.Threading.Sql
1313
internal sealed class TransactionScopedSqlDistributedLock : IInternalSqlDistributedLock
1414
{
1515
private readonly string lockName;
16-
private readonly DbTransaction transaction;
16+
private readonly IDbTransaction transaction;
1717

18-
public TransactionScopedSqlDistributedLock(string lockName, DbTransaction transaction)
18+
public TransactionScopedSqlDistributedLock(string lockName, IDbTransaction transaction)
1919
{
2020
this.lockName = lockName;
2121
this.transaction = transaction;
@@ -25,7 +25,7 @@ public IDisposable TryAcquire(int timeoutMillis, SqlApplicationLock.Mode mode, I
2525
{
2626
this.CheckConnection();
2727

28-
return SqlApplicationLock.ExecuteAcquireCommand(this.transaction, this.lockName, timeoutMillis, mode)
28+
return SqlApplicationLock.ExecuteAcquireCommand(new ConnectionOrTransaction(this.transaction), this.lockName, timeoutMillis, mode)
2929
? new LockScope(this)
3030
: null;
3131
}
@@ -34,7 +34,7 @@ public async Task<IDisposable> TryAcquireAsync(int timeoutMillis, SqlApplication
3434
{
3535
this.CheckConnection();
3636

37-
return await SqlApplicationLock.ExecuteAcquireCommandAsync(this.transaction, this.lockName, timeoutMillis, mode, cancellationToken).ConfigureAwait(false)
37+
return await SqlApplicationLock.ExecuteAcquireCommandAsync(new ConnectionOrTransaction(this.transaction), this.lockName, timeoutMillis, mode, cancellationToken).ConfigureAwait(false)
3838
? new LockScope(this)
3939
: null;
4040
}
@@ -55,7 +55,7 @@ private void Release()
5555
return;
5656
}
5757

58-
SqlApplicationLock.ExecuteReleaseCommand(this.transaction, this.lockName);
58+
SqlApplicationLock.ExecuteReleaseCommand(new ConnectionOrTransaction(this.transaction), this.lockName);
5959
}
6060

6161
private sealed class LockScope : IDisposable

DistributedLock/Sql/SqlApplicationLock.cs

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public enum Mode
2121

2222
public static bool ExecuteAcquireCommand(ConnectionOrTransaction connectionOrTransaction, string lockName, int timeoutMillis, Mode mode)
2323
{
24-
DbParameter returnValue;
24+
IDbDataParameter returnValue;
2525
using (var command = CreateAcquireCommand(connectionOrTransaction, lockName, timeoutMillis, mode, out returnValue))
2626
{
2727
command.ExecuteNonQuery();
@@ -31,30 +31,30 @@ public static bool ExecuteAcquireCommand(ConnectionOrTransaction connectionOrTra
3131

3232
public static async Task<bool> ExecuteAcquireCommandAsync(ConnectionOrTransaction connectionOrTransaction, string lockName, int timeoutMillis, Mode mode, CancellationToken cancellationToken)
3333
{
34-
DbParameter returnValue;
34+
IDbDataParameter returnValue;
3535
using (var command = CreateAcquireCommand(connectionOrTransaction, lockName, timeoutMillis, mode, out returnValue))
3636
{
37-
await command.ExecuteNonQueryAndPropagateCancellationAsync(cancellationToken).ConfigureAwait(false);
37+
await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
3838
return ParseExitCode((int)returnValue.Value);
3939
}
4040
}
4141

4242
public static void ExecuteReleaseCommand(ConnectionOrTransaction connectionOrTransaction, string lockName)
4343
{
44-
DbParameter returnValue;
44+
IDbDataParameter returnValue;
4545
using (var command = CreateReleaseCommand(connectionOrTransaction, lockName, out returnValue))
4646
{
4747
command.ExecuteNonQuery();
4848
ParseExitCode((int)returnValue.Value);
4949
}
5050
}
5151

52-
public static DbCommand CreateAcquireCommand(
52+
public static IDbCommand CreateAcquireCommand(
5353
ConnectionOrTransaction connectionOrTransaction,
5454
string lockName,
5555
int timeoutMillis,
5656
Mode mode,
57-
out DbParameter returnValue)
57+
out IDbDataParameter returnValue)
5858
{
5959
var command = connectionOrTransaction.Connection.CreateCommand();
6060
command.Transaction = connectionOrTransaction.Transaction;
@@ -79,7 +79,7 @@ public static DbCommand CreateAcquireCommand(
7979
return command;
8080
}
8181

82-
public static DbCommand CreateReleaseCommand(ConnectionOrTransaction connectionOrTransaction, string lockName, out DbParameter returnValue)
82+
public static IDbCommand CreateReleaseCommand(ConnectionOrTransaction connectionOrTransaction, string lockName, out IDbDataParameter returnValue)
8383
{
8484
var command = connectionOrTransaction.Connection.CreateCommand();
8585
command.Transaction = connectionOrTransaction.Transaction;
@@ -128,7 +128,7 @@ private static string GetErrorMessage(int exitCode, string type)
128128
return string.Format("The request for the distribute lock failed with exit code {0} ({1})", exitCode, type);
129129
}
130130

131-
private static DbParameter CreateParameter(DbCommand command, string name, object value)
131+
private static IDbDataParameter CreateParameter(IDbCommand command, string name, object value)
132132
{
133133
var parameter = command.CreateParameter();
134134
parameter.ParameterName = name;
@@ -152,21 +152,25 @@ internal struct ConnectionOrTransaction
152152
{
153153
private object connectionOrTransaction;
154154

155-
public DbTransaction Transaction => this.connectionOrTransaction as DbTransaction;
156-
public DbConnection Connection => this.Transaction?.Connection ?? (this.connectionOrTransaction as DbConnection);
155+
public IDbTransaction Transaction => this.connectionOrTransaction as IDbTransaction;
156+
public IDbConnection Connection => this.Transaction?.Connection ?? (this.connectionOrTransaction as IDbConnection);
157157

158-
public static implicit operator ConnectionOrTransaction(DbTransaction transaction)
158+
public ConnectionOrTransaction(IDbConnection connection)
159159
{
160-
if (transaction == null) { throw new ArgumentNullException(nameof(transaction)); }
160+
if (connection == null) { throw new ArgumentNullException(nameof(connection)); }
161161

162-
return new ConnectionOrTransaction { connectionOrTransaction = transaction };
162+
this.connectionOrTransaction = connection;
163163
}
164164

165-
public static implicit operator ConnectionOrTransaction(DbConnection connection)
165+
public ConnectionOrTransaction(IDbTransaction transaction)
166166
{
167-
if (connection == null) { throw new ArgumentNullException(nameof(connection)); }
167+
if (transaction == null) { throw new ArgumentNullException(nameof(transaction)); }
168168

169-
return new ConnectionOrTransaction { connectionOrTransaction = connection };
169+
this.connectionOrTransaction = transaction;
170170
}
171+
172+
public static implicit operator ConnectionOrTransaction(DbTransaction transaction) => new ConnectionOrTransaction(transaction);
173+
174+
public static implicit operator ConnectionOrTransaction(DbConnection connection) => new ConnectionOrTransaction(connection);
171175
}
172176
}

DistributedLock/Sql/SqlDistributedLock.cs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,28 @@ public SqlDistributedLock(string lockName, string connectionString, SqlDistribut
5050
/// close, or dispose it
5151
/// </summary>
5252
public SqlDistributedLock(string lockName, DbConnection connection)
53+
: this(lockName, (IDbConnection)connection)
54+
{
55+
}
56+
57+
/// <summary>
58+
/// Creates a lock with name <paramref name="lockName"/> which, when acquired,
59+
/// will be scoped to the given <paramref name="transaction"/>. The <paramref name="transaction"/> and its
60+
/// <see cref="DbTransaction.Connection"/> are assumed to be externally managed: the <see cref="SqlDistributedLock"/> will
61+
/// not attempt to open, close, commit, roll back, or dispose them
62+
/// </summary>
63+
public SqlDistributedLock(string lockName, DbTransaction transaction)
64+
: this(lockName, (IDbTransaction)transaction)
65+
{
66+
}
67+
68+
/// <summary>
69+
/// Creates a lock with name <paramref name="lockName"/> which, when acquired,
70+
/// will be scoped to the given <paramref name="connection"/>. The <paramref name="connection"/> is
71+
/// assumed to be externally managed: the <see cref="SqlDistributedLock"/> will not attempt to open,
72+
/// close, or dispose it
73+
/// </summary>
74+
public SqlDistributedLock(string lockName, IDbConnection connection)
5375
: this(lockName, new ConnectionScopedSqlDistributedLock(lockName, connection))
5476
{
5577
if (connection == null)
@@ -62,7 +84,7 @@ public SqlDistributedLock(string lockName, DbConnection connection)
6284
/// <see cref="DbTransaction.Connection"/> are assumed to be externally managed: the <see cref="SqlDistributedLock"/> will
6385
/// not attempt to open, close, commit, roll back, or dispose them
6486
/// </summary>
65-
public SqlDistributedLock(string lockName, DbTransaction transaction)
87+
public SqlDistributedLock(string lockName, IDbTransaction transaction)
6688
: this(lockName, new TransactionScopedSqlDistributedLock(lockName, transaction))
6789
{
6890
if (transaction == null)

DistributedLock/Sql/SqlDistributedReaderWriterLock.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Data;
34
using System.Data.Common;
45
using System.Linq;
56
using System.Runtime.ExceptionServices;
@@ -51,7 +52,7 @@ public SqlDistributedReaderWriterLock(string lockName, string connectionString,
5152
/// assumed to be externally managed: the <see cref="SqlDistributedReaderWriterLock"/> will not attempt to open,
5253
/// close, or dispose it
5354
/// </summary>
54-
public SqlDistributedReaderWriterLock(string lockName, DbConnection connection)
55+
public SqlDistributedReaderWriterLock(string lockName, IDbConnection connection)
5556
: this(lockName, new ConnectionScopedSqlDistributedLock(lockName, connection))
5657
{
5758
if (connection == null) { throw new ArgumentNullException(nameof(connection)); }
@@ -63,7 +64,7 @@ public SqlDistributedReaderWriterLock(string lockName, DbConnection connection)
6364
/// <see cref="DbTransaction.Connection"/> are assumed to be externally managed: the <see cref="SqlDistributedLock"/> will
6465
/// not attempt to open, close, commit, roll back, or dispose them
6566
/// </summary>
66-
public SqlDistributedReaderWriterLock(string lockName, DbTransaction transaction)
67+
public SqlDistributedReaderWriterLock(string lockName, IDbTransaction transaction)
6768
: this(lockName, new TransactionScopedSqlDistributedLock(lockName, transaction))
6869
{
6970
if (transaction == null) { throw new ArgumentNullException(nameof(transaction)); }

DistributedLock/Sql/SqlHelpers.cs

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,34 @@ namespace Medallion.Threading.Sql
1212
{
1313
internal static class SqlHelpers
1414
{
15-
public static Task<int> ExecuteNonQueryAndPropagateCancellationAsync(this DbCommand command, CancellationToken cancellationToken)
15+
public static Task<int> ExecuteNonQueryAsync(this IDbCommand command, CancellationToken cancellationToken)
1616
{
17-
return cancellationToken.CanBeCanceled
18-
? InternalExecuteNonQueryAndPropagateCancellationAsync(command, cancellationToken)
19-
: command.ExecuteNonQueryAsync();
17+
var dbCommand = command as DbCommand;
18+
if (dbCommand != null)
19+
{
20+
return cancellationToken.CanBeCanceled
21+
? InternalExecuteNonQueryAndPropagateCancellationAsync(dbCommand, cancellationToken)
22+
: dbCommand.ExecuteNonQueryAsync();
23+
}
24+
25+
// synchronous task pattern
26+
var taskBuilder = new TaskCompletionSource<int>();
27+
if (cancellationToken.IsCancellationRequested)
28+
{
29+
taskBuilder.SetCanceled();
30+
return taskBuilder.Task;
31+
}
32+
33+
try
34+
{
35+
taskBuilder.SetResult(command.ExecuteNonQuery());
36+
}
37+
catch (Exception ex)
38+
{
39+
taskBuilder.SetException(ex);
40+
}
41+
42+
return taskBuilder.Task;
2043
}
2144

2245
private static async Task<int> InternalExecuteNonQueryAndPropagateCancellationAsync(DbCommand command, CancellationToken cancellationToken)
@@ -26,24 +49,20 @@ private static async Task<int> InternalExecuteNonQueryAndPropagateCancellationAs
2649
return await command.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false);
2750
}
2851
catch (SqlException ex)
29-
{
3052
// MA: canceled SQL operations throw SqlException when canceled instead of OCE.
3153
// That means that downstream operations end up faulted instead of canceled. We
3254
// wrap with OCE here to correctly propagate cancellation
33-
if (cancellationToken.IsCancellationRequested && ex.Number == 0)
34-
{
35-
throw new OperationCanceledException(
36-
"Command was canceled",
37-
ex,
38-
cancellationToken
39-
);
40-
}
41-
42-
throw;
55+
when (cancellationToken.IsCancellationRequested && ex.Number == 0)
56+
{
57+
throw new OperationCanceledException(
58+
"Command was canceled",
59+
ex,
60+
cancellationToken
61+
);
4362
}
4463
}
4564

46-
public static bool IsClosedOrBroken(this DbConnection connection)
65+
public static bool IsClosedOrBroken(this IDbConnection connection)
4766
=> connection.State == ConnectionState.Closed || connection.State == ConnectionState.Broken;
4867
}
4968
}

DistributedLock/TODO.txt

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,17 @@
22
object hold an object which keys into a conditional weak table that holds the transaction)
33
* Consider providing a hook into when we lose connection to SQL (connection state changed event)
44
* Consider an option for using distributed transactions w/transaction scope instead of holding connections open (probably bad because we don't know where else the scope will flow)
5-
* Expose security options for event wait handles
5+
* Expose security options for event wait handles
6+
* v2 idea: expose ReleaseAsync option by not just returning IDisposable
7+
* v2 idea: use awaitabledisposable pattern for async returns
8+
* v2 idea: remove DbConnection/DbTransaction constructors in favor of just the interface constructors
9+
10+
* 1.2:
11+
* support IDbConnection/Transaction
12+
* support .NET Core if we can
13+
* Rename pooling stuff to "multiplexing"
14+
* Revamp multiplexing to:
15+
- be smarter about lock modes (e. g. why not allow multiple of the same shared lock?)
16+
- use more than one connection: we're going to allocate anyway so we might as well simply allocate as many as we need and free them
17+
when there are no usages left
18+
* Document changes via README, publish NuGet

0 commit comments

Comments
 (0)