Aikido

Libertar bloqueios mesmo em caminhos de exceção: evitar bloqueios

Risco de insectos

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

Os bloqueios não liberados são uma das causas mais comuns de deadlocks e travamentos do sistema em aplicativos Node.js de produção. Quando ocorre uma exceção entre a aquisição e a liberação do bloqueio, o bloqueio permanece retido indefinidamente. Outras operações assíncronas que esperam por esse bloqueio ficam suspensas para sempre, causando falhas em cascata em todo o sistema. Um único mutex não liberado pode derrubar uma API inteira porque o loop de eventos fica bloqueado e as solicitações se acumulam. Isso acontece com bibliotecas como async-mutex, mutexificarou qualquer aplicação de bloqueio manual em que o desbloqueio não seja automático.

Porque é importante

Estabilidade e disponibilidade do sistema: Os bloqueios não liberados causam deadlocks que congelam as operações assíncronas no Node.js. Em servidores Express ou Fastify, isso esgota os trabalhadores disponíveis, tornando o aplicativo incapaz de lidar com novas solicitações. A única recuperação é reiniciar o processo, causando tempo de inatividade. Em arquitecturas de microsserviços, os bloqueios não libertados num serviço podem provocar falhas em cascata nos serviços dependentes, uma vez que estes ficam à espera de respostas.

Degradação do desempenho: Antes do deadlock completo, os bloqueios não liberados causam graves problemas de desempenho. As operações assíncronas disputam os recursos bloqueados, criando uma fila de promessas pendentes que nunca são resolvidas. A contenção de bloqueios cria picos de latência imprevisíveis que degradam a experiência do utilizador. À medida que o número de pedidos simultâneos aumenta sob carga, a contenção aumenta exponencialmente.

Complexidade de depuração: Deadlocks de bloqueios não liberados são notoriamente difíceis de depurar em aplicações Node.js de produção. Os sintomas parecem distantes da causa raiz, as interrupções do processo mostram promessas pendentes, mas não qual caminho de exceção falhou ao liberar o bloqueio. Reproduzir a sequência exata de exceções que desencadeou o deadlock geralmente é impossível em ambientes de desenvolvimento.

Exaustão de recursos: Além dos bloqueios em si, a falha na liberação de bloqueios geralmente está correlacionada com a falha na liberação de outros recursos, como conexões de banco de dados, clientes Redis ou manipuladores de arquivos. Isso agrava o problema, criando vários 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();
}

Porque é que não é seguro: Se o erro de fundos insuficientes for lançado, accountMutex.release() nunca é executada e o mutex permanece bloqueado para sempre. Todas as chamadas subsequentes a transferFunds() ficará suspenso à espera do mutex, congelando todo o sistema de pagamento.

Conformidade:

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();
    }
}

Porque é que é seguro: O captura regista o erro com contexto antes de o voltar a lançar, e o bloco finalmente garante que a função de libertação do mutex é executada, quer a operação seja bem sucedida, quer lance um erro, quer o erro seja novamente lançado a partir de catch. O bloqueio é sempre libertado, evitando bloqueios.

Conclusão

A libertação do cadeado deve ser garantida e não condicionada a uma execução bem sucedida. Utilização tentar-finalmente em JavaScript ou o bloco runExclusive() fornecido por bibliotecas como async-mutex. Cada aquisição de bloqueio deve ter um caminho de libertação incondicional visível no mesmo bloco de código. O gerenciamento adequado de bloqueios não é opcional, é a diferença entre um sistema estável e um que trava aleatoriamente sob carga.

FAQs

Tem perguntas?

Qual é o padrão correto para a libertação garantida de bloqueios 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 liberar o bloqueio?

Utilize try-finally se quiser que as excepções se propaguem para o chamador. Use try-catch-finally se você precisa tratar o erro localmente enquanto ainda garante a liberação do bloqueio. O bloco finally é executado em ambos os casos, mas catch dá-lhe a oportunidade de registar, transformar ou suprimir o erro. Sempre coloque release() no finally, nunca no catch, porque o finally é executado mesmo que o catch retorne ao erro.

E quanto a bloqueios assíncronos com callbacks em vez de promessas?

Converta o código baseado em callback para promessas 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, e é por isso que os bloqueios baseados em promessas são preferidos. Nunca confie na coleta de lixo para liberar bloqueios, ela não é determinística e causará deadlocks.

Como é que lido com vários cadeados que têm de ser adquiridos em conjunto?

Adquirir todos os bloqueios antes de qualquer lógica comercial e libertá-los por ordem inversa num único bloco final. Melhor abordagem: usar uma hierarquia de bloqueios em que os bloqueios são sempre adquiridos na mesma ordem para evitar dependências circulares. Para casos complexos, considere a utilização de um padrão de coordenador de transação ou de bibliotecas como async-lock que suportam o bloqueio de vários recursos com libertação automática em qualquer falha.

Posso libertar um cadeado mais cedo se souber que já não preciso dele?

Sim, mas tenha muito cuidado. Uma vez libertado, não tem qualquer proteção contra o acesso simultâneo. Um padrão comum é a libertação após uma secção crítica, mas antes de operações lentas como o registo ou chamadas externas à API. No entanto, se ocorrer uma exceção após a libertação antecipada, mas antes da saída da função, corre o risco de ter um estado inconsistente. Documentar claramente porque é que a libertação antecipada é segura.

Que ferramentas podem detetar bloqueios não libertados no código JavaScript?

As ferramentas de análise estática podem assinalar aquisições de bloqueios sem blocos finally correspondentes. A deteção em tempo de execução é mais difícil, pois o JavaScript não tem deteção de deadlock incorporada. Implemente timeouts na aquisição de bloqueios (a maioria das bibliotecas suporta isso) para falhar rapidamente em vez de ficar pendurado para sempre. Monitorize as taxas de rejeição de promessas e o atraso do ciclo de eventos na produção para detetar problemas de contenção de bloqueios.

Como é que bibliotecas como async-mutex evitam 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.

Obter segurança gratuitamente

Proteja seu código, nuvem e tempo de execução em um sistema central.
Encontre e corrija vulnerabilidades rapidamente de forma automática.

Não é necessário cartão de crédito | Resultados do scan em 32secs.