Skip to content

Commit 0009001

Browse files
committed
Fix race condition in RepeatedFailuresOverTimeCircuitBreaker
1 parent dbc65a4 commit 0009001

File tree

2 files changed

+394
-40
lines changed

2 files changed

+394
-40
lines changed
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
namespace NServiceBus.Transport.AzureStorageQueues.Tests
2+
{
3+
using System;
4+
using System.Diagnostics;
5+
using System.Linq;
6+
using System.Threading;
7+
using System.Threading.Tasks;
8+
using NUnit.Framework;
9+
using NServiceBus.Transport.AzureStorageQueues;
10+
11+
// Ideally the circuit breaker would use a time provider to allow for easier testing but that would require a significant refactor
12+
// and we want keep the changes to a minimum for now to allow backporting to older versions.
13+
[TestFixture]
14+
public class RepeatedFailuresOverTimeCircuitBreakerTests
15+
{
16+
[Test]
17+
public async Task Should_disarm_on_success()
18+
{
19+
var armedActionCalled = false;
20+
var disarmedActionCalled = false;
21+
22+
var circuitBreaker = new RepeatedFailuresOverTimeCircuitBreaker(
23+
"TestCircuitBreaker",
24+
TimeSpan.FromMilliseconds(100),
25+
ex => { },
26+
() => armedActionCalled = true,
27+
() => disarmedActionCalled = true,
28+
TimeSpan.Zero,
29+
TimeSpan.Zero
30+
);
31+
32+
await circuitBreaker.Failure(new Exception("Test Exception"));
33+
circuitBreaker.Success();
34+
35+
Assert.That(armedActionCalled, Is.True, "The armed action should be called.");
36+
Assert.That(disarmedActionCalled, Is.True, "The disarmed action should be called.");
37+
}
38+
39+
[Test]
40+
public async Task Should_rethrow_exception_on_success()
41+
{
42+
var circuitBreaker = new RepeatedFailuresOverTimeCircuitBreaker(
43+
"TestCircuitBreaker",
44+
TimeSpan.FromMilliseconds(100),
45+
ex => { },
46+
() => { },
47+
() => throw new Exception("Exception from disarmed action"),
48+
timeToWaitWhenTriggered: TimeSpan.Zero,
49+
timeToWaitWhenArmed: TimeSpan.Zero
50+
);
51+
52+
await circuitBreaker.Failure(new Exception("Test Exception"));
53+
54+
var ex = Assert.Throws<Exception>(() => circuitBreaker.Success());
55+
Assert.That(ex.Message, Is.EqualTo("Exception from disarmed action"));
56+
}
57+
58+
[Test]
59+
public async Task Should_trigger_after_failure_timeout()
60+
{
61+
var triggerActionCalled = false;
62+
Exception lastTriggerException = null;
63+
64+
var circuitBreaker = new RepeatedFailuresOverTimeCircuitBreaker(
65+
"TestCircuitBreaker",
66+
TimeSpan.Zero,
67+
ex => { triggerActionCalled = true; lastTriggerException = ex; },
68+
timeToWaitWhenTriggered: TimeSpan.Zero,
69+
timeToWaitWhenArmed: TimeSpan.FromMilliseconds(100)
70+
);
71+
72+
await circuitBreaker.Failure(new Exception("Test Exception"));
73+
74+
Assert.That(triggerActionCalled, Is.True, "The trigger action should be called after timeout.");
75+
Assert.That(lastTriggerException, Is.Not.Null, "The exception passed to the trigger action should not be null.");
76+
}
77+
78+
[Test]
79+
public void Should_rethrow_exception_on_failure()
80+
{
81+
var circuitBreaker = new RepeatedFailuresOverTimeCircuitBreaker(
82+
"TestCircuitBreaker",
83+
TimeSpan.FromMilliseconds(100),
84+
ex => { },
85+
() => throw new Exception("Exception from armed action"),
86+
() => { },
87+
timeToWaitWhenTriggered: TimeSpan.Zero,
88+
timeToWaitWhenArmed: TimeSpan.Zero
89+
);
90+
91+
var ex = Assert.ThrowsAsync<Exception>(async () => await circuitBreaker.Failure(new Exception("Test Exception")));
92+
Assert.That(ex.Message, Is.EqualTo("Exception from armed action"));
93+
}
94+
95+
[Test]
96+
public async Task Should_delay_after_trigger_failure()
97+
{
98+
var timeToWaitWhenTriggered = TimeSpan.FromMilliseconds(50);
99+
var timeToWaitWhenArmed = TimeSpan.FromMilliseconds(100);
100+
101+
var circuitBreaker = new RepeatedFailuresOverTimeCircuitBreaker(
102+
"TestCircuitBreaker",
103+
TimeSpan.Zero,
104+
_ => { },
105+
timeToWaitWhenTriggered: timeToWaitWhenTriggered,
106+
timeToWaitWhenArmed: timeToWaitWhenArmed
107+
);
108+
109+
var stopWatch = Stopwatch.StartNew();
110+
111+
await circuitBreaker.Failure(new Exception("Test Exception"));
112+
await circuitBreaker.Failure(new Exception("Test Exception After Trigger"));
113+
114+
stopWatch.Stop();
115+
116+
Assert.That(stopWatch.ElapsedMilliseconds, Is.GreaterThanOrEqualTo(timeToWaitWhenTriggered.Add(timeToWaitWhenArmed).TotalMilliseconds).Within(20), "The circuit breaker should delay after a triggered failure.");
117+
}
118+
119+
[Test]
120+
public async Task Should_not_trigger_if_disarmed_before_timeout()
121+
{
122+
var triggerActionCalled = false;
123+
124+
var circuitBreaker = new RepeatedFailuresOverTimeCircuitBreaker(
125+
"TestCircuitBreaker",
126+
TimeSpan.FromMilliseconds(100),
127+
ex => triggerActionCalled = true,
128+
timeToWaitWhenTriggered: TimeSpan.Zero,
129+
timeToWaitWhenArmed: TimeSpan.Zero
130+
);
131+
132+
await circuitBreaker.Failure(new Exception("Test Exception"));
133+
circuitBreaker.Success();
134+
135+
Assert.That(triggerActionCalled, Is.False, "The trigger action should not be called if the circuit breaker was disarmed.");
136+
}
137+
138+
[Test]
139+
public async Task Should_handle_concurrent_failure_and_success()
140+
{
141+
var armedActionCalled = false;
142+
var disarmedActionCalled = false;
143+
var triggerActionCalled = false;
144+
145+
var circuitBreaker = new RepeatedFailuresOverTimeCircuitBreaker(
146+
"TestCircuitBreaker",
147+
TimeSpan.FromMilliseconds(100),
148+
ex => triggerActionCalled = true,
149+
() => armedActionCalled = true,
150+
() => disarmedActionCalled = true,
151+
TimeSpan.Zero,
152+
TimeSpan.Zero
153+
);
154+
155+
var failureTask = circuitBreaker.Failure(new Exception("Test Exception"));
156+
var successTask = Task.Run(() =>
157+
{
158+
Thread.Sleep(50); // Simulate some delay before success
159+
circuitBreaker.Success();
160+
});
161+
162+
await Task.WhenAll(failureTask, successTask);
163+
164+
Assert.That(armedActionCalled, Is.True, "The armed action should be called.");
165+
Assert.That(disarmedActionCalled, Is.True, "The disarmed action should be called.");
166+
Assert.That(triggerActionCalled, Is.False, "The trigger action should not be called if success occurred before timeout.");
167+
}
168+
169+
[Test]
170+
public async Task Should_handle_high_concurrent_failure_and_success()
171+
{
172+
var armedActionCalled = 0;
173+
var disarmedActionCalled = 0;
174+
var triggerActionCalled = 0;
175+
176+
var circuitBreaker = new RepeatedFailuresOverTimeCircuitBreaker(
177+
"TestCircuitBreaker",
178+
TimeSpan.FromSeconds(5),
179+
ex => Interlocked.Increment(ref triggerActionCalled),
180+
() => Interlocked.Increment(ref armedActionCalled),
181+
() => Interlocked.Increment(ref disarmedActionCalled),
182+
TimeSpan.Zero,
183+
TimeSpan.FromMilliseconds(25)
184+
);
185+
186+
var tasks = Enumerable.Range(0, 1000)
187+
.Select(
188+
i => i % 2 == 0 ?
189+
circuitBreaker.Failure(new Exception($"Test Exception {i}")) :
190+
Task.Run(() =>
191+
{
192+
Thread.Sleep(25); // Simulate some delay before success
193+
circuitBreaker.Success();
194+
})
195+
).ToArray();
196+
197+
await Task.WhenAll(tasks);
198+
199+
Assert.That(armedActionCalled, Is.EqualTo(1), "The armed action should be called.");
200+
Assert.That(disarmedActionCalled, Is.EqualTo(1), "The disarmed action should be called.");
201+
Assert.That(triggerActionCalled, Is.Zero, "The trigger action should not be called if success occurred before timeout.");
202+
}
203+
204+
[Test]
205+
public async Task Should_trigger_after_multiple_failures_and_timeout()
206+
{
207+
var triggerActionCalled = false;
208+
209+
var circuitBreaker = new RepeatedFailuresOverTimeCircuitBreaker(
210+
"TestCircuitBreaker",
211+
TimeSpan.FromMilliseconds(50),
212+
ex => triggerActionCalled = true,
213+
timeToWaitWhenTriggered: TimeSpan.FromMilliseconds(50),
214+
timeToWaitWhenArmed: TimeSpan.FromMilliseconds(50)
215+
);
216+
217+
await circuitBreaker.Failure(new Exception("Test Exception"));
218+
await circuitBreaker.Failure(new Exception("Another Exception After Trigger"));
219+
220+
Assert.That(triggerActionCalled, Is.True, "The trigger action should be called after repeated failures and timeout.");
221+
}
222+
}
223+
}

0 commit comments

Comments
 (0)