Générer des codes uniques avec Drupal 8

Kevin

Kevin

Vincent

Vincent


May 29th 2019 in code test

Le brief

Pour un de nos clients, nous avons été mandatés pour réaliser un générateur de code à usage unique. Quelque chose qui n'arrive pas forcément tous les jours, mais qui est vraiment très intéressant à réaliser.

Pour ce faire, voici les spécificités retenues :

  • Possibilité de générer plusieurs millions de codes uniques
  • La liste des codes doit être fournie à un centre d'impression
  • La durée de vie des codes est indéterminée
  • Les codes doivent être uniquement formés de caractère alphanumérique en majuscule (ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789)
  • Le format doit obligatoirement suivre l'exemple suivant: 12345-ABCDE
  • L'utilisateur doit pouvoir être capable de générer les codes à l'aide d'une interface simple
  • L'utilisateur doit pouvoir spécifier un préfixe aux codes (Ex. la génération de 1’000 codes commençant par AAA)

Challenges

Stockage à vie

L'un des principaux objectifs consiste à garder une trace ad aeternam sur l'ensemble des codes générés.
En effet, ceux-ci peuvent être générés aujourd'hui et utilisés dans quelques années !

Nous avons alors pensé à une solution d'indexation massive à l'aide d'ElasticSearch ou Solr puis nous nous sommes rabattu sur une base de données classique MariaDB.
En effet, la solution Solr ou ElasticSearch nous aurait permis d'améliorer très nettement les performances en lecture sur des milliards de codes, nous n’aurions pas eu la possibilité de gérer des liaisons fortes entre l'utilisation des codes, les utilisateurs et le contexte.
Pour cette raison, nous nous sommes rabattus sur une base de données relationnelle classique.

Temps de génération des codes

Puisque la génération de plusieurs millions de codes uniques peut-être plus au moins long. Nous avons dû créer un système capable de fonctionner en "Background".

1) Nous avons mis en place un Worker (Message Queue) en tâche de fond. Dès lors qu'une demande de génération de code est envoyée, un nouveau message est stocké en base de données en mode "En attente".
Le Cron Drupal s'occupe alors de vérifier - à intervalle régulier - les Messages Queue "En attente" et lance les différents Worker nécessaires - dont notre génération de code.

2) À la fin de la tâche de génération des codes, une liste de code est sauvegardée - pour archives - sur le serveur et une copie est envoyée par email à l'administrateur ayant effectué la requête lors de l'étape N°1.

Un nombre de combinaison limité

La contrainte du format XXXX-YYYY couplé avec la demande de spécification de Préfix fût un réel défi.
En effet, le format nous restreint à un nombre limité de code et la possibilité de fixer un préfix diminue d'autant plus ce range.

Sans parler du fait qu'un préfix est réutilisable plusieurs fois, il nous a donc fallu près calculer le nombre de codes déjà existant et le nombre de codes possible restant - avant la génération du Message Queue - afin de prévenir l'administrateur lorsqu'il effectue des demandes impossibles.

Écritures massives en base de données

Notre outil doit être capable de générer 1'000'000 de codes en peu de temps et de manière extrêmement fiable.

Lorsque nous devons générer des millions d’entités de manière concurrente, le plus grand danger est la fiabilité de la base de données - que ce passe-t-il si notre Script crash/out-of-memory durant les 40'000 derniers codes ?
Nous ne pouvons pas nous permettre de perdre 960'000 codes dans notre Database à cause d’un erreur serveur/PHP/SQL/.

Nous avons alors deux possibilités pour générer beaucoup de codes:

1. Insertion en Database

L'idée de cette solution est de créer des codes les uns après les autres et de les sauvegarder en base de données en même temps.

