Persistance des grains

Plusieurs objets de données persistants nommés peuvent être associés aux grains. Ces objets d’état sont chargés à partir du stockage pendant l’activation des grains afin d’être disponibles lors des demandes. La persistance des grains utilise un modèle de plug-in extensible afin que des fournisseurs de stockage pour n’importe quelle base de données puissent être utilisés. Ce modèle de persistance est conçu pour des raisons de simplicité et n’est pas destiné à couvrir tous les modèles d’accès aux données. Les grains peuvent également accéder directement aux bases de données, sans utiliser le modèle de persistance de grains.

Dans le diagramme ci-dessus, UserGrain a un état Profil et un état Panier, stockés dans des systèmes de stockage distincts.

Objectifs

  1. Plusieurs objets de données persistants nommés par grain.
  2. Plusieurs fournisseurs de stockage configurés, chacun pouvant avoir une configuration différente et être soutenu par un système de stockage différent.
  3. Les fournisseurs de stockage peuvent être développés et publiés par la communauté.
  4. Les fournisseurs de stockage contrôlent complètement la façon dont ils stockent les données d’état des grains dans le magasin de stockage persistant. Corollaire : Orleans ne fournit pas de solution de stockage ORM complète, mais permet plutôt aux fournisseurs de stockage personnalisés de prendre en charge des exigences ORM spécifiques comme et quand cela est nécessaire.

Paquets

Des fournisseurs de stockage de grains Orleans sont disponibles dans NuGet. Les packages officiellement gérés incluent les suivants :

API

Les grains interagissent avec leur état persistant avec IPersistentState<TState>TState est le type d’état sérialisable :

public interface IPersistentState<TState> where TState : new()
{
    TState State { get; set; }
    string Etag { get; }
    Task ClearStateAsync();
    Task WriteStateAsync();
    Task ReadStateAsync();
}

Les instances de IPersistentState<TState> sont injectées dans le grain en tant que paramètres de constructeur. Ces paramètres peuvent être annotés avec un attribut PersistentStateAttribute pour identifier le nom de l’état injecté et le nom du fournisseur de stockage qui le fournit. L’exemple suivant illustre cela en injectant deux états nommés dans le constructeur UserGrain :

public class UserGrain : Grain, IUserGrain
{
    private readonly IPersistentState<ProfileState> _profile;
    private readonly IPersistentState<CartState> _cart;

    public UserGrain(
        [PersistentState("profile", "profileStore")] IPersistentState<ProfileState> profile,
        [PersistentState("cart", "cartStore")] IPersistentState<CartState> cart)
    {
        _profile = profile;
        _cart = cart;
    }
}

Différents types de grain peuvent utiliser différents fournisseurs de stockage configurés, même si les deux sont du même type. Par exemple, deux instances de fournisseur de Stockage Table Azure différentes, connectées à différents comptes de Stockage Azure.

Lire l’état

L’état du grain est automatiquement lu lorsque le grain est activé, mais les grains sont responsables du déclenchement explicite de l’écriture pour tout état de grain modifié, si nécessaire.

Si un grain souhaite relire explicitement l’état le plus récent d’un autre grain à partir du magasin de stockage, le grain doit appeler la méthode ReadStateAsync. Cela recharge l’état du grain à partir du magasin persistant via le fournisseur de stockage, et la copie en mémoire précédente de l’état du grain est écrasée et remplacée quand l’opération Task de ReadStateAsync() se termine.

La valeur de l’état est accessible à l’aide de la propriété State. Par exemple, la méthode suivante accède à l’état de profil déclaré dans le code ci-dessus :

public Task<string> GetNameAsync() => Task.FromResult(_profile.State.Name);

Il n’est pas nécessaire d’appeler ReadStateAsync() pendant l’opération normale. L’état est chargé automatiquement pendant l’activation. Toutefois, ReadStateAsync() peut être utilisé pour actualiser l’état qui est modifié en externe.

Consultez la section Modes d’échec ci-dessous pour plus d’informations sur les mécanismes de gestion des erreurs.

Écrire l’état

