L’idempotence, la suite

Dans l’article précédent, j’expliquais très simplement les risques pour le système à rejouer un ensemble de commandes ou de notifications. Conférer à ces objets une identité est un des prérequis à cela.

Les sagas

Le dernier exemple du précédent article fait émerger certaines problématiques auxquelles peuvent répondre les sagas. Une saga est un gestionnaire d’événements. Lorsque votre système discrétise temporellement votre logique métier par des événements écoutés par plusieurs BCs, le rôle de la saga est de coordonner l’ensemble, pour notamment, permettre des reprises sur erreurs, et fournir une vue globale du système, à savoir le passé, et l’avenir. Les sagas permettent d’implémenter des traitements très longs sans avoir à demander aux différents BCs de gérer eux-mêmes les états transitoires (complexité exponentielle…) : lorsqu’un événement ou une commande est captée par un BC, ce dernier s’en acquitte logiquement par un nouvel événement. Cet acquittement est intercepté par le gestionnaire, qui décide (simplement et en vue très macro) des suites à donner, des commandes à déclencher…

La saga ne contient pas à proprement parler de logique métier, elle est responsable du bon déroulement de l’ensemble du processus. Sans gestionnaire de sagas, nous avons un système qui n’implémente pas cette vue macro des processus. Chaque événement donne lieu à d’autres événements sans qu’il ne soit possible de dire ou le processus global en est, et sans qu’il ne soit possible de décider de stratégies de reprise sur erreurs ou d’idempotence. Le processus piloté par la saga décrit donc un arbre logique relativement prédictible. Les sagas font du sens pour les traitements complexes pouvant durer dans le temps et courir à cheval sur de nombreux BCs. Les sagas permettent aussi de donner d’avantage de connaissance fonctionnelle aux développeurs, plus rapidement. Donc, lorsque la séquence représentant le processus décrit un arbre complexe, et qu’il est impossible d’implémenter une saga, il s’avérera judicieux d’utiliser un identifiant de corrélation, qui sera en fait l’identifiant de la notification originatrice de plus haut niveau, ou autrement dit, la racine de l’arbre. Cet identifiant va permettre de lier entre elles l’ensemble des commandes, et de se substituer partiellement à la saga. Cet identifiant est nécessaire. Sans cet identifiant, lorsque l’on rejoue un événement, chacun des BCs générera de nouveaux événements, en leur conférant une nouvelle identité (traçable, certes…) mais il sera impossible de différencier la première occurrence de la notification des suivantes, car chacune d’elles aura une nouvelle identité. L’identifiant de corrélation permet de définir le contexte d’exécution de la séquence. Si cet identifiant de corrélation est inconnu du BC, il s’agit d’une première occurrence. Dans le cas contraire, il s’agit d’une redondance à prendre en compte.

internal class DomainEvent
{
    protected DomainEvent(DomainEvent root)
    {
        Identity = Guid.NewGuid();

        RootIdentity = root?.Identity ?? Identity;

        CreationDate = DateTime.Now;
        MacAddress = GetMacAddress();
        User = ClaimsPrincipal.Current.Identity;
    }

    public Guid RootIdentity { get; }

    public Guid Identity { get; }

    public DateTime CreationDate { get; }

    public PhysicalAddress MacAddress { get; }

    public IIdentity User { get; set; }

    public bool Done { get; set; }

    private PhysicalAddress GetMacAddress()
    {
        var address = new PhysicalAddress(new byte[0]);
        var nics = NetworkInterface.GetAllNetworkInterfaces();
        var adapter = nics.FirstOrDefault();

        if (adapter != null)
        {
            address = adapter.GetPhysicalAddress();
        }

        return address;
    }
}

Cette syntaxe est lourde, peu pratique, et particulièrement contraignante. Mieux vaut opter pour la création d’un contexte ambiant. Avec ce pattern, chaque notification embarque son identité et l’identité de la racine de la saga. Dès qu’une notification est captée, ou qu’un appel est reçu par une API, on instancie un contexte d’exécution, dans lequel on enferme la valeur correspondant à la racine. L’identifiant de la racine transite donc par les DTOs, et chaque artéfact du code est en mesure d’y accéder. Magique ! Cela change la signature de la classe mère DomainEvent. Le constructeur peut simplement s’écrire :

public DomainEvent()
{
    RootIdentity = AmbientContext<DomainEvent>.Current.RootIdentity;
}

Le gestionnaire de notifications devient :

private void HandleIdempotentEvent<T>(Func<T, bool> isKnown, Action<T> process, T notification) where T : DomainEvent
{
    try
    {
        using (new AmbientContext<T>(notification))
        {
            using (var transaction = new TransactionScope())
            {
                if (isKnown.Invoke(notification))
                {
                    // Run
                    process.Invoke(notification);
                    transaction.Complete();
                }
            }
        }
    }
    catch
    {
        // Log
    }
}

En le contexte pourrait ressembler à cela :

public AmbientContext(DomainEvent notification)
{
    RootIdentity = notification.RootIdentity;
}

Pour conclure

Le sujet de l’idempotence est un sujet d’architectures technique et fonctionnelle. C’est un sujet complexe devant être challengé avec le métier (chaque cas d’utilisation est unique, créer un système idempotent universel n’est pas possible…). Lorsque nous développons une basée sur des messages asynchrones avec consistance éventuelle, il peut arriver qu’une partie du système ne puisse pas prendre livraison d’un ou plusieurs messages. Compte tenu de cela, le système producteur – quel qu’il soit – peut commodément adopter des contre-mesures suite au défaut de livraison constaté, en envoyant ainsi le même message une nouvelle fois, ou peut-être plusieurs fois.

Le gestionnaire recevra le message plusieurs fois, et sera tenté d’exécuter sa logique autant de fois que nécessaire. Pour éviter cela, nous pouvons, par exemple :

  • Nous assurer que la manipulation des messages est intrinsèquement idempotente, ce qui signifie qu’une exécution répétée produira le même résultat…
  • Laisser le gestionnaire garder la trace de messages traités, identifier ceux qui ont déjà manipulé et décider de les jeter…
  • Donner une identité forte à ces messages…
  • Implémenter un contexte ambiant qui est “véhiculé” avec chacun des messages qui transitent…

Cela implique que nous devons trouver un moyen d’identifier des messages identiques, de sorte que nous pouvons comprendre quand une origination multiple est survenue. Le métier peut aider à cela !

Add a Comment

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

31 − 25 =