Skip to content

Commit b4485e4

Browse files
committed
feat: improve circuit breaker resilience and recovery
- Increase failure threshold from 5 to 8 for better tolerance - Reduce base cooldown from 30s to 10s for faster recovery - Add exponential backoff with cooldown multiplier (up to 8x) - Implement consecutive success tracking in half-open state - Add gradual recovery mechanism to reduce failures over time - Enhance logging with detailed state information - Add manual reset capability for debugging This improves MCP connection stability when Obsidian API is temporarily unavailable
1 parent 799bffe commit b4485e4

File tree

1 file changed

+80
-9
lines changed

1 file changed

+80
-9
lines changed

src/obsidian.ts

Lines changed: 80 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -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
115116
class 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

Comments
 (0)