33using System ;
44using System . Threading ;
55using System . Threading . Tasks ;
6- using NServiceBus . Logging ;
7-
8- class RepeatedFailuresOverTimeCircuitBreaker : IDisposable
6+ using Logging ;
7+
8+ /// <summary>
9+ /// A circuit breaker that is armed on a failure and disarmed on success. After <see cref="timeToWaitBeforeTriggering"/> in the
10+ /// armed state, the <see cref="triggerAction"/> will fire. The <see cref="armedAction"/> and <see cref="disarmedAction"/> allow
11+ /// changing other state when the circuit breaker is armed or disarmed.
12+ /// </summary>
13+ sealed class RepeatedFailuresOverTimeCircuitBreaker
914{
10- public RepeatedFailuresOverTimeCircuitBreaker ( string name , TimeSpan timeToWaitBeforeTriggering , Action < Exception > triggerAction )
15+ /// <summary>
16+ /// A circuit breaker that is armed on a failure and disarmed on success. After <see cref="timeToWaitBeforeTriggering"/> in the
17+ /// armed state, the <see cref="triggerAction"/> will fire. The <see cref="armedAction"/> and <see cref="disarmedAction"/> allow
18+ /// changing other state when the circuit breaker is armed or disarmed.
19+ /// </summary>
20+ /// <param name="name">A name that is output in log messages when the circuit breaker changes states.</param>
21+ /// <param name="timeToWaitBeforeTriggering">The time to wait after the first failure before triggering.</param>
22+ /// <param name="triggerAction">The action to take when the circuit breaker is triggered.</param>
23+ /// <param name="armedAction">The action to execute on the first failure.
24+ /// <b>Warning:</b> This action is also invoked from within a lock. Any long-running, blocking, or I/O-bound code should be avoided
25+ /// within this action, as it can prevent other threads from proceeding, potentially leading to contention or performance bottlenecks.
26+ /// </param>
27+ /// <param name="disarmedAction">The action to execute when a success disarms the circuit breaker.
28+ /// <b>Warning:</b> This action is also invoked from within a lock. Any long-running, blocking, or I/O-bound code should be avoided
29+ /// within this action, as it can prevent other threads from proceeding, potentially leading to contention or performance bottlenecks.
30+ /// </param>
31+ /// <param name="timeToWaitWhenTriggered">How long to delay on each failure when in the Triggered state. Defaults to 10 seconds.</param>
32+ /// <param name="timeToWaitWhenArmed">How long to delay on each failure when in the Armed state. Defaults to 1 second.</param>
33+ /// <remarks>
34+ /// The <see cref="armedAction"/> and <see cref="disarmedAction"/> are invoked from within a lock to ensure that arming and disarming
35+ /// actions are serialized and do not execute concurrently. As a result, care must be taken to ensure that these actions do not
36+ /// introduce delays or deadlocks by performing lengthy operations or synchronously waiting on external resources.
37+ ///
38+ /// <b>Best practice:</b> If the logic inside these actions involves blocking or long-running tasks, consider offloading
39+ /// the work to a background task or thread that doesn't hold the lock.
40+ /// </remarks>
41+ public RepeatedFailuresOverTimeCircuitBreaker (
42+ string name ,
43+ TimeSpan timeToWaitBeforeTriggering ,
44+ Action < Exception > triggerAction ,
45+ Action armedAction = null ,
46+ Action disarmedAction = null ,
47+ TimeSpan timeToWaitWhenTriggered = default ,
48+ TimeSpan timeToWaitWhenArmed = default )
1149 {
1250 this . name = name ;
1351 this . triggerAction = triggerAction ;
52+ this . armedAction = armedAction ?? ( static ( ) => { } ) ;
53+ this . disarmedAction = disarmedAction ?? ( static ( ) => { } ) ;
1454 this . timeToWaitBeforeTriggering = timeToWaitBeforeTriggering ;
55+ this . timeToWaitWhenTriggered = timeToWaitWhenTriggered == TimeSpan . MinValue ? TimeSpan . FromSeconds ( 10 ) : timeToWaitWhenTriggered ;
56+ this . timeToWaitWhenArmed = timeToWaitWhenArmed == TimeSpan . MinValue ? TimeSpan . FromSeconds ( 1 ) : timeToWaitWhenArmed ;
1557
1658 timer = new Timer ( CircuitBreakerTriggered ) ;
1759 }
1860
19- public bool Triggered => triggered ;
20-
21- public void Dispose ( )
22- {
23- //Injected
24- }
25-
61+ /// <summary>
62+ /// Log a success, disarming the circuit breaker if it was previously armed.
63+ /// </summary>
2664 public void Success ( )
2765 {
28- var oldValue = Interlocked . Exchange ( ref failureCount , 0 ) ;
29-
30- if ( oldValue == 0 )
66+ // Check the status of the circuit breaker, exiting early outside the lock if already disarmed
67+ if ( Volatile . Read ( ref circuitBreakerState ) == Disarmed )
3168 {
3269 return ;
3370 }
3471
35- timer . Change ( Timeout . Infinite , Timeout . Infinite ) ;
36- triggered = false ;
37- Logger . InfoFormat ( "The circuit breaker for {0} is now disarmed" , name ) ;
72+ lock ( stateLock )
73+ {
74+ // Recheck state after obtaining the lock
75+ if ( circuitBreakerState == Disarmed )
76+ {
77+ return ;
78+ }
79+
80+ circuitBreakerState = Disarmed ;
81+
82+ _ = timer . Change ( Timeout . Infinite , Timeout . Infinite ) ;
83+ Logger . InfoFormat ( "The circuit breaker for '{0}' is now disarmed." , name ) ;
84+ try
85+ {
86+ disarmedAction ( ) ;
87+ }
88+ catch ( Exception ex )
89+ {
90+ Logger . Error ( $ "The circuit breaker for '{ name } ' was unable to execute the disarm action.", ex ) ;
91+ throw ;
92+ }
93+ }
3894 }
3995
96+ /// <summary>
97+ /// Log a failure, arming the circuit breaker if it was previously disarmed.
98+ /// </summary>
99+ /// <param name="exception">The exception that caused the failure.</param>
100+ /// <param name="cancellationToken">A cancellation token.</param>
40101 public Task Failure ( Exception exception , CancellationToken cancellationToken = default )
41102 {
42- lastException = exception ;
43- var newValue = Interlocked . Increment ( ref failureCount ) ;
103+ // Atomically store the exception that caused the circuit breaker to trip
104+ _ = Interlocked . Exchange ( ref lastException , exception ) ;
44105
45- if ( newValue == 1 )
106+ var previousState = Volatile . Read ( ref circuitBreakerState ) ;
107+ if ( previousState is Armed or Triggered )
46108 {
47- timer . Change ( timeToWaitBeforeTriggering , NoPeriodicTriggering ) ;
48- Logger . WarnFormat ( "The circuit breaker for {0} is now in the armed state" , name ) ;
109+ return Delay ( ) ;
49110 }
50111
51- var delay = Triggered ? ThrottledDelay : NonThrottledDelay ;
52- return Task . Delay ( delay , cancellationToken ) ;
112+ lock ( stateLock )
113+ {
114+ // Recheck state after obtaining the lock
115+ previousState = circuitBreakerState ;
116+ if ( previousState is Armed or Triggered )
117+ {
118+ return Delay ( ) ;
119+ }
120+
121+ circuitBreakerState = Armed ;
122+
123+ try
124+ {
125+ // Executing the action first before starting the timer to ensure that the action is executed before the timer fires
126+ // and the time of the action is not included in the time to wait before triggering.
127+ armedAction ( ) ;
128+ }
129+ catch ( Exception ex )
130+ {
131+ Logger . Error ( $ "The circuit breaker for '{ name } ' was unable to execute the arm action.", new AggregateException ( ex , exception ) ) ;
132+ throw ;
133+ }
134+
135+ _ = timer . Change ( timeToWaitBeforeTriggering , NoPeriodicTriggering ) ;
136+ Logger . WarnFormat ( "The circuit breaker for '{0}' is now in the armed state due to '{1}' and might trigger in '{2}' when not disarmed." , name , exception , timeToWaitBeforeTriggering ) ;
137+ }
138+
139+ return Delay ( ) ;
140+
141+ Task Delay ( )
142+ {
143+ var timeToWait = previousState == Triggered ? timeToWaitWhenTriggered : timeToWaitWhenArmed ;
144+ if ( Logger . IsDebugEnabled )
145+ {
146+ Logger . DebugFormat ( "The circuit breaker for '{0}' is delaying the operation by '{1}'." , name , timeToWait ) ;
147+ }
148+ return Task . Delay ( timeToWait , cancellationToken ) ;
149+ }
53150 }
54151
152+ /// <summary>
153+ /// Disposes the resources associated with the circuit breaker.
154+ /// </summary>
155+ public void Dispose ( ) => timer . Dispose ( ) ;
156+
55157 void CircuitBreakerTriggered ( object state )
56158 {
57- if ( Interlocked . Read ( ref failureCount ) > 0 )
159+ var previousState = Volatile . Read ( ref circuitBreakerState ) ;
160+ if ( previousState == Disarmed )
58161 {
59- triggered = true ;
60- Logger . WarnFormat ( "The circuit breaker for {0} will now be triggered" , name ) ;
162+ return ;
163+ }
164+
165+ lock ( stateLock )
166+ {
167+ // Recheck state after obtaining the lock
168+ if ( circuitBreakerState == Disarmed )
169+ {
170+ return ;
171+ }
172+
173+ circuitBreakerState = Triggered ;
174+ Logger . WarnFormat ( "The circuit breaker for '{0}' will now be triggered with exception '{1}'." , name , lastException ) ;
61175
62176 try
63177 {
64- triggerAction ( lastException ) ;
178+ triggerAction ( lastException ! ) ;
65179 }
66180 catch ( Exception ex )
67181 {
68- Logger . Error ( $ "Error invoking trigger action for circuit breaker { name } ", ex ) ;
182+ Logger . Fatal ( $ "The circuit breaker for ' { name } ' was unable to execute the trigger action. ", new AggregateException ( ex , lastException ! ) ) ;
69183 }
70184 }
71185 }
72186
187+ public bool IsTriggered => circuitBreakerState == Triggered ;
73188
74- string name ;
75- TimeSpan timeToWaitBeforeTriggering ;
76- Timer timer ;
77- Action < Exception > triggerAction ;
78- long failureCount ;
189+ int circuitBreakerState = Disarmed ;
79190 Exception lastException ;
80- volatile bool triggered ;
81191
82- static TimeSpan NoPeriodicTriggering = TimeSpan . FromMilliseconds ( - 1 ) ;
83- static ILog Logger = LogManager . GetLogger < RepeatedFailuresOverTimeCircuitBreaker > ( ) ;
84- static TimeSpan NonThrottledDelay = TimeSpan . FromSeconds ( 1 ) ;
85- static TimeSpan ThrottledDelay = TimeSpan . FromSeconds ( 10 ) ;
192+ readonly string name ;
193+ readonly Timer timer ;
194+ readonly TimeSpan timeToWaitBeforeTriggering ;
195+ readonly Action < Exception > triggerAction ;
196+ readonly Action armedAction ;
197+ readonly Action disarmedAction ;
198+ readonly TimeSpan timeToWaitWhenTriggered ;
199+ readonly TimeSpan timeToWaitWhenArmed ;
200+ readonly object stateLock = new ( ) ;
201+
202+ const int Disarmed = 0 ;
203+ const int Armed = 1 ;
204+ const int Triggered = 2 ;
205+
206+ static readonly TimeSpan NoPeriodicTriggering = TimeSpan . FromMilliseconds ( - 1 ) ;
207+ static readonly ILog Logger = LogManager . GetLogger < RepeatedFailuresOverTimeCircuitBreaker > ( ) ;
86208}
0 commit comments