L’état peut être modifié via la propriété State. L’état modifié n’est pas conservé automatiquement. Au lieu de cela, le développeur décide quand conserver l’état en appelant la méthode WriteStateAsync. Par exemple, la méthode suivante met à jour une propriété sur State et rend persistant l’état mis à jour :

public async Task SetNameAsync(string name)
{
    _profile.State.Name = name;
    await _profile.WriteStateAsync();
}

Conceptuellement, le runtime Orleans prend une copie complète de l’objet de données d’état du grain pour son utilisation pendant toutes les opérations d’écriture. Sous les couvertures, le runtime peut utiliser des règles d’optimisation et des heuristiques pour éviter d’effectuer une partie ou l’ensemble de la copie complète dans certaines circonstances, à condition que la sémantique d’isolation logique attendue soit conservée.

Consultez la section Modes d’échec ci-dessous pour plus d’informations sur les mécanismes de gestion des erreurs.

Effacer l’état

La méthode ClearStateAsync efface l’état du grain dans le stockage. Selon le fournisseur, cette opération peut éventuellement supprimer entièrement l’état du grain.

Bien démarrer

Pour qu’un grain puisse utiliser la persistance, un fournisseur de stockage doit être configuré sur le silo.

Tout d’abord, configurez des fournisseurs de stockage, un pour l’état du profil et un pour l’état du panier :

var host = new HostBuilder()
    .UseOrleans(siloBuilder =>
    {
        siloBuilder.AddAzureTableGrainStorage(
            name: "profileStore",
            configureOptions: options =>
            {
                // Use JSON for serializing the state in storage
                options.UseJson = true;

                // Configure the storage connection key
                options.ConnectionString =
                    "DefaultEndpointsProtocol=https;AccountName=data1;AccountKey=SOMETHING1";
            })
            .AddAzureBlobGrainStorage(
                name: "cartStore",
                configureOptions: options =>
                {
                    // Use JSON for serializing the state in storage
                    options.UseJson = true;

                    // Configure the storage connection key
                    options.ConnectionString =
                        "DefaultEndpointsProtocol=https;AccountName=data2;AccountKey=SOMETHING2";
                });
    })
    .Build();

Maintenant qu’un fournisseur de stockage a été configuré avec le nom "profileStore", nous pouvons accéder à ce fournisseur à partir d’un grain.

L’état persistant peut être ajouté à un grain de deux manières principales :

  1. En injectant IPersistentState<TState> dans le constructeur du grain.
  2. En héritant de Grain<TGrainState>.

La méthode recommandée pour ajouter du stockage à un grain consiste à injecter IPersistentState<TState> dans le constructeur du grain avec un attribut [PersistentState("stateName", "providerName")] associé. Pour plus de détails sur Grain<TState>, voir ci-dessous. Cette approche est toujours prise en charge, mais elle est considérée comme une approche héritée.

Déclarez une classe pour contenir l’état de notre grain :

[Serializable]
public class ProfileState
{
    public string Name { get; set; }

    public Date DateOfBirth
}

Injectez IPersistentState<ProfileState> dans le constructeur du grain :

public class UserGrain : Grain, IUserGrain
{
    private readonly IPersistentState<ProfileState> _profile;

    public UserGrain(
        [PersistentState("profile", "profileStore")]
        IPersistentState<ProfileState> profile)
    {
        _profile = profile;
    }
}

Notes

L’état du profil ne sera pas chargé au moment où il est injecté dans le constructeur, de sorte qu’il n’est pas valide d’y accéder à ce moment-là. L’état est chargé avant que OnActivateAsync soit appelé.

Maintenant que le grain a un état persistant, nous pouvons ajouter des méthodes pour lire et écrire cet état :

public class UserGrain : Grain, IUserGrain
    {
    private readonly IPersistentState<ProfileState> _profile;

    public UserGrain(
        [PersistentState("profile", "profileStore")]
        IPersistentState<ProfileState> profile)
    {
        _profile = profile;
    }

    public Task<string> GetNameAsync() => Task.FromResult(_profile.State.Name);

    public async Task SetNameAsync(string name)
    {
        _profile.State.Name = name;
        await _profile.WriteStateAsync();
    }
}

Modes d’échec pour les opérations de persistance

