-
Notifications
You must be signed in to change notification settings - Fork 247
Expand file tree
/
Copy pathExternalTransactionStrategyTestCases.cs
More file actions
152 lines (125 loc) · 6.59 KB
/
ExternalTransactionStrategyTestCases.cs
File metadata and controls
152 lines (125 loc) · 6.59 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
using NUnit.Framework;
using System.Data.Common;
using System.Runtime.CompilerServices;
namespace Medallion.Threading.Tests.Data;
public abstract class ExternalTransactionStrategyTestCases<TLockProvider, TDb>
where TLockProvider : TestingLockProvider<TestingExternalTransactionSynchronizationStrategy<TDb>>, new()
where TDb : TestingDb, new()
{
private TLockProvider _lockProvider = default!;
[SetUp]
public async Task SetUp()
{
this._lockProvider = new TLockProvider();
await this._lockProvider.SetupAsync();
}
[TearDown]
public async Task TearDown() => await this._lockProvider.DisposeAsync();
[Test]
public async Task TestScopedToTransactionOnly()
{
this._lockProvider.Strategy.StartAmbient();
var ambientTransactionLock = this._lockProvider.CreateLock(nameof(TestScopedToTransactionOnly));
using (ambientTransactionLock.Acquire())
{
Assert.That(this._lockProvider.CreateLock(nameof(TestScopedToTransactionOnly)).IsHeld(), Is.True);
// create a lock of the same type on the underlying connection of the ambient transaction
await using dynamic specificConnectionProvider = Activator.CreateInstance(
ReplaceGenericParameter(typeof(TLockProvider), this._lockProvider.Strategy.GetType(), typeof(SpecificConnectionStrategy))
)!;
specificConnectionProvider.Strategy.Test = this;
Assert.Catch<InvalidOperationException>(() => ((IDistributedLock)specificConnectionProvider.CreateLock(nameof(TestScopedToTransactionOnly))).Acquire());
}
static Type ReplaceGenericParameter(Type type, Type old, Type @new)
{
if (type == old) { return @new; }
if (!type.IsConstructedGenericType) { return type; }
var newGenericArguments = type.GetGenericArguments()
.Select(a => ReplaceGenericParameter(a, old, @new))
.ToArray();
return type.GetGenericTypeDefinition()
.MakeGenericType(newGenericArguments);
}
}
/// <summary>
/// Special strategy designed to allow us to make connection-scoped locks using the same connection as
/// the ambient transaction from our own <see cref="_lockProvider"/>
/// </summary>
private class SpecificConnectionStrategy : TestingDbSynchronizationStrategy<TDb>
{
public SpecificConnectionStrategy()
{
Console.WriteLine("SpecificConnectionStrategy created");
}
public ExternalTransactionStrategyTestCases<TLockProvider, TDb>? Test { get; set; }
public override TestingDbConnectionOptions GetConnectionOptions() =>
new() { Connection = this.Test!._lockProvider.Strategy.AmbientTransaction!.Connection };
}
public void TestCloseTransactionLockOnClosedConnectionOrTransaction([Values] bool closeConnection)
{
var lockName = closeConnection ? "Connection" : "Transaction";
var nonAmbientTransactionLock = this._lockProvider.CreateLock(lockName);
// Disable pooling for the ambient connection. This is important because we want to show that the lock
// will get released; in reality for a pooled connection in this scenario the lock-holding connection will
// return to the pool and would get released the next time that connection was fetched from the pool
this._lockProvider.Strategy.Db.ConnectionStringBuilder["Pooling"] = false;
this._lockProvider.Strategy.StartAmbient();
var ambientTransactionLock = this._lockProvider.CreateLock(lockName);
using var handle = ambientTransactionLock.Acquire();
Assert.That(nonAmbientTransactionLock.IsHeld(), Is.True);
if (closeConnection)
{
this._lockProvider.Strategy.AmbientTransaction!.Connection!.Dispose();
}
else
{
this._lockProvider.Strategy.AmbientTransaction!.Dispose();
}
Assert.DoesNotThrow(handle.Dispose);
// now lock can be re-acquired
Assert.That(nonAmbientTransactionLock.IsHeld(), Is.False);
}
[Test]
public void TestLockOnRolledBackTransaction() => this.TestLockOnCompletedTransactionHelper(t => t.Rollback());
[Test]
public void TestLockOnCommittedTransaction() => this.TestLockOnCompletedTransactionHelper(t => t.Commit());
[Test]
public void TestLockOnDisposedTransaction() => this.TestLockOnCompletedTransactionHelper(t => t.Dispose());
private void TestLockOnCompletedTransactionHelper(Action<DbTransaction> complete, [CallerMemberName] string lockName = "")
{
var nonAmbientTransactionLock = this._lockProvider.CreateLock(lockName);
// Disable pooling for the ambient connection. This is important because we want to show that the lock
// will get released; in reality for a pooled connection in this scenario the lock-holding connection will
// return to the pool and would get released the next time that connection was fetched from the pool
this._lockProvider.Strategy.Db.ConnectionStringBuilder["Pooling"] = false;
this._lockProvider.Strategy.StartAmbient();
var ambientTransactionLock = this._lockProvider.CreateLock(lockName);
using var handle = ambientTransactionLock.Acquire();
Assert.That(nonAmbientTransactionLock.IsHeld(), Is.True);
complete(this._lockProvider.Strategy.AmbientTransaction!);
var transactionSupport = this._lockProvider.Strategy.Db.TransactionSupport;
if (transactionSupport == TransactionSupport.ExplicitParticipation)
{
// this will throw because the lock will still be trying to use the transaction and we've ended it
Assert.Throws<InvalidOperationException>(handle.Dispose);
}
else
{
Assert.DoesNotThrow(handle.Dispose);
}
nonAmbientTransactionLock.IsHeld()
// explicit participation will fail to release above, so it is still held
.ShouldEqual(transactionSupport == TransactionSupport.ExplicitParticipation ? true : false);
if (transactionSupport == TransactionSupport.ImplicitParticipation)
{
// If we use transactions implicitly then we can keep using our lock without issue
// because we're just using the underlyign connection which is still good.
Assert.DoesNotThrow(() => ambientTransactionLock.Acquire().Dispose());
}
else
{
// Otherwise we'll fail to use a transaction that has been ended
Assert.Catch<InvalidOperationException>(() => ambientTransactionLock.Acquire());
}
}
}