-
Notifications
You must be signed in to change notification settings - Fork 247
Expand file tree
/
Copy pathExternalConnectionOrTransactionStrategyTestCases.cs
More file actions
107 lines (90 loc) · 4.56 KB
/
ExternalConnectionOrTransactionStrategyTestCases.cs
File metadata and controls
107 lines (90 loc) · 4.56 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
using NUnit.Framework;
using System.Data;
using System.Data.Common;
using System.Reflection;
namespace Medallion.Threading.Tests.Data;
public abstract class ExternalConnectionOrTransactionStrategyTestCases<TLockProvider, TStrategy, TDb>
where TLockProvider : TestingLockProvider<TStrategy>, new()
where TStrategy : TestingExternalConnectionOrTransactionSynchronizationStrategy<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]
[NonParallelizable, Retry(tryCount: 3)] // timing sensitive for SqlSemaphore (see comment in that file regarding the 32ms wait)
public async Task TestDeadlockDetection()
{
var timeout = TimeSpan.FromSeconds(20);
using var barrier = new Barrier(participantCount: 2);
const string LockName1 = nameof(TestDeadlockDetection) + "_1",
LockName2 = nameof(TestDeadlockDetection) + "_2";
Task RunDeadlockAsync(bool isFirst)
{
this._lockProvider.Strategy.StartAmbient();
var lock1 = this._lockProvider.CreateLock(isFirst ? LockName1 : LockName2);
var lock2 = this._lockProvider.CreateLock(isFirst ? LockName2 : LockName1);
return Task.Run(async () =>
{
using (await lock1.AcquireAsync(timeout))
{
barrier.SignalAndWait();
(await lock2.AcquireAsync(timeout)).Dispose();
}
});
}
var tasks = new[] { RunDeadlockAsync(isFirst: true), RunDeadlockAsync(isFirst: false) };
(await Task.WhenAll(tasks).ContinueWith(_ => { }).TryWaitAsync(TimeSpan.FromSeconds(15))).ShouldEqual(true, this.GetType().Name);
// MariaDB fails both tasks due to deadlock instead of just picking a single victim
Assert.That(tasks.Count(t => t.IsFaulted), Is.GreaterThanOrEqualTo(1));
Assert.That(tasks.Count(t => t.Status == TaskStatus.RanToCompletion), Is.LessThanOrEqualTo(1));
Assert.That(tasks.Where(t => t.IsCanceled), Is.Empty);
foreach (var deadlockVictim in tasks.Where(t => t.IsFaulted))
{
Assert.That(deadlockVictim.Exception!.GetBaseException(), Is.InstanceOf<InvalidOperationException>()); // backwards compat check
Assert.That(deadlockVictim.Exception.GetBaseException(), Is.InstanceOf<DeadlockException>());
}
}
[Test]
public async Task TestReAcquireLockOnSameConnection()
{
var @lock = this._lockProvider.CreateLock("lock");
await using var handle = await @lock.AcquireAsync();
Assert.ThrowsAsync<DeadlockException>(() => @lock.AcquireAsync().AsTask());
Assert.ThrowsAsync<TimeoutException>(() => @lock.AcquireAsync(TimeSpan.FromSeconds(.01)).AsTask());
}
/// <summary>
/// Currently, we leverage <see cref="DbConnection.StateChange"/> to track handle loss. This test
/// validates that the handler is properly removed when the lock handle is disposed
/// </summary>
[Test]
public void TestStateChangeHandlerIsNotLeaked()
{
this._lockProvider.Strategy.StartAmbient();
// creating this first assures that the Semaphore5 provider's handlers get included in initial
var @lock = this._lockProvider.CreateLock(nameof(TestStateChangeHandlerIsNotLeaked));
var initialHandler = GetStateChanged(this._lockProvider.Strategy.AmbientConnection!);
using (@lock.Acquire())
{
Assert.That(GetStateChanged(this._lockProvider.Strategy.AmbientConnection!), Is.Not.Null);
}
GetStateChanged(this._lockProvider.Strategy.AmbientConnection!).ShouldEqual(initialHandler);
static StateChangeEventHandler? GetStateChanged(DbConnection connection) =>
// We check both the connection type and the base type because OracleConnection overrides the storage for
// the StateChange event handler
(StateChangeEventHandler?)new[] { connection.GetType(), typeof(DbConnection) }
.Select(
t => t.GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.DeclaredOnly)
.Where(f => f.FieldType == typeof(StateChangeEventHandler))
.SingleOrDefault()
)
.First(f => f != null)!
.GetValue(connection);
}
}