@@ -112,20 +112,30 @@ const ListFilesInputSchema = z.object({
112112} ) ;
113113
114114// Circuit breaker for handling repeated failures
115+ // Circuit breaker for handling repeated failures with improved recovery
115116class CircuitBreaker {
116117 private failures = 0 ;
117118 private lastFailureTime = 0 ;
118119 private state : "closed" | "open" | "half-open" = "closed" ;
120+ private consecutiveSuccesses = 0 ;
121+ private cooldownMultiplier = 1 ;
122+ private readonly maxCooldownMultiplier = 8 ;
123+ private readonly halfOpenSuccessThreshold = 2 ;
119124
120125 constructor (
121- private failureThreshold = 5 ,
122- private cooldownMs = 30000 // 30 seconds
126+ private failureThreshold = 8 , // Increased from 5 for better tolerance
127+ private baseCooldownMs = 10000 // Reduced from 30s to 10s for faster recovery
123128 ) { }
124129
125130 isOpen ( ) : boolean {
126131 if ( this . state === "open" ) {
127- if ( Date . now ( ) - this . lastFailureTime > this . cooldownMs ) {
132+ const currentCooldown = this . baseCooldownMs * this . cooldownMultiplier ;
133+ if ( Date . now ( ) - this . lastFailureTime > currentCooldown ) {
128134 this . state = "half-open" ;
135+ logObsidianEvent ( "info" , "circuit breaker entering half-open state" , {
136+ cooldownMs : currentCooldown ,
137+ multiplier : this . cooldownMultiplier ,
138+ } ) ;
129139 return false ;
130140 }
131141 return true ;
@@ -134,25 +144,86 @@ class CircuitBreaker {
134144 }
135145
136146 recordSuccess ( ) : void {
137- this . failures = 0 ;
138- this . state = "closed" ;
147+ if ( this . state === "half-open" ) {
148+ this . consecutiveSuccesses ++ ;
149+ if ( this . consecutiveSuccesses >= this . halfOpenSuccessThreshold ) {
150+ // Require multiple successes in half-open before fully closing
151+ this . state = "closed" ;
152+ this . failures = 0 ;
153+ this . consecutiveSuccesses = 0 ;
154+ this . cooldownMultiplier = Math . max ( 1 , this . cooldownMultiplier / 2 ) ; // Gradually reduce multiplier
155+ logObsidianEvent ( "info" , "circuit breaker closed after successful recovery" , {
156+ consecutiveSuccesses : this . consecutiveSuccesses ,
157+ newMultiplier : this . cooldownMultiplier ,
158+ } ) ;
159+ }
160+ } else if ( this . state === "closed" ) {
161+ // Gradual recovery in closed state
162+ if ( this . failures > 0 ) {
163+ this . failures = Math . max ( 0 , this . failures - 1 ) ;
164+ }
165+ this . consecutiveSuccesses ++ ;
166+ }
139167 }
140168
141169 recordFailure ( ) : void {
142170 this . failures ++ ;
143171 this . lastFailureTime = Date . now ( ) ;
172+ this . consecutiveSuccesses = 0 ;
144173
145- if ( this . failures >= this . failureThreshold ) {
174+ if ( this . state === "half-open" ) {
175+ // Immediate open on failure in half-open state
146176 this . state = "open" ;
177+ this . cooldownMultiplier = Math . min (
178+ this . maxCooldownMultiplier ,
179+ this . cooldownMultiplier * 2
180+ ) ;
181+ logObsidianEvent ( "warn" , "circuit breaker reopened from half-open state" , {
182+ failures : this . failures ,
183+ cooldownMultiplier : this . cooldownMultiplier ,
184+ } ) ;
185+ } else if ( this . failures >= this . failureThreshold ) {
186+ this . state = "open" ;
187+ logObsidianEvent ( "error" , "circuit breaker opened after threshold reached" , {
188+ failures : this . failures ,
189+ threshold : this . failureThreshold ,
190+ cooldownMs : this . baseCooldownMs * this . cooldownMultiplier ,
191+ } ) ;
147192 }
148193 }
149194
150- getStatus ( ) : { state : string ; failures : number ; lastFailure : number } {
151- return {
195+ reset ( ) : void {
196+ this . failures = 0 ;
197+ this . state = "closed" ;
198+ this . consecutiveSuccesses = 0 ;
199+ this . cooldownMultiplier = 1 ;
200+ this . lastFailureTime = 0 ;
201+ logObsidianEvent ( "info" , "circuit breaker manually reset" ) ;
202+ }
203+
204+ getStatus ( ) : {
205+ state : string ;
206+ failures : number ;
207+ lastFailure : number ;
208+ consecutiveSuccesses : number ;
209+ cooldownMultiplier : number ;
210+ nextRetryIn ?: number ;
211+ } {
212+ const status = {
152213 state : this . state ,
153214 failures : this . failures ,
154215 lastFailure : this . lastFailureTime ,
155- } ;
216+ consecutiveSuccesses : this . consecutiveSuccesses ,
217+ cooldownMultiplier : this . cooldownMultiplier ,
218+ } as any ;
219+
220+ if ( this . state === "open" && this . lastFailureTime > 0 ) {
221+ const currentCooldown = this . baseCooldownMs * this . cooldownMultiplier ;
222+ const timeSinceFailure = Date . now ( ) - this . lastFailureTime ;
223+ status . nextRetryIn = Math . max ( 0 , currentCooldown - timeSinceFailure ) ;
224+ }
225+
226+ return status ;
156227 }
157228}
158229
0 commit comments