From 66fbf98d54bf4c6c17331de3f0e151689e16571e Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Tue, 21 Oct 2025 21:27:18 +0200 Subject: [PATCH] feat: integrate CircuitBreakerService and adjust request timeout and queue management for improved API stability --- src/lib/services/base-api.service.ts | 12 ++-- src/lib/services/circuit-breaker.service.ts | 78 +++++++++++++++++++++ src/lib/services/request-queue.service.ts | 11 ++- 3 files changed, 95 insertions(+), 6 deletions(-) create mode 100644 src/lib/services/circuit-breaker.service.ts diff --git a/src/lib/services/base-api.service.ts b/src/lib/services/base-api.service.ts index ccc7187..2792ee6 100644 --- a/src/lib/services/base-api.service.ts +++ b/src/lib/services/base-api.service.ts @@ -8,6 +8,7 @@ import type { KomgaConfig } from "@/types/komga"; import type { ServerCacheService } from "./server-cache.service"; import { RequestMonitorService } from "./request-monitor.service"; import { RequestQueueService } from "./request-queue.service"; +import { CircuitBreakerService } from "./circuit-breaker.service"; export type { CacheType }; @@ -109,14 +110,16 @@ export abstract class BaseApiService { } } - // Timeout de 60 secondes au lieu de 10 par défaut - const timeoutMs = 60000; + // Timeout réduit à 15 secondes pour éviter les blocages longs + const timeoutMs = 15000; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeoutMs); try { - // Enqueue la requête pour limiter la concurrence - const response = await RequestQueueService.enqueue(async () => { + // Utiliser le circuit breaker pour éviter de surcharger Komga + const response = await CircuitBreakerService.execute(async () => { + // Enqueue la requête pour limiter la concurrence + return await RequestQueueService.enqueue(async () => { try { return await fetch(url, { headers, @@ -166,6 +169,7 @@ export abstract class BaseApiService { throw fetchError; } + }); }); clearTimeout(timeoutId); diff --git a/src/lib/services/circuit-breaker.service.ts b/src/lib/services/circuit-breaker.service.ts new file mode 100644 index 0000000..ea035a4 --- /dev/null +++ b/src/lib/services/circuit-breaker.service.ts @@ -0,0 +1,78 @@ +/** + * Circuit Breaker pour éviter de surcharger Komga quand il est défaillant + * Évite l'effet avalanche en coupant les requêtes vers un service défaillant + */ +interface CircuitBreakerState { + state: 'CLOSED' | 'OPEN' | 'HALF_OPEN'; + failureCount: number; + lastFailureTime: number; + nextAttemptTime: number; +} + +class CircuitBreaker { + private state: CircuitBreakerState = { + state: 'CLOSED', + failureCount: 0, + lastFailureTime: 0, + nextAttemptTime: 0, + }; + + private readonly config = { + failureThreshold: 5, // Nombre d'échecs avant ouverture + recoveryTimeout: 30000, // 30s avant tentative de récupération + successThreshold: 3, // Nombre de succès pour fermer le circuit + }; + + async execute(operation: () => Promise): Promise { + if (this.state.state === 'OPEN') { + if (Date.now() < this.state.nextAttemptTime) { + throw new Error('Circuit breaker is OPEN - Komga service unavailable'); + } + this.state.state = 'HALF_OPEN'; + } + + try { + const result = await operation(); + this.onSuccess(); + return result; + } catch (error) { + this.onFailure(); + throw error; + } + } + + private onSuccess(): void { + if (this.state.state === 'HALF_OPEN') { + this.state.failureCount = 0; + this.state.state = 'CLOSED'; + console.log('[CIRCUIT-BREAKER] ✅ Circuit closed - Komga recovered'); + } + } + + private onFailure(): void { + this.state.failureCount++; + this.state.lastFailureTime = Date.now(); + + if (this.state.failureCount >= this.config.failureThreshold) { + this.state.state = 'OPEN'; + this.state.nextAttemptTime = Date.now() + this.config.recoveryTimeout; + console.warn(`[CIRCUIT-BREAKER] 🔴 Circuit OPEN - Komga failing (${this.state.failureCount} failures)`); + } + } + + getState(): CircuitBreakerState { + return { ...this.state }; + } + + reset(): void { + this.state = { + state: 'CLOSED', + failureCount: 0, + lastFailureTime: 0, + nextAttemptTime: 0, + }; + console.log('[CIRCUIT-BREAKER] 🔄 Circuit reset'); + } +} + +export const CircuitBreakerService = new CircuitBreaker(); diff --git a/src/lib/services/request-queue.service.ts b/src/lib/services/request-queue.service.ts index e81583b..1a1b6f5 100644 --- a/src/lib/services/request-queue.service.ts +++ b/src/lib/services/request-queue.service.ts @@ -22,6 +22,12 @@ class RequestQueue { async enqueue(execute: () => Promise): Promise { return new Promise((resolve, reject) => { + // Limiter la taille de la queue pour éviter l'accumulation + if (this.queue.length >= 50) { + reject(new Error('Request queue is full - Komga may be overloaded')); + return; + } + this.queue.push({ execute, resolve, reject }); this.processQueue(); }); @@ -45,8 +51,9 @@ class RequestQueue { } try { - // Délai de 200ms entre chaque requête pour espacer la charge CPU sur Komga - await this.delay(200); + // Délai adaptatif : plus long si la queue est pleine + const delayMs = this.queue.length > 10 ? 500 : 200; + await this.delay(delayMs); const result = await request.execute(); request.resolve(result); } catch (error) {