Modes d’échec pour les opérations de lecture

Les échecs retournés par le fournisseur de stockage lors de la lecture initiale des données d’état pour ce grain particulier échouent à l’opération d’activation de ce grain. Dans ce cas, il n’y aura aucun appel à la méthode de rappel de cycle de vie OnActivateAsync() de ce grain. La demande d’origine au grain qui a provoqué l’activation est renvoyée en cas d’erreur à l’appelant, de la même façon que tout autre échec lors de l’activation du grain. Les échecs rencontrés par le fournisseur de stockage lors de la lecture des données d’état pour un grain particulier entraînent une exception de ReadStateAsync()Task. Le grain peut choisir de gérer ou d’ignorer l’exception Task, comme n’importe quel autre élément Task dans Orleans.

Toute tentative d’envoi d’un message à un grain qui n’a pas pu être chargé au démarrage du silo en raison d’une configuration de fournisseur de stockage manquante/incorrecte retournera l’erreur permanente BadProviderConfigException.

Modes d’échec pour les opérations d’écriture

Les échecs rencontrés par le fournisseur de stockage lors de l’écriture de données d’état pour un grain particulier entraînent une exception levée par l’élément TaskWriteStateAsync(). En règle générale, cela signifie que l’exception d’appel de grain sera renvoyée à l’appelant client, à condition que l’élément TaskWriteStateAsync() soit correctement chaîné dans l’élément Task de retour final pour cette méthode de grain. Toutefois, dans certains scénarios avancés, il est possible d’écrire le code du grain pour gérer spécifiquement ces erreurs d’écriture, tout comme il est possible de gérer tout autre élément Task ayant échoué.

Les grains qui exécutent la gestion des erreurs/le code de récupération doivent intercepter les exceptions/les éléments TaskWriteStateAsync() ayant échoué et ne pas les lever à nouveau, pour indiquer qu’ils ont correctement géré l’erreur d’écriture.

Recommandations

Utiliser la sérialisation JSON ou un autre format de sérialisation avec tolérance de version

Le code évolue et cela inclut également souvent des types de stockage. Pour prendre en charge ces modifications, un sérialiseur approprié doit être configuré. Pour la plupart des fournisseurs de stockage, une option UseJson ou similaire est disponible pour utiliser JSON comme format de sérialisation. Lors de l’évolution des contrats de données, veillez à ce que les données déjà stockées restent chargeables.

Utilisation de Grain<TState> pour ajouter du stockage à un grain

Important

L’utilisation de Grain<T> pour ajouter un stockage à un grain est considérée comme une fonctionnalité héritée : le stockage de grain doit être ajouté à l’aide de IPersistentState<T>, comme décrit précédemment.

Les classes de grain qui héritent de Grain<T> (où T est un type de données d’état spécifique à l’application qui doit être rendu persistant) voient leur état chargé automatiquement à partir du stockage spécifié.

De tels grains sont marqués avec un élément StorageProviderAttribute qui spécifie une instance nommée d’un fournisseur de stockage à utiliser pour lire/écrire les données d’état pour ce grain.

[StorageProvider(ProviderName="store1")]
public class MyGrain : Grain<MyGrainState>, /*...*/
{
  /*...*/
}

La classe de base Grain<T> a défini les méthodes suivantes pour les sous-classes à appeler :

protected virtual Task ReadStateAsync() { /*...*/ }
protected virtual Task WriteStateAsync() { /*...*/ }
protected virtual Task ClearStateAsync() { /*...*/ }

Le comportement de ces méthodes correspond à leurs équivalents sur IPersistentState<TState> définis précédemment.

Créer un fournisseur de stockage

Il existe deux parties aux API de persistance d’état : l’API exposée au grain via IPersistentState<T> ou Grain<T>, et l’API du fournisseur de stockage, centrée autour de IGrainStorage, l’interface que les fournisseurs de stockage doivent implémenter :

