Repository navigation
Expand file tree
/
Copy pathMultipleThreadLockManagerTests.cs
More file actions
178 lines (135 loc) · 5.62 KB
/
Copy pathMultipleThreadLockManagerTests.cs
File metadata and controls
178 lines (135 loc) · 5.62 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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using ThunderDesign.Net.Threading.Locks;
using Xunit;
namespace ThunderDesign.Net_PCL.Threading.UnitTests.Locks
{
public class MultipleThreadLockManagerTests
{
[Fact]
public async Task LockAsync_ShouldThrowArgumentNullException_WhenKeyIsNull()
{
using var manager = new MultipleThreadLocks<string, SingleThreadLock>();
await Assert.ThrowsAsync<ArgumentNullException>(() => manager.LockAsync(null!));
}
[Fact]
public void Unlock_ShouldThrowArgumentNullException_WhenKeyIsNull()
{
using var manager = new MultipleThreadLocks<string, SingleThreadLock>();
Assert.Throws<ArgumentNullException>(() => manager.Unlock(null!));
}
[Fact]
public void Unlock_ShouldThrowKeyNotFoundException_WhenKeyDoesNotExist()
{
using var manager = new MultipleThreadLocks<string, SingleThreadLock>();
Assert.Throws<KeyNotFoundException>(() => manager.Unlock("missing-key"));
}
[Fact]
public async Task Unlock_ShouldThrowSynchronizationLockException_WhenKeyLockIsNotHeld()
{
using var manager = new MultipleThreadLocks<string, SingleThreadLock>();
await manager.LockAsync("key1");
manager.Unlock("key1");
Assert.Throws<SynchronizationLockException>(() => manager.Unlock("key1"));
}
[Fact]
public async Task LockAsync_ShouldCreateSeparateLocks_ForDifferentKeys()
{
using var manager = new MultipleThreadLocks<string, SingleThreadLock>();
var lockTask1 = manager.LockAsync("key1");
var lockTask2 = manager.LockAsync("key2");
var waitTask = Task.Delay(1000);
var completed = await Task.WhenAny(Task.WhenAll(lockTask1, lockTask2), waitTask);
Assert.NotEqual(waitTask, completed);
}
[Fact]
public async Task LockAsync_ShouldBlock_ForSameKey_UntilUnlocked()
{
using var manager = new MultipleThreadLocks<string, SingleThreadLock>();
await manager.LockAsync("shared-key");
var secondLockTask = manager.LockAsync("shared-key");
var completedEarly = await Task.WhenAny(secondLockTask, Task.Delay(200)) == secondLockTask;
Assert.False(completedEarly);
manager.Unlock("shared-key");
var completed = await Task.WhenAny(secondLockTask, Task.Delay(1000)) == secondLockTask;
Assert.True(completed);
}
[Fact]
public async Task LockAsync_ShouldThrowOperationCanceledException_WhenCancelled()
{
using var manager = new MultipleThreadLocks<string, SingleThreadLock>();
await manager.LockAsync("key1");
using var cts = new CancellationTokenSource();
var waitingTask = manager.LockAsync("key1", cts.Token);
cts.Cancel();
await Assert.ThrowsAsync<OperationCanceledException>(() => waitingTask);
}
[Fact]
public async Task LockAsync_ShouldThrowObjectDisposedException_WhenManagerIsDisposedWhileWaiting()
{
var manager = new MultipleThreadLocks<string, SingleThreadLock>();
await manager.LockAsync("key1");
var waitingTask = manager.LockAsync("key1");
manager.Dispose();
await Assert.ThrowsAsync<ObjectDisposedException>(() => waitingTask);
}
[Fact]
public async Task LockAsync_ShouldThrowObjectDisposedException_WhenAlreadyDisposed()
{
var manager = new MultipleThreadLocks<string, SingleThreadLock>();
manager.Dispose();
await Assert.ThrowsAsync<ObjectDisposedException>(() => manager.LockAsync("key1"));
}
[Fact]
public void Unlock_ShouldThrowObjectDisposedException_WhenAlreadyDisposed()
{
var manager = new MultipleThreadLocks<string, SingleThreadLock>();
manager.Dispose();
Assert.Throws<ObjectDisposedException>(() => manager.Unlock("key1"));
}
[Fact]
public void Dispose_ShouldBeIdempotent()
{
var manager = new MultipleThreadLocks<string, SingleThreadLock>();
manager.Dispose();
var exception = Record.Exception(() => manager.Dispose());
Assert.Null(exception);
}
[Fact]
public async Task LockAsync_ShouldOnlyAllowOneThreadPerKey_UnderConcurrentAccess()
{
using var manager = new MultipleThreadLocks<string, SingleThreadLock>();
var counter = 0;
var maxObservedConcurrency = 0;
var gate = new object();
async Task Worker()
{
await manager.LockAsync("shared-key");
try
{
lock (gate)
{
counter++;
maxObservedConcurrency = Math.Max(maxObservedConcurrency, counter);
}
await Task.Delay(20);
lock (gate)
{
counter--;
}
}
finally
{
manager.Unlock("shared-key");
}
}
var tasks = new Task[10];
for (int i = 0; i < tasks.Length; i++)
tasks[i] = Worker();
await Task.WhenAll(tasks);
Assert.Equal(1, maxObservedConcurrency);
}
}
}