Persistencia de grano

Los granos pueden tener asociados varios objetos de datos persistentes con nombre. Estos objetos de estado se cargan desde el almacenamiento durante la activación del grano para que estén disponibles durante las solicitudes. La persistencia de grano usa un modelo de complemento extensible para que se puedan usar proveedores de almacenamiento para cualquier base de datos. Este modelo de persistencia está diseñado para ser sencillo y no está pensado para abarcar todos los patrones de acceso a datos. Los granos también pueden acceder a las bases de datos directamente, sin usar el modelo de persistencia de grano.

En el diagrama anterior, UserGrain tiene un estado Profile y un estado Cart, cada uno de los cuales se guarda en un sistema de almacenamiento independiente.

Objetivos

  1. Varios objetos de datos persistentes con nombre por grano.
  2. Varios proveedores de almacenamiento configurados, cada uno de los cuales puede tener una configuración diferente y estar respaldado por un sistema de almacenamiento diferente.
  3. La comunidad puede desarrollar y publicar proveedores de almacenamiento.
  4. Los proveedores de almacenamiento tienen un control total sobre cómo almacenan los datos de estado del grano en la memoria auxiliar persistente. Como consecuencia, Orleans no proporciona una solución completa de almacenamiento de ORM, sino que permite a los proveedores de almacenamiento personalizados admitir requisitos específicos de ORM según sea necesario.

Paquetes

Se pueden encontrar proveedores de almacenamiento de granos de Orleans en NuGet. Entre los paquetes que se mantienen oficialmente se incluyen los siguientes:

API

Los granos interactúan con su estado persistente mediante IPersistentState<TState>, donde TState es el tipo de estado serializable:

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

Las instancias de IPersistentState<TState> se insertan en el grano como parámetros de constructor. Estos parámetros se pueden anotar con un atributo PersistentStateAttribute para identificar el nombre del estado que se inserta y el nombre del proveedor de almacenamiento que lo proporciona. Para demostrarlo, en el ejemplo siguiente se insertan dos estados con nombre en el constructor 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;
    }
}

Diferentes tipos de grano pueden usar proveedores de almacenamiento configurados diferentes, incluso si ambos son del mismo tipo; por ejemplo, dos instancias diferentes de proveedor de Azure Table Storage, conectadas a cuentas diferentes de Azure Storage.

Lectura del estado

El estado del grano se leerá automáticamente cuando se active el grano, pero los granos son responsables de desencadenar explícitamente la escritura para cualquier estado de grano cambiado cuando sea necesario.

Si un grano quiere volver a leer explícitamente el estado más reciente de este grano desde la memoria auxiliar, el grano debe llamar al método ReadStateAsync. Esto volverá a cargar el estado del grano desde el almacén persistente mediante el proveedor de almacenamiento, y la copia en memoria anterior del estado del grano se sobrescribirá y se reemplazará cuando se complete el objeto Task de ReadStateAsync().

Para acceder al valor del estado, se usa la propiedad State. Por ejemplo, el método siguiente accede al estado del perfil declarado en el código anterior:

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

No es necesario llamar a ReadStateAsync() durante el funcionamiento normal, ya que el estado se carga automáticamente durante la activación. Aun así, se puede usar ReadStateAsync() para actualizar el estado que se modifica externamente.

Consulte la sección Modos de error a continuación para obtener más información sobre los mecanismos de control de errores.

Escritura del estado

El estado se puede modificar mediante la propiedad State. El estado modificado no se conserva automáticamente. En su lugar, el desarrollador decide cuándo se conserva el estado mediante una llamada al método WriteStateAsync. Por ejemplo, el método siguiente actualiza una propiedad en State y conserva el estado actualizado:

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

Conceptualmente, el runtime de Orleans realizará una copia en profundidad del objeto de datos de estado del grano para su uso durante cualquier operación de escritura. En segundo plano, el tiempo de ejecución podría usar reglas de optimización y heurística para evitar realizar una parte o la totalidad de la copia en profundidad en algunas circunstancias, siempre y cuando se conserve la semántica de aislamiento lógico esperada.

Consulte la sección Modos de error a continuación para obtener más información sobre los mecanismos de control de errores.

Borrado del estado

El método ClearStateAsync borra el estado del grano en el almacenamiento. En función del proveedor, esta operación podría eliminar opcionalmente el estado del grano por completo.

Introducción

Para que un grano pueda usar la persistencia, es necesario configurar un proveedor de almacenamiento en el silo.

En primer lugar, configure proveedores de almacenamiento, uno para el estado Profile y otro para el estado Cart:

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();

Ahora que se ha configurado un proveedor de almacenamiento denominado "profileStore", podemos acceder a este proveedor desde un grano.

El estado persistente se puede agregar a un grano de dos formas principales:

  1. Mediante la inserción de IPersistentState<TState> en el constructor del grano.
  2. Mediante la herencia de Grain<TGrainState>.

La forma recomendada de agregar almacenamiento a un grano consiste en insertar IPersistentState<TState> en el constructor del grano con un atributo [PersistentState("stateName", "providerName")] asociado. Puede obtener más información sobre Grain<TState> más adelante. Esto todavía se admite, pero se considera un enfoque heredado.

Declare una clase para contener el estado del grano:

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

    public Date DateOfBirth
}

Inserte IPersistentState<ProfileState> en el constructor del grano:

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

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

Nota

El estado del perfil no se cargará en el momento en el que se inserta en el constructor, por lo que acceder a él no es una acción válida en ese momento. El estado se cargará antes de que se llame a OnActivateAsync.

Ahora que el grano tiene un estado persistente, es posible agregar métodos para leer y escribir el estado:

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();
    }
}

Modos de error para operaciones de persistencia

Modos de error para operaciones de lectura

Los errores que devuelve el proveedor de almacenamiento durante la lectura inicial de los datos de estado para ese grano determinado producirán un error en la operación de activación del grano. En este caso, no habrá ninguna llamada al método de devolución de llamada del ciclo de vida OnActivateAsync() de ese grano. La solicitud original al grano que provocó la activación se devolverá al autor de la llamada, de la misma manera que cualquier otro error durante la activación del grano. Los errores que detecte el proveedor de almacenamiento al leer los datos de estado de un grano determinado darán lugar a una excepción de ReadStateAsync()Task. El grano puede optar por controlar o ignorar la excepción de Task, al igual que cualquier otro objeto Task en Orleans.

Cualquier intento de enviar un mensaje a un grano que no se pudo cargar durante el tiempo de inicio del silo debido a que falta una configuración del proveedor de almacenamiento o es incorrecta devolverá el error permanente BadProviderConfigException.

Modos de error para operaciones de escritura

Los errores que detecte el proveedor de almacenamiento al escribir los datos de estado de un grano determinado darán lugar a una excepción generada por WriteStateAsync()Task. En general, esto significa que la excepción de llamada de grano se devolverá al autor de la llamada del cliente, siempre y cuando WriteStateAsync()Task se haya encadenado correctamente en el objeto Task devuelto finalmente para este método de grano. Aun así, es posible en ciertos escenarios avanzados escribir código de grano para controlar específicamente estos errores de escritura, del mismo modo que se pueden controlar otros objetos Task con errores.

Los granos que ejecutan código de control de errores o de recuperación deben capturar los objetos WriteStateAsync()Task con excepciones o errores y no volver a lanzarlos, para indicar que han controlado correctamente el error de escritura.

Recomendaciones

Uso de la serialización de JSON u otro formato de serialización tolerante a versiones

El código evoluciona y esto suele incluir también los tipos de almacenamiento. Para dar cabida a estos cambios, se debe configurar un serializador adecuado. Para la mayoría de los proveedores de almacenamiento, hay disponible una opción UseJson o similar para usar JSON como formato de serialización. Asegúrese de que, al evolucionar los contratos de datos, los datos ya almacenados seguirán pudiendo cargarse.

Uso de Grain<TState> para agregar almacenamiento a un grano

Importante

El uso de Grain<T> para agregar almacenamiento a un grano se considera una funcionalidad heredada. Debe agregarse almacenamiento de granos mediante IPersistentState<T> como se describió anteriormente.

El estado de las clases de grano que heredan de Grain<T> (donde T es un tipo de datos de estado específico de la aplicación que debe conservarse) se cargará automáticamente desde el almacenamiento especificado.

Estos granos se marcan con un atributo StorageProviderAttribute que especifica una instancia con nombre de un proveedor de almacenamiento que se usará para leer y escribir los datos de estado de este grano.

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

La clase base Grain<T> ha definido los métodos siguientes para las subclases a las que se va a llamar:

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

El comportamiento de estos métodos se corresponde con el de sus homólogos en IPersistentState<TState> definidos anteriormente.

Creación de un proveedor de almacenamiento

Hay dos partes en las API de persistencia de estado: la API expuesta al grano mediante IPersistentState<T> o Grain<T>, y la API del proveedor de almacenamiento, que se centra en IGrainStorage, la interfaz que los proveedores de almacenamiento deben implementar:

/// <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);
}

Para crear un proveedor de almacenamiento personalizado, implemente esta interfaz y registre esta implementación. Para obtener un ejemplo de una implementación de proveedor de almacenamiento existente, consulte AzureBlobGrainStorage.

Semántica del proveedor de almacenamiento

Un proveedor de almacenamiento podría establecer un valor Etag opaco específico del proveedor (string) como parte de los metadatos de estado del grano que se rellenaron cuando se leyó el estado. Algunos proveedores pueden optar por dejarlo como null si no usan objetos Etag.

Cualquier intento de realizar una operación de escritura cuando el proveedor de almacenamiento detecta una infracción de restricción de Etagdebe provocar un error de escritura de Task con un error transitorio InconsistentStateException y ajustar la excepción de almacenamiento subyacente.

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; }
}

Cualquier otra condición de error de una operación de almacenamiento debe hacer que el objeto Task devuelto se interrumpa con una excepción que indica el problema de almacenamiento subyacente. En muchos casos, esta excepción se puede devolver al autor de la llamada que desencadenó la operación de almacenamiento mediante una llamada a un método del grano. Es importante tener en cuenta si el autor de la llamada podrá deserializar esta excepción. Por ejemplo, es posible que el cliente no haya cargado la biblioteca de persistencia específica que contiene el tipo de excepción. Por este motivo, es aconsejable convertir las excepciones en otras que se puedan propagar de nuevo al autor de la llamada.

Asignación de datos

Cada proveedor de almacenamiento debe decidir cómo se almacena mejor el estado del grano. Las opciones obvias son en un blob (varios formatos o formularios serializados) o en una columna por campo.

Registro de un proveedor de almacenamiento

El runtime de Orleans resolverá un proveedor de almacenamiento del proveedor de servicios (IServiceProvider) cuando se cree un grano. El tiempo de ejecución resolverá una instancia de IGrainStorage. Si se asigna un nombre al proveedor de almacenamiento, por ejemplo, mediante el atributo [PersistentState(stateName, storageName)], se resolverá una instancia con nombre de IGrainStorage.

Para registrar una instancia con nombre de IGrainStorage, use el método de extensión AddSingletonNamedService como se indica en el ejemplo de este proveedor AzureTableGrainStorage.