Circuit Breaker Avanzado con Auto-Recuperación (Self-Healing) para Microservicios en Node.js

JAVASCRIPT / NODE.JS 14 de julio de 2026 104 lecturas
He desarrollado un Circuit Breaker resiliente con auto-recuperación en JavaScript puro, diseñado para proteger microservicios Node.js de fallos en cascada y latencias externas, garantizando la estabilidad del sistema.

En el complejo mundo de las arquitecturas distribuidas y los entornos de alta concurrencia, la dependencia de APIs de terceros o microservicios satélite introduce un punto crítico de fallo. He observado cómo una degradación severa en un servicio de facturación o una base de datos externa puede provocar que las peticiones entrantes se acumulen en el Event Loop de Node.js, agotando los hilos de red y la memoria, lo que culmina en un colapso total del servidor principal y un temido fallo en cascada.


Para mitigar este riesgo, he implementado un Circuit Breaker que actúa como un fusible eléctrico inteligente. Mi diseño opera bajo una máquina de estados de tres fases bien definida: Cerrado (CLOSED), donde el tráfico fluye normalmente; Abierto (OPEN), que corta inmediatamente las peticiones para evitar la saturación de la red tras alcanzar un umbral de fallos; y Medio-Abierto (HALF_OPEN), un estado crucial que permite una única petición de prueba después de un tiempo de enfriamiento. Si esta prueba tiene éxito, el circuito se restablece automáticamente a Cerrado; si falla, vuelve a bloquear el tráfico, demostrando su capacidad de auto-recuperación.


Desde una perspectiva de ingeniería, mi implementación es pura en JavaScript, eliminando la necesidad de depender de librerías pesadas. Utilizo el aislamiento de clases y la asincronía nativa (async/await) para mantener una huella de memoria mínima. Al concebirlo como un wrapper (envoltorio) alrededor de cualquier función que retorne una Promesa, puedo inyectar resiliencia instantánea a llamadas de axios, consultas de Prisma o conexiones de WebSockets, garantizando que el hilo principal de su aplicación nunca se bloquee por la inestabilidad de un servicio externo.

/**
 * Advanced Circuit Breaker Pattern - AndresSY.dev
 * Protege el Event Loop de fallos en cascada por latencia externa.
 */
class CircuitBreaker {
    static STATES = {
        CLOSED: 'CLOSED',
        OPEN: 'OPEN',
        HALF_OPEN: 'HALF_OPEN'
    };

    /**
     * @param {Function} requestFunction - La función asíncrona a envolver. Debe retornar una Promesa.
     * @param {Object} options - Configuración del circuito.
     * @param {number} [options.failureThreshold=3] - Número de fallos consecutivos antes de abrir el circuito.
     * @param {number} [options.recoveryTimeout=10000] - Tiempo en milisegundos que el circuito permanece abierto antes de intentar un HALF_OPEN.
     * @param {string} [options.name='UnnamedCircuit'] - Nombre opcional para identificar este Circuit Breaker en los logs.
     */
    constructor(requestFunction, options = {}) {
        if (typeof requestFunction !== 'function') {
            throw new Error('CircuitBreaker: requestFunction debe ser una función.');
        }

        this.request = requestFunction;
        this.state = CircuitBreaker.STATES.CLOSED;
        this.failureThreshold = options.failureThreshold || 3;
        this.recoveryTimeout = options.recoveryTimeout || 10000;
        this.name = options.name || 'UnnamedCircuit';
        
        this.failureCount = 0;
        this.nextAttempt = null;
    }

    /**
     * Dispara la ejecución controlada de la función envuelta.
     * Si el circuito está abierto, rechazará la petición inmediatamente.
     * @param {...any} args - Argumentos a pasar a la función requestFunction.
     * @returns {Promise<any>} - La promesa resultante de la función requestFunction.
     * @throws {Error} Si el circuito está abierto y no es momento de un intento de recuperación.
     */
    async fire(...args) {
        if (this.state === CircuitBreaker.STATES.OPEN) {
            if (Date.now() > this.nextAttempt) {
                // El tiempo de enfriamiento terminó, intentamos una prueba de "Self-Healing"
                this.state = CircuitBreaker.STATES.HALF_OPEN;
                console.log(`[CircuitBreaker:${this.name}] Transición a HALF_OPEN. Intentando una petición de prueba.`);
            } else {
                // Cortocircuito activo: Rechazo inmediato sin gastar recursos de red
                const remainingTime = Math.ceil((this.nextAttempt - Date.now()) / 1000);
                throw new Error(`[CircuitBreaker:${this.name}] OPEN: Servicio suspendido. Reintentando en ${remainingTime}s para evitar colapso.`);
            }
        }

        try {
            const result = await this.request(...args);
            this.success();
            return result;
        } catch (error) {
            this.fail();
            throw error; // Re-lanza el error original para que el consumidor lo maneje
        }
    }

    /**
     * Registra un éxito en la ejecución de la función.
     * Si el circuito no estaba en CLOSED, lo restablece.
     */
    success() {
        if (this.state !== CircuitBreaker.STATES.CLOSED) {
            console.log(`[CircuitBreaker:${this.name}] HALF_OPEN -> CLOSED: Conexión restaurada con éxito.`);
        }
        this.failureCount = 0;
        this.state = CircuitBreaker.STATES.CLOSED;
    }

    /**
     * Registra un fallo en la ejecución de la función.
     * Si se supera el umbral de fallos, el circuito se abre.
     */
    fail() {
        this.failureCount++;
        if (this.failureCount >= this.failureThreshold) {
            this.state = CircuitBreaker.STATES.OPEN;
            this.nextAttempt = Date.now() + this.recoveryTimeout;
            console.warn(`[CircuitBreaker:${this.name}] Abierto tras ${this.failureCount} fallos. Tráfico bloqueado por ${this.recoveryTimeout}ms.`);
        } else {
            console.warn(`[CircuitBreaker:${this.name}] Fallo ${this.failureCount}/${this.failureThreshold}.`);
        }
    }

    /**
     * Restablece manualmente el circuito a su estado inicial (CLOSED).
     * Esto puede ser útil para pruebas o recuperaciones manuales.
     */
    reset() {
        this.state = CircuitBreaker.STATES.CLOSED;
        this.failureCount = 0;
        this.nextAttempt = null;
        console.log(`[CircuitBreaker:${this.name}] Circuito restablecido manualmente a CLOSED.`);
    }
}
¿Qué te pareció?
🔥 Brillante 1
💡 Me sirvió 0
🚀 A otro nivel 0

¿Te resultó útil este snippet? Explora más código y soluciones en AndresSY.dev.

Volver a Snippets