Aikido

Libérer les verrous même sur les chemins d'exception : prévenir les blocages

Risque de bogue

Règle
Libération blocage même sur d'exception d'exception. 
Chaque verrouillage acquisition doit avoir a garantie
garantie, même lorsque exceptions se produisent. 

Langues pris en charge:** Java, C, C++, PHP, JavaScript,
TypeScript, Go, Python

Introduction

Les verrous non libérés sont l'une des causes les plus courantes de blocages et d'arrêts du système dans les applications Node.js de production. Lorsqu'une exception se produit entre l'acquisition du verrou et sa libération, le verrou reste bloqué indéfiniment. Les autres opérations asynchrones qui attendent ce verrou sont bloquées pour toujours, ce qui provoque des pannes en cascade sur le système. Un seul mutex non libéré peut entraîner l'arrêt d'une API entière, car la boucle d'événements se bloque et les demandes s'accumulent. Cela se produit avec des bibliothèques comme async-mutex, mutexifierou tout système de verrouillage manuel dont le déverrouillage n'est pas automatique.

Pourquoi c'est important

Stabilité et disponibilité du système : Les verrous non libérés provoquent des blocages qui bloquent les opérations asynchrones dans Node.js. Dans les serveurs Express ou Fastify, cela épuise les travailleurs disponibles, rendant l'application incapable de traiter de nouvelles requêtes. La seule solution consiste à redémarrer le processus, ce qui entraîne des temps d'arrêt. Dans les architectures microservices, les verrous non libérés dans un service peuvent entraîner des défaillances en cascade dans les services dépendants qui attendent des réponses.

Dégradation des performances : Avant le blocage complet, les verrous non libérés entraînent de graves problèmes de performance. Les opérations asynchrones se disputent les ressources verrouillées, créant une file d'attente de promesses en suspens qui ne sont jamais résolues. La contention des verrous crée des pics de latence imprévisibles qui dégradent l'expérience de l'utilisateur. Lorsque le nombre de requêtes simultanées augmente sous l'effet de la charge, la contestation s'aggrave de manière exponentielle.

Complexité du débogage : Les blocages dus à des verrous non libérés sont notoirement difficiles à déboguer dans les applications Node.js de production. Les symptômes apparaissent loin de la cause première, les processus suspendus montrent les promesses en attente mais pas le chemin d'exception qui n'a pas permis de libérer le verrou. Reproduire la séquence exacte d'exceptions qui a déclenché le blocage est souvent impossible dans les environnements de développement.

Épuisement des ressources : Au-delà des verrous eux-mêmes, l'incapacité à libérer les verrous est souvent liée à l'incapacité à libérer d'autres ressources telles que les connexions aux bases de données, les clients Redis ou les gestionnaires de fichiers. Cela aggrave le problème, en créant de multiples fuites de ressources qui font tomber les systèmes plus rapidement sous la charge.

Exemples de code

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

Pourquoi ce n'est pas sûr : Si l'erreur "fonds insuffisants" est déclenchée, accountMutex.release() ne s'exécute jamais et le mutex reste verrouillé pour toujours. Tous les appels ultérieurs à transferFunds() se bloquera dans l'attente du mutex, gelant ainsi l'ensemble du système de paiement.

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

Pourquoi c'est sûr : Le attraper enregistre l'erreur avec le contexte avant de la relancer, et le bloc enfin garantit que la fonction de libération du mutex s'exécute, que l'opération réussisse, qu'elle provoque une erreur ou que l'erreur soit à nouveau provoquée par la fonction catch. Le verrou est toujours libéré, ce qui évite les blocages.

Conclusion

La libération du verrou doit être garantie et non conditionnée par une exécution réussie. Utiliser essai final en JavaScript ou les blocs runExclusive() fournie par des bibliothèques telles que async-mutex. Chaque acquisition de verrou doit avoir un chemin de libération inconditionnel visible dans le même bloc de code. Une bonne gestion des verrous n'est pas facultative, c'est la différence entre un système stable et un système qui se bloque aléatoirement sous charge.

FAQ

Vous avez des questions ?

Quel est le modèle correct pour garantir le déblocage d'un verrou 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.

Dois-je utiliser try-catch-finally ou simplement try-finally pour le déblocage des verrous ?

Utilisez try-finally si vous voulez que les exceptions se propagent à l'appelant. Utilisez try-catch-finally si vous devez gérer l'erreur localement tout en garantissant la libération du verrou. Le bloc finally s'exécute dans les deux cas, mais catch vous permet d'enregistrer, de transformer ou de supprimer l'erreur. Placez toujours release() dans finally, jamais dans catch, car finally s'exécute même si catch relance.

Qu'en est-il des verrous asynchrones avec des callbacks au lieu de promesses ?

Commencez par convertir le code basé sur les callbacks en promesses, puis utilisez async/await avec try-finally. Si ce n'est pas possible, assurez-vous que chaque chemin de callback (succès, erreur, timeout) appelle la fonction release. C'est une source d'erreurs, c'est pourquoi les verrous basés sur des promesses sont préférables. Ne comptez jamais sur le garbage collection pour libérer les verrous, ce n'est pas déterministe et provoquera des blocages.

Comment gérer plusieurs serrures qui doivent être acquises ensemble ?

Acquérir tous les verrous avant toute logique commerciale et les libérer dans l'ordre inverse dans un seul bloc final. Meilleure approche : utiliser une hiérarchie de verrous où les verrous sont toujours acquis dans le même ordre pour éviter les dépendances circulaires. Pour les cas complexes, envisagez d'utiliser un modèle de coordinateur de transaction ou des bibliothèques telles que async-lock qui prennent en charge le verrouillage de ressources multiples avec libération automatique en cas d'échec.

Puis-je débloquer une serrure de manière anticipée si je sais que j'en ai fini avec elle ?

Oui, mais soyez extrêmement prudent. Une fois libéré, vous n'avez plus aucune protection contre les accès concurrents. Un modèle courant consiste à libérer après une section critique, mais avant des opérations lentes telles que la journalisation ou les appels d'API externes. Cependant, si une exception survient après la libération anticipée mais avant la sortie de la fonction, vous risquez un état incohérent. Documentez clairement les raisons pour lesquelles la libération anticipée est sûre.

Quels outils permettent de détecter les verrous non libérés dans le code JavaScript ?

Les outils d'analyse statique peuvent repérer les acquisitions de verrous sans blocs finally correspondants. La détection en cours d'exécution est plus difficile car JavaScript ne dispose pas d'un système intégré de détection des blocages. Mettez en place des délais d'attente lors de l'acquisition d'un verrou (la plupart des bibliothèques prennent cela en charge) afin d'échouer rapidement au lieu de rester bloqué indéfiniment. Surveillez les taux de rejet des promesses et le retard des boucles d'événements en production pour détecter les problèmes de contention des verrous.

Comment des bibliothèques comme async-mutex permettent-elles d'éviter ce problème ?

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.

Obtenir la sécurité gratuitement

Sécurisez votre code, votre cloud et votre environnement d'exécution dans un système central.
Trouvez et corrigez rapidement et automatiquement les vulnérabilités.

Aucune carte de crédit n'est requise | Scanner les résultats en 32sec.