Aikido

Perché è necessario rilasciare i lock anche nei percorsi di eccezione per prevenire i deadlock

Rischio di bug

Regola
Rilascia i lock anche nei percorsi di eccezione.
Ogni acquisizione di lock deve avere un rilascio
garantito, anche quando si verificano eccezioni.

Linguaggi supportati:** Java, C, C++, PHP, JavaScript,
TypeScript, Go, Python

Introduzione

I lock non rilasciati rappresentano una delle cause più comuni di deadlock e blocchi di sistema nelle applicazioni Node.js in produzione. Quando si verifica un'eccezione tra l'acquisizione e il rilascio del lock, questo rimane occupato a tempo indeterminato. Altre operazioni asincrone in attesa di quel lock si bloccano per sempre, innescando guasti a catena in tutto il sistema. Un singolo mutex non rilasciato può compromettere un'intera API poiché il ciclo degli eventi viene bloccato e le richieste si accumulano. Questo problema si verifica con librerie come async-mutex, mutexify, o con qualsiasi implementazione di lock manuale in cui il rilascio non avviene automaticamente.

Perché è importante

Stabilità e disponibilità del sistema: I lock non rilasciati provocano deadlock che bloccano le operazioni asincrone in Node.js. Nei server Express o Fastify, questo esaurisce i worker disponibili, impedendo all'applicazione di gestire nuove richieste. L'unico modo per ripristinare il servizio è riavviare il processo, causando un periodo di downtime. Nelle architetture a microservizi, i lock non rilasciati in un servizio possono propagare guasti ai servizi dipendenti a causa del timeout durante l'attesa delle risposte.

Degradazione delle performance: Prima di arrivare a un deadlock totale, i lock non rilasciati causano gravi problemi di performance. Le operazioni asincrone competono per le risorse bloccate, creando una coda di promise pendenti che non verranno mai risolte. La contesa dei lock genera picchi di latenza imprevedibili che peggiorano l'esperienza utente. All'aumentare del numero di richieste concorrenti sotto carico, la contesa si intensifica in modo esponenziale.

Complessità di debugging: I deadlock causati da lock non rilasciati sono notoriamente difficili da debuggare nelle app Node.js in produzione. I sintomi si manifestano lontano dalla causa principale e i processi bloccati mostrano promise in sospeso, ma non specificano quale percorso di eccezione non sia riuscito a rilasciare il lock. Riprodurre l'esatta sequenza di eccezioni che ha attivato il deadlock risulta spesso impossibile negli ambienti di sviluppo.

Esaurimento delle risorse: Oltre ai lock stessi, la mancata liberazione delle risorse è spesso correlata all'incapacità di rilasciare altri elementi come connessioni al database, client Redis o file handle. Questo aggrava il problema, generando molteplici leak di risorse che fanno collassare i sistemi ancora più rapidamente sotto carico.

Esempi di codice

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

Perché non è sicuro: Se viene generato l'errore per fondi insufficienti, accountMutex.release() non viene mai eseguito e il mutex rimane bloccato per sempre. Tutte le chiamate successive a transferFunds() rimarrà in attesa del mutex, bloccando l'intero sistema di pagamento.

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

Perché è sicuro: Il parametro catch il blocco registra l'errore con il relativo contesto prima di rilanciarlo, e il finally blocco garantisce che la funzione di rilascio del mutex venga eseguita indipendentemente dal fatto che l'operazione vada a buon fine, generi un errore o che l'errore venga rilanciato dal blocco catch. Il lock viene rilasciato in ogni caso, prevenendo i deadlock.

Conclusione

Il rilascio del lock deve essere garantito e non subordinato al successo dell'esecuzione. Utilizza try-finally blocchi in JavaScript o l'helper runExclusive() fornito da librerie come async-mutex. Ogni acquisizione di un lock dovrebbe prevedere un percorso di rilascio incondizionato visibile nello stesso blocco di codice. Una corretta gestione dei lock non è un'opzione, è ciò che fa la differenza tra un sistema stabile e uno che si blocca casualmente sotto carico.

FAQ

Hai domande?

Qual è il pattern corretto per garantire il rilascio del lock in 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.

Dovrei usare try-catch-finally o semplicemente try-finally per il rilascio del lock?

Usa try-finally se desideri che le eccezioni vengano propagate al chiamante. Usa try-catch-finally se hai bisogno di gestire l'errore localmente garantendo comunque il rilascio del lock. Il blocco finally viene eseguito in entrambi i casi, ma il blocco catch ti offre la possibilità di registrare, trasformare o sopprimere l'errore. Inserisci sempre release() in finally, mai in catch, poiché finally viene eseguito anche se il blocco catch rilancia l'eccezione.

E per quanto riguarda i lock asincroni con callback anziché con le Promise?

Converti prima il codice basato su callback in Promise, quindi utilizza async/await con try-finally. Se non è possibile, assicurati che ogni percorso di callback (successo, errore, timeout) chiami la funzione di rilascio. Questo approccio è soggetto a errori, motivo per cui i lock basati su Promise sono preferibili. Non fare mai affidamento sul Garbage Collection per rilasciare i lock: non è deterministico e causerà deadlock.

Come gestisco più lock che devono essere acquisiti insieme?

Acquisisci tutti i lock prima di qualsiasi logica di business e rilasciali in ordine inverso all'interno di un unico blocco finally. Un approccio migliore: utilizza una gerarchia di lock in cui i lock vengono sempre acquisiti nello stesso ordine per evitare dipendenze circolari. Per casi complessi, valuta l'uso di un pattern transaction coordinator o di librerie come async-lock che supportano il blocco di risorse multiple con rilascio automatico in caso di qualsiasi errore.

Posso rilasciare un lock in anticipo se so di aver finito di usarlo?

Sì, ma fai estrema attenzione. Una volta rilasciato, non hai più alcuna protezione contro l'accesso concorrente. Un pattern comune consiste nel rilasciare il lock dopo la sezione critica ma prima di operazioni lente come la scrittura di log o chiamate API esterne. Tuttavia, se si verifica un'eccezione dopo il rilascio anticipato ma prima dell'uscita dalla funzione, rischi di trovarti in uno stato inconsistente. Documenta chiaramente il motivo per cui il rilascio anticipato è sicuro.

Quali strumenti possono rilevare lock non rilasciati nel codice JavaScript?

Gli strumenti di analisi statica possono segnalare acquisizioni di lock prive dei corrispondenti blocchi finally. Il rilevamento a runtime è più complesso poiché JavaScript non dispone di un meccanismo integrato per il rilevamento dei deadlock. Implementa dei timeout sull'acquisizione dei lock (la maggior parte delle librerie li supporta) per fallire rapidamente invece di rimanere bloccati indefinitamente. Monitora i tassi di rigetto delle Promise e il ritardo dell'event loop in produzione per individuare problemi di contesa dei lock.

In che modo librerie come async-mutex prevengono questo 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.

Metti in sicurezza ora

Metti in sicurezza il tuo codice, il cloud e il runtime in un unico sistema centralizzato.
Trova e risolvi le vulnerabilità rapidamente e automaticamente.

Nessuna carta di credito richiesta | Risultati della scansione in 32 secondi.