/// <summary>
/// Interface to be implemented for a storage able to read and write Orleans grain state data.
/// </summary>
public interface IGrainStorage
{
    /// <summary>Read data function for this storage instance.</summary>
    /// <param name="grainType">Type of this grain [fully qualified class name]</param>
    /// <param name="grainReference">Grain reference object for this grain.</param>
    /// <param name="grainState">State data object to be populated for this grain.</param>
    /// <returns>Completion promise for the Read operation on the specified grain.</returns>
    Task ReadStateAsync(
        string grainType, GrainReference grainReference, IGrainState grainState);

    /// <summary>Write data function for this storage instance.</summary>
    /// <param name="grainType">Type of this grain [fully qualified class name]</param>
    /// <param name="grainReference">Grain reference object for this grain.</param>
    /// <param name="grainState">State data object to be written for this grain.</param>
    /// <returns>Completion promise for the Write operation on the specified grain.</returns>
    Task WriteStateAsync(
        string grainType, GrainReference grainReference, IGrainState grainState);

    /// <summary>Delete / Clear data function for this storage instance.</summary>
    /// <param name="grainType">Type of this grain [fully qualified class name]</param>
    /// <param name="grainReference">Grain reference object for this grain.</param>
    /// <param name="grainState">Copy of last-known state data object for this grain.</param>
    /// <returns>Completion promise for the Delete operation on the specified grain.</returns>
    Task ClearStateAsync(
        string grainType, GrainReference grainReference, IGrainState grainState);
}

Créez un fournisseur de stockage personnalisé en implémentant cette interface et en inscrivant cette implémentation. Pour obtenir un exemple d’une implémentation de fournisseur de stockage existante, consultez AzureBlobGrainStorage.

Sémantique du fournisseur de stockage

Une valeur Etag opaque spécifique au fournisseur (string) peut être définie par un fournisseur de stockage dans le cadre des métadonnées d’état de grain remplies lors de la lecture de l’état. Certains fournisseurs peuvent choisir de laisser cela en tant que null s’ils n’utilisent pas de Etag.

Toute tentative d’exécution d’une opération d’écriture quand le fournisseur de stockage détecte une violation de contrainte Etagdoit provoquer l’échec de l’élément Task d’écriture avec l’erreur temporaire InconsistentStateException et l’encapsulation de l’exception de stockage sous-jacente.

public class InconsistentStateException : OrleansException
{
    public InconsistentStateException(
    string message,
    string storedEtag,
    string currentEtag,
    Exception storageException)
        : base(message, storageException)
    {
        StoredEtag = storedEtag;
        CurrentEtag = currentEtag;
    }

    public InconsistentStateException(
        string storedEtag,
        string currentEtag,
        Exception storageException)
        : this(storageException.Message, storedEtag, currentEtag, storageException)
    {
    }

    /// <summary>The Etag value currently held in persistent storage.</summary>
    public string StoredEtag { get; }

    /// <summary>The Etag value currently held in memory, and attempting to be updated.</summary>
    public string CurrentEtag { get; }
}

Toute autre condition d’échec attenant à une opération de stockage doit provoquer l’échec de l’élément Task renvoyé avec une exception indiquant le problème de stockage sous-jacent. Dans de nombreux cas, cette exception peut être renvoyée à l’appelant qui a déclenché l’opération de stockage en appelant une méthode sur le grain. Il est important de déterminer si l’appelant pourra ou non désérialiser cette exception. Par exemple, le client n’a peut-être pas chargé la bibliothèque de persistance spécifique contenant le type d’exception. Pour cette raison, il est conseillé de convertir les exceptions en exceptions pouvant être propagées en retour à l’appelant.

Mappage des données

Les fournisseurs de stockage individuels doivent décider de la meilleure façon de stocker l’état du grain : un objet blob (divers formats/formes sérialisées) ou une colonne par champ sont les choix évidents.

Inscrire un fournisseur de stockage

Le runtime Orleans résout un fournisseur de stockage à partir du fournisseur de services (IServiceProvider) quand un grain est créé. Le runtime résout une instance de IGrainStorage. Si le fournisseur de stockage est nommé, par exemple via l’attribut [PersistentState(stateName, storageName)], une instance nommée de IGrainStorage sera résolue.

Pour inscrire une instance nommée de IGrainStorage, utilisez la méthode d’extension AddSingletonNamedService suivant l’exemple du fournisseur AzureTableGrainStorage ici.