Aikido

¿Por qué se deberían liberar los locks incluso en rutas de excepción para prevenir deadlocks?

Riesgo de bug

Regla
Liberar bloquea incluso en excepción caminos. 
Cada bloqueo adquisición debe tener a garantizado
liberación, incluso cuando excepciones excepciones. 

Idiomas compatibles lenguajes:** Java, C, C++, PHP, JavaScript,
TypeScript, Go, Python

Introducción

Los bloqueos no liberados son una de las causas más comunes de interbloqueos (deadlocks) y cuelgues del sistema en aplicaciones Node.js en producción. Cuando ocurre una excepción entre la adquisición y la liberación de un bloqueo, este permanece retenido indefinidamente. Otras operaciones asíncronas que esperan ese bloqueo se cuelgan para siempre, causando fallos en cascada en todo el sistema. Un solo mutex no liberado puede paralizar una API completa porque el bucle de eventos se bloquea y las solicitudes se acumulan. Esto ocurre con librerías como async-mutex, mutexify, o cualquier implementación manual de bloqueo donde la liberación no sea automática.

Por qué es importante

Estabilidad y disponibilidad del sistema: Los bloqueos no liberados causan interbloqueos que congelan las operaciones asíncronas en Node.js. En servidores Express o Fastify, esto agota los workers disponibles, haciendo que la aplicación sea incapaz de manejar nuevas solicitudes. La única recuperación es reiniciar el proceso, causando tiempo de inactividad. En arquitecturas de microservicios, los bloqueos no liberados en un servicio pueden provocar fallos en cascada en los servicios dependientes, ya que estos agotan su tiempo de espera para las respuestas.

Degradación del rendimiento: Antes del interbloqueo completo, los bloqueos no liberados causan graves problemas de rendimiento. Las operaciones asíncronas compiten por recursos bloqueados, creando una cola de promesas pendientes que nunca se resuelven. La contención de bloqueos crea picos de latencia impredecibles que degradan la experiencia del usuario. A medida que aumenta el número de solicitudes concurrentes bajo carga, la contención se agrava exponencialmente.

Complejidad de la depuración: Los interbloqueos (deadlocks) por bloqueos no liberados son notoriamente difíciles de depurar en aplicaciones Node.js en producción. Los síntomas aparecen lejos de la causa raíz, los procesos colgados muestran promesas pendientes pero no qué ruta de excepción no liberó el bloqueo. Reproducir la secuencia exacta de excepciones que desencadenaron el interbloqueo es a menudo imposible en entornos de desarrollo.

Agotamiento de recursos: Más allá de los propios bloqueos, la falta de liberación de estos a menudo se correlaciona con la falta de liberación de otros recursos como conexiones a bases de datos, clientes de Redis o manejadores de archivos. Esto agrava el problema, creando múltiples fugas de recursos que colapsan los sistemas más rápidamente bajo carga.

Ejemplos de código

❌ No conforme:

const { Mutex } = require('async-mutex');
const accountMutex = new Mutex();

async function transferFunds(from, to, amount) {
    await accountMutex.acquire();

    if (from.balance < amount) {
        throw new Error('Insufficient funds');
    }

    from.balance -= amount;
    to.balance += amount;

    accountMutex.release();
}

Por qué es inseguro: Si se lanza el error de fondos insuficientes, accountMutex.release() nunca se ejecuta y el mutex permanece bloqueado indefinidamente. Todas las llamadas posteriores a transferFunds() se quedará colgado esperando el mutex, congelando todo el sistema de pagos.

✅ Conforme:

const { Mutex } = require('async-mutex');
const accountMutex = new Mutex();

async function transferFunds(from, to, amount) {
    const release = await accountMutex.acquire();
    try {
        if (from.balance < amount) {
            throw new Error('Insufficient funds');
        }
        
        from.balance -= amount;
        to.balance += amount;
    } catch (error) {
        logger.error('Transfer failed', { 
            fromId: from.id, 
            toId: to.id, 
            amount,
            error: error.message 
        });
        throw error;
    } finally {
        release();
    }
}

Por qué es seguro: El detectar el bloque registra el error con contexto antes de relanzarlo, y el finalmente el bloque garantiza que la función de liberación del mutex se ejecuta tanto si la operación tiene éxito, como si lanza un error, o si el error se relanza desde el 'catch'. El bloqueo siempre se libera, evitando interbloqueos.

