Files
OllaHub/services/engine-circuit-breaker.ts
Evandro Fonseca Junior 8c546f6474 feat(chat): implement adaptive timeout and enhanced search functionality
- Introduced adaptive timeout logic for web searches and chat streaming, improving responsiveness and efficiency.
- Updated `smartSearchRag` method to support timeout adjustments based on search rounds.
- Enhanced `executeProgressiveSearch` to utilize adaptive timeouts and improve search result handling.
- Added new utility functions for calculating adaptive timeouts based on rounds and attempts.
- Integrated event listeners for chat session management, improving user experience during chat interactions.
- Refactored chat message handling to accommodate new streaming and timeout features.
2025-11-27 17:32:46 -03:00

192 lines
5.8 KiB
TypeScript

/**
* Circuit Breaker para motores de busca
* Desabilita motores que estão falhando consistentemente
*/
export interface EngineHealth {
engine: string;
successCount: number;
failureCount: number;
totalAttempts: number;
avgLatency: number; // ms
lastFailure: number | null; // timestamp
lastSuccess: number | null; // timestamp
isOpen: boolean; // circuit breaker aberto?
openedAt: number | null; // quando foi aberto
}
export interface CircuitBreakerConfig {
failureThreshold: number; // Taxa de falha para abrir (0-1, padrão: 0.5 = 50%)
cooldownPeriod: number; // Tempo em ms antes de tentar novamente (padrão: 5 minutos)
trackingWindow: number; // Número de tentativas para rastrear (padrão: 10)
minAttempts: number; // Mínimo de tentativas antes de considerar (padrão: 3)
}
const DEFAULT_CONFIG: CircuitBreakerConfig = {
failureThreshold: 0.5, // 50% de falha
cooldownPeriod: 5 * 60 * 1000, // 5 minutos
trackingWindow: 10,
minAttempts: 3,
};
export class EngineCircuitBreaker {
private health: Map<string, EngineHealth> = new Map();
private config: CircuitBreakerConfig;
constructor(config: Partial<CircuitBreakerConfig> = {}) {
this.config = { ...DEFAULT_CONFIG, ...config };
}
/**
* Registra uma tentativa de busca
*/
recordAttempt(engine: string, success: boolean, latency: number): void {
const health = this.getOrCreateHealth(engine);
health.totalAttempts++;
health.avgLatency = (health.avgLatency * (health.totalAttempts - 1) + latency) / health.totalAttempts;
if (success) {
health.successCount++;
health.lastSuccess = Date.now();
// Se teve sucesso e circuit estava aberto há tempo suficiente, tentar fechar
if (health.isOpen && health.openedAt) {
const timeSinceOpen = Date.now() - health.openedAt;
if (timeSinceOpen >= this.config.cooldownPeriod) {
health.isOpen = false;
health.openedAt = null;
console.log(`[CircuitBreaker] ${engine}: Circuit fechado após sucesso (${timeSinceOpen}ms desde abertura)`);
}
}
} else {
health.failureCount++;
health.lastFailure = Date.now();
}
// Limitar histórico às últimas N tentativas
if (health.totalAttempts > this.config.trackingWindow) {
// Remover tentativas mais antigas (simplificado: resetar contadores)
const successRate = health.successCount / health.totalAttempts;
health.successCount = Math.floor(successRate * this.config.trackingWindow);
health.failureCount = this.config.trackingWindow - health.successCount;
health.totalAttempts = this.config.trackingWindow;
}
// Verificar se deve abrir circuit breaker
if (!health.isOpen && health.totalAttempts >= this.config.minAttempts) {
const failureRate = health.failureCount / health.totalAttempts;
if (failureRate >= this.config.failureThreshold) {
health.isOpen = true;
health.openedAt = Date.now();
console.warn(`[CircuitBreaker] ${engine}: Circuit aberto (taxa de falha: ${(failureRate * 100).toFixed(1)}%)`);
}
}
this.health.set(engine, health);
}
/**
* Verifica se um motor está disponível (circuit não está aberto)
*/
isAvailable(engine: string): boolean {
const health = this.getOrCreateHealth(engine);
if (!health.isOpen) {
return true;
}
// Se está aberto, verificar se passou tempo suficiente para tentar novamente
if (health.openedAt) {
const timeSinceOpen = Date.now() - health.openedAt;
if (timeSinceOpen >= this.config.cooldownPeriod) {
// Tentar fechar (half-open state)
health.isOpen = false;
this.health.set(engine, health);
console.log(`[CircuitBreaker] ${engine}: Tentando fechar circuit (half-open state)`);
return true;
}
}
return false;
}
/**
* Prioriza motores baseado em saúde
*/
prioritizeEngines(engines: string[]): string[] {
return [...engines].sort((a, b) => {
const healthA = this.getOrCreateHealth(a);
const healthB = this.getOrCreateHealth(b);
// Motores não disponíveis vão para o final
if (healthA.isOpen && !healthB.isOpen) return 1;
if (!healthA.isOpen && healthB.isOpen) return -1;
// Comparar taxa de sucesso
const successRateA = healthA.totalAttempts > 0
? healthA.successCount / healthA.totalAttempts
: 0.5;
const successRateB = healthB.totalAttempts > 0
? healthB.successCount / healthB.totalAttempts
: 0.5;
if (Math.abs(successRateA - successRateB) > 0.1) {
return successRateB - successRateA; // Maior taxa de sucesso primeiro
}
// Se taxa similar, comparar latência
return healthA.avgLatency - healthB.avgLatency; // Menor latência primeiro
});
}
/**
* Obtém estatísticas de um motor
*/
getHealth(engine: string): EngineHealth | null {
return this.health.get(engine) || null;
}
/**
* Obtém ou cria health para um motor
*/
private getOrCreateHealth(engine: string): EngineHealth {
if (!this.health.has(engine)) {
this.health.set(engine, {
engine,
successCount: 0,
failureCount: 0,
totalAttempts: 0,
avgLatency: 0,
lastFailure: null,
lastSuccess: null,
isOpen: false,
openedAt: null,
});
}
return this.health.get(engine)!;
}
/**
* Reseta estatísticas de um motor
*/
reset(engine: string): void {
this.health.delete(engine);
}
/**
* Reseta todas as estatísticas
*/
resetAll(): void {
this.health.clear();
}
/**
* Obtém todos os motores disponíveis
*/
getAvailableEngines(engines: string[]): string[] {
return engines.filter(engine => this.isAvailable(engine));
}
}