Aikido

Por que você deve liberar locks mesmo em caminhos de exceção para prevenir deadlocks

Risco de Bug

Regra
Libertação fechaduras mesmo em exceção caminhos. 
Todos os bloqueio aquisição deve ter a garantida
libertação, mesmo quando excepções ocorrem. 

Suportados linguagens suportadas:** Java, C, C++, PHP, JavaScript,
TypeScript, Go, Python

Introdução

Locks não liberados são uma das causas mais comuns de deadlocks e travamentos de sistema em aplicações Node.js em produção. Quando uma exceção ocorre entre a aquisição e a liberação de um lock, o lock permanece retido indefinidamente. Outras operações assíncronas esperando por esse lock travam para sempre, causando falhas em cascata por todo o sistema. Um único mutex não liberado pode derrubar uma API inteira porque o event loop fica bloqueado e as requisições se acumulam. Isso acontece com bibliotecas como async-mutex, mutexify, ou qualquer implementação de bloqueio manual onde a liberação não é automática.

Por que isso importa

Estabilidade e disponibilidade do sistema: Locks não liberados causam deadlocks que congelam operações assíncronas no Node.js. Em servidores Express ou Fastify, isso esgota os workers disponíveis, tornando a aplicação incapaz de lidar com novas requisições. A única recuperação é reiniciar o processo, causando downtime. Em arquiteturas de microsserviços, locks não liberados em um serviço podem causar falhas em cascata em serviços dependentes, à medida que eles expiram esperando por respostas.

Degradação de desempenho: Antes do deadlock completo, locks não liberados causam problemas graves de desempenho. Operações assíncronas disputam recursos bloqueados, criando uma fila de promises pendentes que nunca se resolvem. A contenção de locks cria picos de latência imprevisíveis que degradam a experiência do usuário. À medida que o número de requisições concorrentes aumenta sob carga, a contenção se agrava exponencialmente.

Complexidade de depuração: Deadlocks de locks não liberados são notoriamente difíceis de depurar em aplicações Node.js em produção. Os sintomas aparecem longe da causa raiz, travamentos de processo mostram promessas pendentes, mas não qual caminho de exceção falhou em liberar o lock. Reproduzir a sequência exata de exceções que acionaram o deadlock é frequentemente impossível em ambientes de desenvolvimento.

Esgotamento de recursos: Além dos próprios locks, a falha em liberar locks frequentemente se correlaciona com a falha em liberar outros recursos, como conexões de banco de dados, clientes Redis ou manipuladores de arquivos. Isso agrava o problema, criando múltiplos vazamentos de recursos que derrubam os sistemas mais rapidamente sob carga.

Exemplos de código

❌ Não-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 que é inseguro: Se o erro de fundos insuficientes for lançado, accountMutex.release() nunca é executado e o mutex permanece bloqueado para sempre. Todas as chamadas subsequentes para transferFunds() ficará travado esperando pelo mutex, congelando todo o sistema de pagamento.

✅ Compatível:

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 que é seguro: O Detectar O bloco registra o erro com contexto antes de relançá-lo, e o finalmente O bloco garante que a função de liberação do mutex seja executada, seja a operação bem-sucedida, lançando um erro, ou se o erro for relançado do catch. O lock é sempre liberado, prevenindo deadlocks.

Conclusão

A liberação do lock deve ser garantida, não condicional à execução bem-sucedida. Use try-finally blocos em JavaScript ou o runExclusive() auxiliar fornecido por bibliotecas como async-mutex. Toda aquisição de lock deve ter um caminho de liberação incondicional visível no mesmo bloco de código. O gerenciamento adequado de locks não é opcional, é a diferença entre um sistema estável e um que trava aleatoriamente sob carga.

FAQs

Dúvidas?

Qual é o padrão correto para a liberação garantida de lock em 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.

Devo usar try-catch-finally ou apenas try-finally para liberação de lock?

Use try-finally se você quiser que as exceções se propaguem para o chamador. Use try-catch-finally se você precisar lidar com o erro localmente, garantindo ainda a liberação do lock. O bloco finally é executado em ambos os casos, mas o catch lhe dá a chance de registrar, transformar ou suprimir o erro. Sempre coloque `release()` no finally, nunca no catch, porque o finally é executado mesmo que o catch relance a exceção.

E quanto aos locks assíncronos com callbacks em vez de promises?

Converta o código baseado em callbacks para promises primeiro, depois use async/await com try-finally. Se isso não for possível, garanta que cada caminho de callback (sucesso, erro, timeout) chame a função de liberação. Isso é propenso a erros, razão pela qual locks baseados em promises são preferidos. Nunca confie na coleta de lixo para liberar locks; ela não é determinística e causará deadlocks.

Como lidar com múltiplos locks que precisam ser adquiridos em conjunto?

Adquira todos os locks antes de qualquer lógica de negócio e os libere na ordem inversa em um único bloco finally. Melhor abordagem: use uma hierarquia de locks onde os locks são sempre adquiridos na mesma ordem para evitar dependências circulares. Para casos complexos, considere usar um padrão de coordenador de transações ou bibliotecas como async-lock que suportam bloqueio de múltiplos recursos com liberação automática em caso de falha.

Posso liberar um lock antecipadamente se eu souber que terminei com ele?

Sim, mas tenha extremo cuidado. Uma vez liberado, você não tem proteção contra acesso concorrente. Um padrão comum é liberar após a seção crítica, mas antes de operações lentas como logging ou chamadas de API externas. No entanto, se qualquer exceção ocorrer após a liberação antecipada, mas antes da saída da função, você corre o risco de um estado inconsistente. Documente claramente por que a liberação antecipada é segura.

Quais ferramentas podem detectar bloqueios não liberados em código JavaScript?

Ferramentas de análise estática podem sinalizar aquisições de locks sem blocos finally correspondentes. A detecção em tempo de execução é mais difícil, pois o JavaScript não possui detecção de deadlock integrada. Implemente timeouts na aquisição de locks (a maioria das bibliotecas suporta isso) para falhar rapidamente em vez de travar indefinidamente. Monitore as taxas de rejeição de promises e o lag do event loop em produção para detectar problemas de contenção de locks.

Como bibliotecas como async-mutex previnem este problema?

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.

Fique seguro agora

Proteja seu código, Cloud e runtime em um único sistema centralizado.
Encontre e corrija vulnerabilidades rapidamente de forma automática.

Não é necessário cartão de crédito | Resultados da varredura em 32 segundos.