forked from elizaOS/eliza
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: (core) Add circuit breaker pattern for database operations
Implements circuit breaker pattern to handle database failures gracefully and prevent cascading failures. Fixes elizaOS#712. Changes: - Adds CircuitBreaker class with CLOSED, OPEN, and HALF-OPEN states - Introduces BaseCircuitBreakerAdapter for database adapters - Configurable failure thresholds and recovery timeouts - Automatic recovery attempts in HALF-OPEN state - Detailed logging of circuit breaker state changes Circuit breaker configuration: - Opens after 5 consecutive failures (configurable) - Resets after 60 seconds in OPEN state - Requires 3 successful operations in HALF-OPEN state to close This helps prevent overwhelming failed database connections and provides graceful degradation during outages.
- Loading branch information
Showing
4 changed files
with
169 additions
and
52 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import { CircuitBreaker } from "./CircuitBreaker"; | ||
import { elizaLogger } from "../logger"; | ||
import { DatabaseAdapter } from "../database"; | ||
|
||
export abstract class BaseCircuitBreakerAdapter<T> extends DatabaseAdapter<T> { | ||
protected circuitBreaker: CircuitBreaker; | ||
|
||
constructor(circuitBreakerConfig?: { | ||
failureThreshold?: number; | ||
resetTimeout?: number; | ||
halfOpenMaxAttempts?: number; | ||
}) { | ||
super(); | ||
this.circuitBreaker = new CircuitBreaker(circuitBreakerConfig); | ||
} | ||
|
||
protected async withCircuitBreaker<T>( | ||
operation: () => Promise<T>, | ||
context: string | ||
): Promise<T> { | ||
try { | ||
return await this.circuitBreaker.execute(operation); | ||
} catch (error) { | ||
elizaLogger.error(`Circuit breaker error in ${context}:`, { | ||
error: error instanceof Error ? error.message : String(error), | ||
state: this.circuitBreaker.getState(), | ||
}); | ||
throw error; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
export class CircuitBreaker { | ||
private state: "CLOSED" | "OPEN" | "HALF_OPEN" = "CLOSED"; | ||
private failureCount: number = 0; | ||
private lastFailureTime?: number; | ||
|
||
private readonly failureThreshold: number; | ||
private readonly resetTimeout: number; | ||
private readonly halfOpenMaxAttempts: number; | ||
private halfOpenSuccesses: number = 0; | ||
|
||
constructor( | ||
config: { | ||
failureThreshold?: number; | ||
resetTimeout?: number; | ||
halfOpenMaxAttempts?: number; | ||
} = {} | ||
) { | ||
this.failureThreshold = config.failureThreshold ?? 5; | ||
this.resetTimeout = config.resetTimeout ?? 60000; // 1 minute | ||
this.halfOpenMaxAttempts = config.halfOpenMaxAttempts ?? 3; | ||
} | ||
|
||
async execute<T>(operation: () => Promise<T>): Promise<T> { | ||
if (this.state === "OPEN") { | ||
if (Date.now() - (this.lastFailureTime || 0) > this.resetTimeout) { | ||
this.state = "HALF_OPEN"; | ||
this.halfOpenSuccesses = 0; | ||
} else { | ||
throw new Error("Circuit breaker is OPEN"); | ||
} | ||
} | ||
|
||
try { | ||
const result = await operation(); | ||
|
||
if (this.state === "HALF_OPEN") { | ||
this.halfOpenSuccesses++; | ||
if (this.halfOpenSuccesses >= this.halfOpenMaxAttempts) { | ||
this.reset(); | ||
} | ||
} | ||
|
||
return result; | ||
} catch (error) { | ||
this.handleFailure(); | ||
throw error; | ||
} | ||
} | ||
|
||
private handleFailure(): void { | ||
this.failureCount++; | ||
this.lastFailureTime = Date.now(); | ||
|
||
if (this.failureCount >= this.failureThreshold) { | ||
this.state = "OPEN"; | ||
} | ||
} | ||
|
||
private reset(): void { | ||
this.state = "CLOSED"; | ||
this.failureCount = 0; | ||
this.lastFailureTime = undefined; | ||
} | ||
|
||
getState(): "CLOSED" | "OPEN" | "HALF_OPEN" { | ||
return this.state; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters