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
- Plusieurs objets de données persistants nommés par grain.
- Plusieurs fournisseurs de stockage configurés, chacun pouvant avoir une configuration différente et être soutenu par un système de stockage différent.
- Les fournisseurs de stockage peuvent être développés et publiés par la communauté.
- 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 :
- Microsoft.Orleans.Persistence.AdoNet est destiné aux bases de données SQL et à d’autres systèmes de stockage pris en charge par ADO.NET. Pour plus d’informations, consultez Persistance des grains ADO.NET.
- Microsoft.Orleans.Persistence.AzureStorage est destiné au Stockage Azure, notamment au Stockage Blob Azure, Stockage Table Azure et Azure CosmosDB, via l’API Stockage Table Azure. Pour plus d’informations, consultez Persistance des grains de Stockage Azure.
- Microsoft.Orleans.Persistence.DynamoDB est destiné à Amazon DynamoDB. Pour plus d’informations, consultez Persistance des grains Amazon DynamoDB.
API
Les grains interagissent avec leur état persistant avec IPersistentState<TState> où 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 :
- En injectant
IPersistentState<TState>
dans le constructeur du grain. - 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 Task
WriteStateAsync()
. 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 Task
WriteStateAsync()
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 Task
WriteStateAsync()
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 Etag
doit 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.