@juspay/neurolink
Version:
Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio
432 lines • 14.6 kB
JavaScript
/**
* RAG Circuit Breaker
*
* Implements circuit breaker pattern for RAG operations including
* vector store queries, embeddings, and reranking calls.
* Provides fault tolerance and prevents cascading failures.
*/
import { TypedEventEmitter } from "../../core/infrastructure/index.js";
import { logger } from "../../utils/logger.js";
import { RAGCircuitBreakerError, RAGErrorCodes } from "../errors/RAGError.js";
/**
* Default configuration
*/
const DEFAULT_CONFIG = {
failureThreshold: 5,
resetTimeout: 60000,
halfOpenMaxCalls: 3,
operationTimeout: 30000,
minimumCallsBeforeCalculation: 10,
statisticsWindowSize: 300000,
};
/**
* RAG Circuit Breaker
*
* Provides circuit breaker pattern implementation for RAG operations
* with comprehensive statistics and event handling.
*/
export class RAGCircuitBreaker extends TypedEventEmitter {
name;
state = "closed";
config;
callHistory = [];
lastFailureTime = 0;
halfOpenCalls = 0;
lastStateChange = new Date();
cleanupTimer;
constructor(name, config = {}) {
super();
this.name = name;
this.config = { ...DEFAULT_CONFIG, ...config };
// Clean up old call records periodically
this.cleanupTimer = setInterval(() => this.cleanupCallHistory(), 60000);
}
/**
* Execute an operation with circuit breaker protection
*/
async execute(operation, operationType) {
const startTime = Date.now();
try {
// Check if circuit is open
if (this.state === "open") {
if (Date.now() - this.lastFailureTime < this.config.resetTimeout) {
const nextRetryTime = new Date(this.lastFailureTime + this.config.resetTimeout);
throw new RAGCircuitBreakerError(`Circuit breaker '${this.name}' is open. Next retry at ${nextRetryTime.toISOString()}`, {
code: RAGErrorCodes.CIRCUIT_BREAKER_OPEN,
circuitName: this.name,
nextRetryTime,
});
}
// Transition to half-open
this.changeState("half-open", "Reset timeout reached");
}
// Check half-open call limit
if (this.state === "half-open" &&
this.halfOpenCalls >= this.config.halfOpenMaxCalls) {
throw new RAGCircuitBreakerError(`Circuit breaker '${this.name}' is half-open but call limit reached`, {
code: RAGErrorCodes.CIRCUIT_BREAKER_HALF_OPEN_LIMIT,
circuitName: this.name,
});
}
// Execute operation with timeout
const result = await Promise.race([
operation(),
this.timeoutPromise(this.config.operationTimeout),
]);
// Record successful call
this.recordCall(true, Date.now() - startTime, operationType);
// Handle half-open success
if (this.state === "half-open") {
this.halfOpenCalls++;
// If enough successful calls in half-open, close the circuit
if (this.halfOpenCalls >= this.config.halfOpenMaxCalls) {
this.changeState("closed", "Half-open test successful");
}
}
return result;
}
catch (error) {
// Record failed call
const duration = Date.now() - startTime;
this.recordCall(false, duration, operationType);
const errorMessage = error instanceof Error ? error.message : String(error);
// Emit failure event
this.emit("callFailure", {
error: errorMessage,
duration,
timestamp: new Date(),
operationType,
});
// Handle state transitions on failure
if (this.state === "half-open") {
// Failure in half-open immediately opens circuit
this.changeState("open", `Half-open test failed: ${errorMessage}`);
}
else if (this.state === "closed") {
// Check if we should open the circuit
this.checkFailureThreshold();
}
throw error;
}
}
/**
* Record a call in the history
*/
recordCall(success, duration, operationType) {
const now = Date.now();
this.callHistory.push({
timestamp: now,
success,
duration,
operationType,
});
// Emit success event
if (success) {
this.emit("callSuccess", {
duration,
timestamp: new Date(),
operationType,
});
}
// Update failure time
if (!success) {
this.lastFailureTime = now;
}
}
/**
* Check if failure threshold is exceeded
*/
checkFailureThreshold() {
const windowStart = Date.now() - this.config.statisticsWindowSize;
const windowCalls = this.callHistory.filter((call) => call.timestamp >= windowStart);
// Need minimum calls before calculating failure rate
if (windowCalls.length < this.config.minimumCallsBeforeCalculation) {
return;
}
const failedCalls = windowCalls.filter((call) => !call.success).length;
const failureRate = failedCalls / windowCalls.length;
logger.debug(`[RAGCircuitBreaker:${this.name}] Failure rate: ${(failureRate * 100).toFixed(1)}% (${failedCalls}/${windowCalls.length})`);
// Open circuit if failure count exceeds threshold
if (failedCalls >= this.config.failureThreshold) {
this.changeState("open", `Failure threshold exceeded: ${failedCalls} failures`);
this.emit("circuitOpen", {
failureRate,
totalCalls: windowCalls.length,
timestamp: new Date(),
});
}
}
/**
* Change circuit breaker state
*/
changeState(newState, reason) {
const oldState = this.state;
this.state = newState;
this.lastStateChange = new Date();
// Reset counters based on state
if (newState === "half-open") {
this.halfOpenCalls = 0;
this.emit("circuitHalfOpen", { timestamp: new Date() });
}
else if (newState === "closed") {
this.halfOpenCalls = 0;
this.emit("circuitClosed", { timestamp: new Date() });
}
logger.info(`[RAGCircuitBreaker:${this.name}] State changed: ${oldState} -> ${newState} (${reason})`);
// Emit state change event
this.emit("stateChange", {
oldState,
newState,
reason,
timestamp: new Date(),
});
}
/**
* Create a timeout promise
*/
timeoutPromise(timeout) {
return new Promise((_, reject) => {
setTimeout(() => {
reject(new Error(`Operation timed out after ${timeout}ms`));
}, timeout);
});
}
/**
* Clean up old call records
*/
cleanupCallHistory() {
const cutoffTime = Date.now() - this.config.statisticsWindowSize;
const originalLength = this.callHistory.length;
this.callHistory = this.callHistory.filter((call) => call.timestamp >= cutoffTime);
const removed = originalLength - this.callHistory.length;
if (removed > 0) {
logger.debug(`[RAGCircuitBreaker:${this.name}] Cleaned up ${removed} old call records`);
}
}
/**
* Calculate percentile latency
*/
calculatePercentileLatency(percentile) {
if (this.callHistory.length === 0) {
return 0;
}
const sortedDurations = this.callHistory
.filter((call) => call.success)
.map((call) => call.duration)
.sort((a, b) => a - b);
if (sortedDurations.length === 0) {
return 0;
}
const index = Math.ceil((percentile / 100) * sortedDurations.length) - 1;
return sortedDurations[Math.max(0, index)] ?? 0;
}
/**
* Get current statistics
*/
getStats() {
const windowStart = Date.now() - this.config.statisticsWindowSize;
const windowCalls = this.callHistory.filter((call) => call.timestamp >= windowStart);
const successfulCalls = windowCalls.filter((call) => call.success).length;
const failedCalls = windowCalls.length - successfulCalls;
const failureRate = windowCalls.length > 0 ? failedCalls / windowCalls.length : 0;
// Calculate average latency for successful calls
const successfulDurations = windowCalls
.filter((call) => call.success)
.map((call) => call.duration);
const averageLatency = successfulDurations.length > 0
? successfulDurations.reduce((a, b) => a + b, 0) /
successfulDurations.length
: 0;
return {
state: this.state,
totalCalls: this.callHistory.length,
successfulCalls: this.callHistory.filter((call) => call.success).length,
failedCalls: this.callHistory.filter((call) => !call.success).length,
failureRate,
windowCalls: windowCalls.length,
lastStateChange: this.lastStateChange,
nextRetryTime: this.state === "open"
? new Date(this.lastFailureTime + this.config.resetTimeout)
: undefined,
halfOpenCalls: this.halfOpenCalls,
averageLatency,
p95Latency: this.calculatePercentileLatency(95),
};
}
/**
* Manually reset the circuit breaker
*/
reset() {
this.changeState("closed", "Manual reset");
this.callHistory = [];
this.lastFailureTime = 0;
this.halfOpenCalls = 0;
}
/**
* Force open the circuit breaker
*/
forceOpen(reason = "Manual force open") {
this.changeState("open", reason);
this.lastFailureTime = Date.now();
}
/**
* Get circuit breaker name
*/
getName() {
return this.name;
}
/**
* Check if circuit is open
*/
isOpen() {
return this.state === "open";
}
/**
* Check if circuit is closed
*/
isClosed() {
return this.state === "closed";
}
/**
* Check if circuit is half-open
*/
isHalfOpen() {
return this.state === "half-open";
}
/**
* Get current state
*/
getState() {
return this.state;
}
/**
* Destroy the circuit breaker and clean up resources
*/
destroy() {
if (this.cleanupTimer) {
clearInterval(this.cleanupTimer);
this.cleanupTimer = undefined;
}
this.removeAllListeners();
this.callHistory = [];
logger.debug(`[RAGCircuitBreaker:${this.name}] Destroyed`);
}
}
/**
* Circuit breaker manager for RAG operations
*/
export class RAGCircuitBreakerManager {
breakers = new Map();
/**
* Get or create a circuit breaker
*/
getBreaker(name, config) {
if (!this.breakers.has(name)) {
const breaker = new RAGCircuitBreaker(name, config);
this.breakers.set(name, breaker);
logger.debug(`[RAGCircuitBreakerManager] Created circuit breaker: ${name}`);
}
const breaker = this.breakers.get(name);
if (!breaker) {
throw new Error(`Circuit breaker ${name} not found after creation`);
}
return breaker;
}
/**
* Remove a circuit breaker
*/
removeBreaker(name) {
const breaker = this.breakers.get(name);
if (breaker) {
breaker.destroy();
this.breakers.delete(name);
logger.debug(`[RAGCircuitBreakerManager] Removed circuit breaker: ${name}`);
return true;
}
return false;
}
/**
* Get all circuit breaker names
*/
getBreakerNames() {
return Array.from(this.breakers.keys());
}
/**
* Get statistics for all circuit breakers
*/
getAllStats() {
const stats = {};
for (const [name, breaker] of this.breakers) {
stats[name] = breaker.getStats();
}
return stats;
}
/**
* Reset all circuit breakers
*/
resetAll() {
for (const breaker of this.breakers.values()) {
breaker.reset();
}
logger.info("[RAGCircuitBreakerManager] Reset all circuit breakers");
}
/**
* Get health summary
*/
getHealthSummary() {
let closedBreakers = 0;
let openBreakers = 0;
let halfOpenBreakers = 0;
const unhealthyBreakers = [];
for (const [name, breaker] of this.breakers) {
const state = breaker.getState();
switch (state) {
case "closed":
closedBreakers++;
break;
case "open":
openBreakers++;
unhealthyBreakers.push(name);
break;
case "half-open":
halfOpenBreakers++;
break;
}
}
return {
totalBreakers: this.breakers.size,
closedBreakers,
openBreakers,
halfOpenBreakers,
unhealthyBreakers,
};
}
/**
* Destroy all circuit breakers
*/
destroyAll() {
for (const breaker of this.breakers.values()) {
breaker.destroy();
}
this.breakers.clear();
logger.info("[RAGCircuitBreakerManager] Destroyed all circuit breakers");
}
}
/**
* Global circuit breaker manager for RAG operations
*/
export const ragCircuitBreakerManager = new RAGCircuitBreakerManager();
/**
* Convenience function to get a circuit breaker
*/
export function getCircuitBreaker(name, config) {
return ragCircuitBreakerManager.getBreaker(name, config);
}
/**
* Convenience function to execute with circuit breaker
*/
export async function executeWithCircuitBreaker(breakerName, operation, operationType, config) {
const breaker = ragCircuitBreakerManager.getBreaker(breakerName, config);
return breaker.execute(operation, operationType);
}
//# sourceMappingURL=CircuitBreaker.js.map