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