📐 Construire une app en JS
6. L'intégrité

L'intégrité

Reprenons notre commande de réservation book().

Elle provoque une modification de l'état du système, puisqu'elle modifie le calendrier des réservations de l'hébergement.

Mais qu'est-ce qu'elle ne modifie pas ?

Bah tout le reste.

Mais posons la question autrement : qu'est-ce qui doit rester exactement pareil lors du processus de réservation ?

Les entités qui doivent être inchangées

Pour que le processus de réservation n'enfreigne pas les règles métier, certaines entités doivent demeurer inchangées au cours du traitement de la réservation.

  1. L'hébergement Si le nombre de chambres de l'hébergement est modifié pendant la réservation, on risque d'accepter une réservation pour 6 personnes alors que l'hébergement vient d'être modifié pour n'accueillir que 4.

  2. Le locataire Si l'administrateur décide de geler le compte utilisateur du locataire (parce qu'il a eu un litige avec une précédente réservation), on ne veut pas d'une nouvelle réservation.

  3. Le calendrier initial Si une autre réservation est réalisée simultanément par un autre locataire, on risque d'avoir 2 réservations sur la même période.

// Avant : le calendrier est vide
const initialCalendar = Calendar.of(accommodation)
expect(initialCalendar.count()).toBe(0)
 
// 2 locataires différents tentent une réservation simultanée
const bookings = await Promise.all([
    book(accommodation, tenant1, stay),
    book(accommodation, tenant2, stay),
])
 
// 1 seule réservation est acceptée.
const alteredCalendar = Calendar.of(accommodation)
expect(alteredCalendar.count()).toBe(1)

Commande et transaction

La plupart des développeurs se désintéressent de cette question parce que les cas sont très rares. Il est très improbable d'avoir une modification de l'hébergement ou du locataire exactement au milieu d'un processus de réservation.

Très improbable, tant que le système est peu sollicité.

Si par chance vous êtes victime de votre succès, et que les réservations pleuvent, alors les bugs pleuvent aussi. Des bugs difficiles à reproduire (donc à corriger) parce qu'ils surviennent de façon aléatoire.

Pourtant, ça n'est pas compliqué de s'en préoccuper. Il suffit d'isoler le périmètre des données nécessaires à l'exécution du traitement.

Pendant la réservation d'un hébergement par un locataire, le système doit verrouiller toute modification du calendrier, de l'hébergement et du locataire. On nomme parfois ce périmètre un agrégat.

Pour empêcher la modification de l'agrégat, on doit l'isoler au travers d'une transaction.

La transaction possède un début et une fin. Elle encapsule la commande afin d'isoler les entités de son agrégat.

Implémentation d'un système de transaction

Le développeur a 3 choix pour aborder cette question de l'isolation :

  1. Il ne le prend pas en compte (volontairement). Parfois, ça n'est juste pas grave d'avoir une incohérence. Car sa probabilité d'occurrence reste très faible.

  2. Déléguer la question à la base de données relationnelle. Les bases de données implémentent un mécanisme de transaction. La norme SQL définit quatre niveaux d'isolation. PostgreSQL les accepte tous les quatre, mais n'en implémente que trois comportements distincts (read uncommitted s'y comporte comme read committed) :

  • read committed (le défaut)
  • repeatable read
  • serializable

Ce niveau est paramétrable pour chaque transaction. Il doit faire l'objet d'un compromis : plus le niveau d'isolation est élevé, plus le risque d'échec de la commande augmente.

  1. Coder un sémaphore En JavaScript, la boucle d'événements (event loop) est mono-thread. Donc on peut facilement coder une liste globale des hébergements en cours de réservation.

La commande débute par le contrôle de cette liste (y a-t-il déjà le même hébergement en cours de réservation ?), puis l'ajoute immédiatement, sans suspendre l'exécution — pas de await entre le contrôle et l'ajout, sinon la garantie disparaît. À la fin de la commande, on retire l'hébergement de la liste. Dans un finally, sans quoi une erreur laisserait l'hébergement verrouillé pour toujours.

Attention : cette liste ne protège qu'un seul processus. Dès que l'on multiplie les workers du backend, chacun a la sienne, et la garantie s'évanouit. Il faut alors router les réservations d'un même hébergement vers le même nœud (affinité par hachage cohérent), ou déléguer le verrou à un service partagé — la base de données, ou un Redis.

Pour aller plus loin : les niveaux d'isolation des transactions PostgreSQL (opens in a new tab).