Cette solution à l'avantage de sauvegarder directement les codes et facilite grandement l'algorithme d’unicité des codes (puisque nous connaissons l'ensemble des codes durant toute la phase de génération).
Le premier désavantage flagrant est qu'il nous faudra alors "cleanup" la base de données en cas de Crash SQL/PHP des codes pending.
Le deuxième désavantage résident dans le stresse auxquels nous soumettons la base de données, celle-ci se retrouve sollicitée en écriture pour chaque code - autant dire une horreur.

2. Transactions SQL

La seconde solution est l'utilisation des Transactions SQL, ces petits bijoux de technologies sont capables de mettre en "buffer" un nombre incalculable d'opérations SQL - dans notre cas des opérations INSERT - et de les exécuter en batch lorsque la base de données aura du temps.

L'un des nombreux avantages des Transactions SQL réside dans le système de "Roolback" - si une seule opération échoue, l'entièreté du "buffer" est invalidée et la base de données ne garde aucune trace des opérations.

Solution

Nous avons bien évidemment opté pour la solution des Transactions SQL qui nous a permis la génération de millions de codes en quelques secondes, et ce de manière extrêmement fiable et rapide.

Aider l'utilisateur à la création à l'aide d'une interface dans l'administration du back-office

Pour permettre avec aisance cette génération, nous avons mis à disposition un formulaire.
Il renseigne les informations nécessaires (préfixe, nombre, email).
Cela, nous permet aisément de laisser l'utilisateur réaliser soit même les codes nécessaires.

Algorithme

Voyons maintenant comment nous avons pensé notre algorithme:

  1. Obtenir le nombre de codes existants dans la base de données en commençant par le préfixe correspondant.
  2. Calculer le nombre de combinaisons uniques avec ce préfixe.
  3. Calculer le nombre de codes uniques restants disponible pour la création.
  4. Générer un fichier avec les codes
  5. Calculer le nombre de boucles avec le nombre de codes défini lors d'une insertion (paquet de 1000 codes en l'occurrence)
  6. Enregistrer les codes
  7. Enregistrer le fichier

Interface d'administration

Comment notre formulaire fonctionne?

  1. Contrôle que l'email est valide.
  2. Vérifier que le préfixe existe dans le dictionnaire défini.
  3. Récupération du type de code à générer.
  4. Obtenir le nombre de codes existants dans la base de données en commençant par le préfixe correspondant.
  5. Calculer le nombre de combinaisons uniques avec ce préfixe.
  6. Calculer le nombre de codes uniques restants disponible pour la création.
  7. Vérifier que le nombre de code demandés sont disponibles
  8. Vérifier que le préfixe est inférieur à la longueur des codes que nous allons générer.

Une fois toutes ces étapes validées, les informations, sont sauvegardées dans la queue Drupal et consommées par le cron qui exécute notre algorithme de génération des codes, puis un email est envoyé au destinataire de cette demande avec le fichier contenant les codes générés.

Ligne de commande

Pour utiliser au mieux la ligne de commande, nous avons utilisé deux composants Symfony :

Tout au long de l'exécution, on peut voir la barre de progression si on exécute la ligne de commande, jusqu'à ce que le processus se termine.

Parlons un peu de code et d'architecture

Commands :

  • GenerateCommand : Commande pour la génération des codes

Entity :

  • CodeClasse : Une classe nous permettant de structurer la manière dont un code doit être généré.

Service :

  • CodeBase : Classe abstraite avec une logique commune aux futurs autres services de générations de Code (compter le nombre de codes par préfixe, génération d'un code, génération de plusieurs codes, insertion en base de données ...)
  • CodeGeneration : Implémente le service CodeBase pour la génération de code via la Class CodeClasse.

Tests

Nous avons réalisé des tests pour vérifier que tout fonctionne à l'aide PHPUNIT.

Unit Test

  • Test de génération de code avec vérification de la longueur des codes générés
  • Test sur les combinaisons possibles suivant le préfixe donné

Kernel Test

  • Vérifier le nombre de codes présents en base de données avec un préfixe indiqué.
  • Générer un nombre X de codes un par un avec l'assertion de base de données et vérifier le nombre correspond en base de données (un par un puis en paquet de 1000 codes).
  • Tester avec un préfixe trop long, pour qu'il n'y ait aucun code de généré
  • Tester le nombre de possibilités pour la génération des codes avec un préfixe et un dictionnaire donné.