Synchronisation distribuée en C# avec Azure Storage

François Hyvrier
Nov 4, 2019 · 5 min read
Image for post
Image for post
La gare centrale de Francfort : “Comment synchroniser les accès aux voies pour éviter l’apocalypse ?”

Beaucoup de langages proposent des mécanismes de lock pour interdire ou limiter les accès simultanés à une même ressource. En C#, on trouve l’instruction lock et des classes telles que Monitor, Mutex, Semaphore, et bien d’autres dans le namespace System.Threading.

Cependant ces outils ne peuvent généralement être appliqués que dans le contexte d’une application multi-threadée ou bien de plusieurs applications exécutées sur la même machine. C’est le cas de la classe Mutex du framework .NET, qui permet de synchroniser plusieurs threads d’une même application ou plusieurs applications exécutées par le même système d’exploitation.

Il n’existe pas de solution prête à l’emploi lorsque les applications à synchroniser sont exécutées sur des machines différentes, il faut se reposer sur les fonctionnalités d’autres produits, comme par exemple Azure Storage.

Azure Storage est un ensemble de services de stockage proposés par Microsoft. Parmi eux, le service Azure Blobs permet de stocker des fichiers texte ou binaires et de leur appliquer des opérations élémentaires (création, modification, suppression, copie, etc.).

L’opération « Lease Blob » (obtenir un bail sur un blob) sert notamment à acquérir un accès exclusif à un fichier pour empêcher qu’il soit modifié ou supprimé par plusieurs auteurs en même temps. Avant de modifier un fichier, un auteur doit donc appeler l’opération « Lease Blob » pour obtenir un accès. Si le fichier est déjà accédé par quelqu’un d’autre l’opération « Lease Blob » lèvera une exception avec le statut « Conflict », mais si ce n’est pas le cas elle rendra la main à l’appelant qui sera alors le seul à pouvoir modifier le fichier jusqu’à ce qu’il invoque l’opération « Release lease », ou bien jusqu’à l’expiration de la durée d’accès exclusif, ou encore jusqu’à ce que quelqu’un invoque l’opération « Break lease ».

C’est ce mécanisme de bail que nous allons utiliser pour implémenter un lock distribué. Le principe est très simple : un lock = un blob (un fichier).

Interface du lock distribué

Les méthodes exposées par le lock distribué sont basiques :

  • AcquireAsync permet d’obtenir le lock et de le conserver pour une durée donnée. Cette méthode attend aussi un paramètre « timeout », il s’agit de la durée maximale que l’on souhaite attendre la libération du lock lorsqu’il est déjà pris par quelqu’un d’autre. Si l’accès n’a pas pu être obtenu au-delà de ce délai, une TimeoutException sera levée, charge à l’appelant de la catcher pour éventuellement tenter à nouveau d’obtenir le lock.

Voici un exemple d’utilisation de ce lock :

Un exemple d’implémentation

Sous le capot, nous allons avoir besoin d’un blob, et donc d’un compte Azure Storage et d’un conteneur où sera localisé ce blob. C’est pourquoi la classe AzureStorageBlobDistributedLock implémentant l’interface IDistributedLock attend en paramètre de son constructeur la chaîne de connexion au compte de stockage, le nom du conteneur, et également le nom du blob. Au passage, il faudra rajouter une dépendance au package NuGet WindowsAzure.Storage.

Dans cette implémentation, c’est la classe AzureStorageBlobDistributedLock qui sera responsable de créer le conteneur et le blob s’ils n’existent pas déjà, à l’aide de la méthode GetBlockBlobReferenceAsync :

L’obtention d’un bail sur un blob se fait grâce à la méthode AcquireLeaseAsync, mais comme elle lève une exception lorsqu’il existe déjà un bail pour le blob, nous devons gérer cette exception pour éviter que notre programme plante. Nous allons mettre en place un mécanisme de retry : l’idée est qu’en cas d’exception, le programme attende un certain temps (quelques centaines de millisecondes), et essaye à nouveau d’acquérir le bail. Si au-delà de la durée « timeout » le bail n’a pas pu être obtenu, la méthode lève une TimeoutException. Ce comportement s’implémente sans difficulté avec un try/catch, mais ce cas est un bon prétexte pour utiliser la bibliothèque Polly, spécialisée dans la gestion des erreurs.

Polly propose des « policies » représentant chacune un comportement différent à appliquer en cas de problème. Nous allons utiliser ici les policies de type « Retry » et « Timeout » :

  • La policy Retry nous servira à intercepter les exceptions indiquant que le bail du blob n’a pas pu être obtenu et à retenter l’opération quelques millisecondes plus tard.

Ainsi on a défini en une dizaine de lignes de codes :

  • Une politique de retry qui ne s’applique qu’en cas de StorageException avec un statut « Conflict ». Elle fera en sorte d’attendre un temps aléatoire compris entre une et deux secondes entre deux tentatives.

L’implémentation de la méthode ReleaseAsync consiste seulement à appeler la méthode ReleaseLeaseAsync du blob en lui passant le lease id obtenu lors de l’acquisition du lock.

Voici la classe AzureStorageBlobDistributedLock dans son intégralité :

Avec cette classe on peut maintenant synchroniser simplement des processus exécutés par des systèmes différents, il suffit qu’ils aient tous configuré la classe AzureStorageBlobDistributedLock de la même façon (mêmes connection string, container name et blob name).

Remarques

La fonctionnalité « Lease blob » d’Azure Blobs peut acquérir un bail pour une durée infinie ou bien comprise entre 15 et 60 secondes. Pour conserver un bail plus longtemps il faut faire appel à l’opération « Renew lease ».

La classe AzureStorageBlobDistributedLock présentée ici ne permet pas d’acquérir des accès exclusifs plus longs que 60 secondes, cela signifie que le traitement à effectuer une fois l’accès exclusif obtenu ne doit pas durer plus de 60 secondes non plus, car au-delà de cette durée, le verrou aura expiré et un autre thread aura pu entrer dans la section critique. Une solution à cela consiste à borner le temps d’exécution de la logique de la section critique, par exemple à l’aide d’une policy de type Timeout du package NuGet Polly.

Si jamais vous avez besoin d’obtenir des locks pour des durées de plus d’une minute, il faudra adapter la classe AzureStorageBlobDistributedLock, voici quelques pistes :

  • Au lieu d’obtenir un bail sur le blob pour une durée finie, demander un bail d’une durée infinie. Attention : en faisant cela on prend le risque qu’un lock distribué ne soit jamais libéré, cela peut se produire si l’application s’arrête avant d’avoir pu mettre fin au bail du blob.

Quelques liens sur ce sujet :

YounitedTech

Le blog Tech de Younited, où l’on parle de développement…

François Hyvrier

Written by

YounitedTech

Le blog Tech de Younited, où l’on parle de développement, d’architecture, de microservices, de cloud, de data… Et de comment on s’organise pour faire tout ça. Ah, et on recrute aussi, on vous a dit ?

François Hyvrier

Written by

YounitedTech

Le blog Tech de Younited, où l’on parle de développement, d’architecture, de microservices, de cloud, de data… Et de comment on s’organise pour faire tout ça. Ah, et on recrute aussi, on vous a dit ?

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store