Conclusión

La liberación del bloqueo debe estar garantizada, no condicionada a una ejecución exitosa. Utilice try-finally bloques en JavaScript o el runExclusive() función de ayuda proporcionada por librerías como async-mutex. Cada adquisición de bloqueo debería tener una ruta de liberación incondicional visible en el mismo bloque de código. La gestión adecuada de bloqueos no es opcional; es la diferencia entre un sistema estable y uno que se cuelga aleatoriamente bajo carga.

Preguntas frecuentes

¿Tiene preguntas?

¿Cuál es el patrón correcto para la liberación de bloqueo garantizada en JavaScript?

Use try-finally blocks with explicit release in finally. Store the release function returned by acquire() and call it in the finally block. Better yet, use the runExclusive() method provided by libraries like async-mutex which handles acquisition and release automatically: await mutex.runExclusive(async () => { /* your code */ }). This eliminates the chance of forgetting the finally block.

¿Debería usar try-catch-finally o solo try-finally para la liberación de bloqueos?

Utilice try-finally si desea que las excepciones se propaguen al llamador. Utilice try-catch-finally si necesita manejar el error localmente mientras garantiza la liberación del bloqueo. El bloque finally se ejecuta en ambos casos, pero catch le da la oportunidad de registrar, transformar o suprimir el error. Siempre coloque release() en finally, nunca en catch, porque finally se ejecuta incluso si catch vuelve a lanzar la excepción.

¿Qué pasa con los bloqueos asíncronos con callbacks en lugar de promises?

Convierta primero el código basado en callbacks a promesas, luego use async/await con try-finally. Si eso no es posible, asegúrese de que cada ruta de callback (éxito, error, tiempo de espera) llame a la función de liberación. Esto es propenso a errores, por lo que se prefieren los bloqueos basados en promesas. Nunca dependa de la recolección de basura para liberar bloqueos; no es determinista y causará interbloqueos.

¿Cómo gestiono múltiples bloqueos que deben adquirirse conjuntamente?

Adquiere todos los bloqueos antes de cualquier lógica de negocio y libéralos en orden inverso en un único bloque `finally`. Un enfoque mejor: utiliza una jerarquía de bloqueos donde los bloqueos se adquieren siempre en el mismo orden para evitar dependencias circulares. Para casos complejos, considera usar un patrón de coordinador de transacciones o bibliotecas como async-lock que soporten el bloqueo de múltiples recursos con liberación automática ante cualquier fallo.

¿Puedo liberar un bloqueo antes de tiempo si sé que ya no lo necesito?

Sí, pero ten mucho cuidado. Una vez liberado, no tienes protección contra el acceso concurrente. Un patrón común es liberar después de la sección crítica, pero antes de operaciones lentas como el registro (logging) o las llamadas a API externas. Sin embargo, si ocurre alguna excepción después de la liberación temprana, pero antes de la salida de la función, te arriesgas a un estado inconsistente. Documenta claramente por qué la liberación temprana es segura.

¿Qué herramientas pueden detectar bloqueos no liberados en código JavaScript?

Las herramientas de análisis estático pueden señalar adquisiciones de bloqueo sin bloques `finally` correspondientes. La detección en tiempo de ejecución es más difícil, ya que JavaScript no tiene detección de interbloqueos incorporada. Implementa tiempos de espera en la adquisición de bloqueos (la mayoría de las librerías lo soportan) para fallar rápidamente en lugar de quedarse colgado indefinidamente. Monitoriza las tasas de rechazo de promesas y el retraso del bucle de eventos en producción para detectar problemas de contención de bloqueos.

Cómo evitan este problema bibliotecas como async-mutex?

async-mutex provides runExclusive() which acquires the lock, runs your function, and releases the lock automatically even if exceptions occur. It's essentially a built-in try-finally wrapper. Use this when possible: await mutex.runExclusive(async () => { /* your code */ }). This eliminates manual release management and prevents the most common mistake of forgetting the finally block.

Asegúrate ahora.

Proteja su código, la nube y el entorno de ejecución en un único sistema central.
Encuentre y corrija vulnerabilidades de forma rápida y automática.

No se requiere tarjeta de crédito | Resultados del escaneo en 32 segundos.