31/10/2018 3 Minutes read Tech 

Ajout du Lock Component dans Symfony 3.3

Découvrez ce que le nouveau Lock Component présenté dans Symfony 3.3 a à offrir.

Un nouveau composant

C’est un problème récurrent et qu’on l’on ne tarde pas à rencontrer lorsque des mots tels que “workers”, “parallèle” ou “concurrence” apparaissent dans une spécification technique. Je veux parler du verrouillage de l’accès à une ressource par un thread ou processus. Dernière nouveauté en date sur Symfony 3.3, le component Lock fait son apparition. Il vient remplacer le LockHandler déjà disponible depuis la version 2.6, pratique mais très limité en particulier par sa restriction à une architecture mono serveur. 

Cette nouvelle version propose désormais un choix complet d’implémentations au travers d’une StoreInterface, incluant Redis et Memcache et permettant ainsi une utilisation dans une architecture multi serveurs.

Plus de choix

Les implémentations proposées sont les suivantes :

  • Flock : Verrou par fichier classique utilisant la fonction flock() de php.
  • Semaphore : Verrou par sémaphore (variables de type compteur) qui utilise les fonctions sem_* de php (nécessite l’installation de l’extension sysvsem).
  • Memcache & Redis : Certainement le point fort du composant, cela permet de stocker le verrou sur l’une de ces bases de données clé-valeur en RAM. Si des processus existent sur différentes instances, chacun voulant accéder à une même ressource, alors ils peuvent accéder à un redis/memcache central pour obtenir ou libérer un verrou, et ainsi, se synchroniser efficacement.
  • RetryTillSave : Enrichit une implémentation de StoreInterface avec une fonction permettant de retenter l’acquisition n fois (bel exemple d’utilisation du pattern decorator si vous voulez voir). Par défaut le store va réessayer d’acquérir le verrou un nombre de fois égal à php_int_max soit tout de même 9223372036854775807 fois sur une machine 64 bit !
  • CombinedStore : Comme son nom l’indique, permet d’acquérir un verrou en se basant sur le résultat de l’acquisition à travers plusieurs stores. Par exemple, si l’infrastructure dispose de plusieurs redis, l’acquisition peut être considérée comme réussie si elle a réussie individuellement sur chaque redis. La stratégie d’acquisition peut être au choix à l’unanimité ou à la majorité des stores.

Dans la pratique

Ci-dessous deux exemples issus du blog Symfony :

Utilisation basique du composant

use SymfonyComponentLockFactory;
use SymfonyComponentLockStoreSemaphoreStore;

$store = new SemaphoreStore();
$factory = new Factory($store);
$lock = $factory->createLock('pdf-invoice-generation');

if ($lock->acquire()) {
// The resource "pdf-invoice-generation" is locked.
// You can compute and generate invoice safely here.

$lock->release();
}

Évidemment nous ne voulons pas instancier ces classes directement dans notre code métier en adeptes de la réduction des dépendances que nous sommes. Nous voulons manipuler des services, ce qui est prévu dans une très prochaine intégration au framework

Utilisation cible du composant après intégration dans Symfony

# app/config/config.yml
framework:
# these are all the supported flock stores
lock: 'flock'
slock: 'semaphore'
mlock: 'memcached://m1.docker'
mclock: ['memcached://m1.docker', 'memcached://m2.docker']
rlock: 'redis://r1.docker'
rclock: ['redis://r1.docker', 'redis://r2.docker']

On peut noter dans cette exemple l’utilisation élégante du ‘CombinedStore’ à travers la notation en tableau dans la configuration yaml.

// non-blocking, non-expiring
$lock = $container->get('lock')->acquire();
// blocking, non-expiring (waits indefinitely until the lock is acquired)
$lock = $container->get('lock')->acquire(true);
// non-blocking, expiring in 60 seconds (unless the lock is refreshed)
$lock = $container->get('lock')->acquire(false, 60);

// normal operations on the lock
$lock->isAcquired();
$lock->refresh();
$lock->release();

On constatera aussi les arguments permettant de choisir le comportement à adopter par le processus qui essaye d’acquérir le verrou : en cas d’échec s’arrêter ou réessayer, en cas de succès définir une durée de vie.

En bref

Parmi la myriade de nouveautés à venir avec Symfony 4, l’apparition de ce composant peut paraître mineure, mais aujourd’hui nous avons une (petite) roue de moins à réinventer. Et c’est avant tout pour cela que Symfony existe.

N.b : On pourrait s’etendre ici sur la pertinence du choix entre flock ou sémaphore, puisque les deux nous sont proposés, mais cela dépasserait le cadre de ce billet. En effet, de nombreux articles et documentations officielles couvrent déjà en détails les fonctions C sous jacentes à ces mécanismes d’exclusions.