Aikido

Perché è opportuno rilasciare i blocchi anche nei percorsi di eccezione per evitare i deadlock

Rischio di bug

Regola
Rilascio blocchi anche su percorsi percorsi. 
Ogni blocco acquisizione deve avere una rilascio
liberazione, anche quando si verifichino si verifichino. 

Lingue :** Java, C, C++, PHP, JavaScript,
TypeScript, Go, Python

Introduzione

I blocchi non rilasciati sono una delle cause più comuni di deadlock e blocchi del sistema nelle applicazioni Node.js in produzione. Quando si verifica un'eccezione tra l'acquisizione e il rilascio di un blocco, quest'ultimo rimane attivo a tempo indeterminato. Le altre operazioni asincrone in attesa di quel blocco rimangono bloccate all'infinito, causando errori a cascata in tutto il sistema. Un singolo mutex non rilasciato può mandare in tilt un'intera API perché il ciclo degli eventi viene bloccato e le richieste si accumulano. Ciò accade con librerie come async-mutex, mutexify, o qualsiasi implementazione di blocco manuale in cui lo sblocco non avvenga automaticamente.

Perché è importante

Stabilità e disponibilità del sistema: i blocchi non rilasciati causano deadlock che bloccano le operazioni asincrone in Node.js. Nei server Express o Fastify, ciò esaurisce i worker disponibili, rendendo l’applicazione incapace di gestire nuove richieste. L’unico modo per ripristinare il funzionamento è riavviare il processo, causando un periodo di inattività. Nelle architetture a microservizi, i blocchi non rilasciati in un servizio possono provocare un effetto a cascata di errori nei servizi dipendenti, poiché questi ultimi superano il timeout in attesa delle risposte.

Diminuzione delle prestazioni: prima che si verifichi un deadlock completo, i blocchi non rilasciati causano gravi problemi di prestazioni. Le operazioni asincrone entrano in competizione per le risorse bloccate, creando una coda di promesse in sospeso che non vengono mai risolte. La contesa per i blocchi genera picchi di latenza imprevedibili che compromettono l'esperienza dell'utente. Man mano che il numero di richieste concorrenti aumenta sotto carico, la contesa si aggrava in modo esponenziale.

Complessità del debug: i deadlock causati da blocchi non rilasciati sono notoriamente difficili da risolvere nelle applicazioni Node.js in produzione. I sintomi si manifestano lontano dalla causa principale; i processi bloccati mostrano le promesse in sospeso, ma non indicano quale percorso di eccezione non sia riuscito a rilasciare il blocco. Spesso, negli ambienti di sviluppo, è impossibile riprodurre l'esatta sequenza di eccezioni che ha innescato il deadlock.

Esaurimento delle risorse: oltre ai blocchi stessi, la mancata liberazione dei blocchi è spesso correlata alla mancata liberazione di altre risorse, come le connessioni al database, i client Redis o gli handle dei file. Ciò aggrava il problema, creando molteplici perdite di risorse che causano un collasso più rapido dei sistemi 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 di fondi insufficienti, accountMutex.release() non viene mai eseguita e il mutex rimane bloccato per sempre. Tutte le successive chiamate 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 cattura Il blocco registra l'errore con il contesto prima di rigettarlo, e il finalmente Il 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 rigenerato dal blocco `catch`. Il blocco viene sempre rilasciato, evitando così i deadlock.

Conclusione

Lo sblocco deve essere garantito e non subordinato al corretto esito dell'operazione. Utilizzare try-finally blocchi in JavaScript o il runExclusive() funzione di supporto fornita da librerie come async-mutex. Ogni acquisizione di un blocco dovrebbe avere un percorso di rilascio incondizionato visibile nello stesso blocco di codice. Una corretta gestione dei blocchi non è facoltativa: è ciò che distingue un sistema stabile da uno che si blocca in modo casuale sotto carico.

Domande frequenti

Hai delle domande?

Qual è il codice corretto per garantire il rilascio del blocco 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.

È meglio usare try-catch-finally o semplicemente try-finally per il rilascio del blocco?

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

E che ne dici dei blocchi asincroni con callback al posto delle promesse?

Converti innanzitutto il codice basato su callback in promesse, quindi utilizza async/await con try-finally. Se ciò non è possibile, assicurati che ogni percorso di callback (successo, errore, timeout) chiami la funzione di rilascio. Questo approccio è soggetto a errori, motivo per cui sono preferibili i blocchi basati su promesse. Non affidarti mai alla garbage collection per rilasciare i blocchi: non è deterministica e causerà deadlock.

Come posso gestire più blocchi che devono essere acquisiti contemporaneamente?

Acquisire tutti i blocchi prima di qualsiasi logica di business e rilasciarli in ordine inverso in un unico blocco `finally`. Approccio migliore: utilizzare una gerarchia di blocchi in cui questi vengano sempre acquisiti nello stesso ordine per evitare dipendenze circolari. Per i casi complessi, valutare l'utilizzo di un pattern di coordinamento delle transazioni o di librerie come `async-lock` che supportano il blocco di più risorse con rilascio automatico in caso di errore.

Posso liberare un blocco in anticipo se so di non averne più bisogno?

Sì, ma fai molta attenzione. Una volta liberata la risorsa, non avrai alcuna protezione contro gli accessi simultanei. Una pratica comune consiste nel liberare la risorsa dopo la sezione critica, ma prima di operazioni lente come la registrazione in log o le chiamate alle API esterne. Tuttavia, se si verifica un’eccezione dopo la liberazione anticipata ma prima dell’uscita dalla funzione, si rischia di ottenere uno stato incoerente. Documenta chiaramente perché la liberazione anticipata è sicura.

Quali strumenti consentono di individuare i blocchi non rilasciati nel codice JavaScript?

Gli strumenti di analisi statica possono segnalare acquisizioni di lock prive dei corrispondenti blocchi `finally`. Il rilevamento in fase di esecuzione è più complesso, poiché JavaScript non dispone di un meccanismo integrato per il rilevamento dei deadlock. Implementare dei timeout sull'acquisizione dei lock (la maggior parte delle librerie lo supporta) per garantire un errore rapido anziché un blocco indefinito. Monitorare i tassi di rifiuto delle promesse e il ritardo del ciclo degli eventi in produzione per individuare eventuali problemi di contesa sui lock.

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