1+ namespace NServiceBus ;
2+
3+ using System ;
4+ using System . Threading ;
5+ using System . Threading . Tasks ;
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+ public sealed class RepeatedFailuresOverTimeCircuitBreaker
14+ {
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 )
49+ {
50+ this . name = name ;
51+ this . triggerAction = triggerAction ;
52+ this . armedAction = armedAction ?? ( static ( ) => { } ) ;
53+ this . disarmedAction = disarmedAction ?? ( static ( ) => { } ) ;
54+ this . timeToWaitBeforeTriggering = timeToWaitBeforeTriggering ;
55+ this . timeToWaitWhenTriggered = timeToWaitWhenTriggered == TimeSpan . MinValue ? TimeSpan . FromSeconds ( 10 ) : timeToWaitWhenTriggered ;
56+ this . timeToWaitWhenArmed = timeToWaitWhenArmed == TimeSpan . MinValue ? TimeSpan . FromSeconds ( 1 ) : timeToWaitWhenArmed ;
57+
58+ timer = new Timer ( CircuitBreakerTriggered ) ;
59+ }
60+
61+ /// <summary>
62+ /// Log a success, disarming the circuit breaker if it was previously armed.
63+ /// </summary>
64+ public void Success ( )
65+ {
66+ // Check the status of the circuit breaker, exiting early outside the lock if already disarmed
67+ if ( Volatile . Read ( ref circuitBreakerState ) == Disarmed )
68+ {
69+ return ;
70+ }
71+
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+ }
94+ }
95+
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>
101+ public Task Failure ( Exception exception , CancellationToken cancellationToken = default )
102+ {
103+ // Atomically store the exception that caused the circuit breaker to trip
104+ _ = Interlocked . Exchange ( ref lastException , exception ) ;
105+
106+ var previousState = Volatile . Read ( ref circuitBreakerState ) ;
107+ if ( previousState is Armed or Triggered )
108+ {
109+ return Delay ( ) ;
110+ }
111+
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+ }
150+ }
151+
152+ void CircuitBreakerTriggered ( object state )
153+ {
154+ var previousState = Volatile . Read ( ref circuitBreakerState ) ;
155+ if ( previousState == Disarmed )
156+ {
157+ return ;
158+ }
159+
160+ lock ( stateLock )
161+ {
162+ // Recheck state after obtaining the lock
163+ if ( circuitBreakerState == Disarmed )
164+ {
165+ return ;
166+ }
167+
168+ circuitBreakerState = Triggered ;
169+ Logger . WarnFormat ( "The circuit breaker for '{0}' will now be triggered with exception '{1}'." , name , lastException ) ;
170+
171+ try
172+ {
173+ triggerAction ( lastException ! ) ;
174+ }
175+ catch ( Exception ex )
176+ {
177+ Logger . Fatal ( $ "The circuit breaker for '{ name } ' was unable to execute the trigger action.", new AggregateException ( ex , lastException ! ) ) ;
178+ }
179+ }
180+ }
181+
182+ int circuitBreakerState = Disarmed ;
183+ Exception lastException ;
184+
185+ readonly string name ;
186+ readonly Timer timer ;
187+ readonly TimeSpan timeToWaitBeforeTriggering ;
188+ readonly Action < Exception > triggerAction ;
189+ readonly Action armedAction ;
190+ readonly Action disarmedAction ;
191+ readonly TimeSpan timeToWaitWhenTriggered ;
192+ readonly TimeSpan timeToWaitWhenArmed ;
193+ readonly object stateLock = new ( ) ;
194+
195+ const int Disarmed = 0 ;
196+ const int Armed = 1 ;
197+ const int Triggered = 2 ;
198+
199+ static readonly TimeSpan NoPeriodicTriggering = TimeSpan . FromMilliseconds ( - 1 ) ;
200+ static readonly ILog Logger = LogManager . GetLogger < RepeatedFailuresOverTimeCircuitBreaker > ( ) ;
201+ }
0 commit comments