Persistência de granularidade

A granularidade pode ter vários objetos de dados persistentes nomeados associados. Esses objetos de estado são carregados do armazenamento durante a ativação de granularidade para que estejam disponíveis durante as solicitações. A persistência de granularidade usa um modelo de plug-in extensível para que os provedores de armazenamento de qualquer banco de dados possam ser usados. Esse modelo de persistência foi projetado visando a simplicidade e não se destina a abranger todos os padrões de acesso a dados. A granularidade também pode acessar bancos de dados diretamente, sem usar o modelo de persistência de granularidade.

No diagrama acima, UserGrain tem um estado Profile e um estado Cart, cada um armazenado em um sistema de armazenamento separado.

Metas

  1. Vários objetos de dados persistentes nomeados por granularidade.
  2. Vários provedores de armazenamento configurados, sendo que cada um pode ter uma configuração diferente e ter o suporte de um sistema de armazenamento diferente.
  3. Os provedores de armazenamento podem ser desenvolvidos e publicados pela comunidade.
  4. Os provedores de armazenamento têm controle total sobre como armazenam os dados de estado de granularidade no repositório de backup persistente. Resultado: o Orleans não fornece uma solução abrangente de armazenamento de ORM, mas permite que os provedores de armazenamento personalizados deem suporte a requisitos de ORM específicos conforme e quando necessário.

Pacotes

Os provedores de armazenamento de granularidade do Orleans podem ser encontrados no NuGet. Os pacotes oficialmente mantidos incluem:

API

A granularidade interage com o estado persistente usando IPersistentState<TState>, em que TState é o tipo de estado serializável:

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

As instâncias de IPersistentState<TState> são injetadas na granularidade como parâmetros de construtor. Esses parâmetros podem ser anotados com um atributo PersistentStateAttribute para identificar o nome do estado que está sendo injetado e o nome do provedor de armazenamento que o fornece. O seguinte exemplo demonstra isso injetando dois estados nomeados no construtor 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 granularidade podem usar diferentes provedores de armazenamento configurados, mesmo que ambos sejam do mesmo tipo. Por exemplo, duas instâncias diferentes do provedor do Armazenamento de Tabelas do Azure, conectadas a contas diferentes do Armazenamento do Azure.

Estado de leitura

O estado de granularidade será lido automaticamente quando a granularidade for ativada, mas a granularidade é responsável por disparar explicitamente a gravação para qualquer estado de granularidade alterado quando necessário.

Se uma granularidade quiser reler explicitamente o estado mais recente dessa granularidade do repositório de backup, a granularidade deverá chamar o método ReadStateAsync. Isso recarregará o estado de granularidade do repositório persistente por meio do provedor de armazenamento, e a cópia anterior na memória do estado de granularidade será substituída quando a Task de ReadStateAsync() for concluída.

O valor do estado é acessado por meio da propriedade State. Por exemplo, o seguinte método acessa o estado de perfil declarado no código acima:

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

Não é necessário chamar ReadStateAsync() durante a operação normal. O estado é carregado automaticamente durante a ativação. No entanto, ReadStateAsync() pode ser usado para atualizar o estado que é modificado externamente.

Confira a seção Modos de falha abaixo para obter detalhes dos mecanismos de tratamento de erro.

Estado de gravação

O estado pode ser modificado por meio da propriedade State. O estado modificado não é persistido automaticamente. Em vez disso, o desenvolvedor decide quando persistir o estado chamando o método WriteStateAsync. Por exemplo, o seguinte método atualiza uma propriedade em State e persiste o estado atualizado:

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

Conceitualmente, o Runtime do Orleans fará uma cópia profunda do objeto de dados do estado de granularidade para o uso dele durante qualquer operação de gravação. Nos bastidores, o runtime poderá usar regras de otimização e heurística para evitar a execução de algumas ou todas as cópias profundas em algumas circunstâncias, desde que a semântica de isolamento lógico esperada seja preservada.

Confira a seção Modos de falha abaixo para obter detalhes dos mecanismos de tratamento de erro.

Estado de limpeza

O método ClearStateAsync limpa o estado de granularidade no armazenamento. Dependendo do provedor, essa operação pode, opcionalmente, excluir por completo o estado de granularidade.

Introdução

Antes que uma granularidade possa usar a persistência, um provedor de armazenamento precisa ser configurado no silo.

Primeiro, configure provedores de armazenamento, um para o estado do perfil e outro para o estado do carrinho:

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

Agora que um provedor de armazenamento foi configurado com o nome "profileStore", podemos acessar esse provedor por meio de uma granularidade.

O estado persistente pode ser Adição de a uma granularidade de duas maneiras principais:

  1. Injetando IPersistentState<TState> no construtor da granularidade.
  2. Herdando de Grain<TGrainState>.

A maneira recomendada de adicionar armazenamento a uma granularidade é injetando IPersistentState<TState> no construtor da granularidade com um atributo [PersistentState("stateName", "providerName")] associado. Para obter detalhes sobre Grain<TState>, confira abaixo. Ainda há suporte para isso, mas ela é considerada uma abordagem herdada.

Declare uma classe para manter o estado da granularidade:

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

    public Date DateOfBirth
}

Injete IPersistentState<ProfileState> no construtor da granularidade:

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

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

Observação

O estado do perfil não será carregado no momento em que ele for injetado no construtor, ou seja, acessá-lo é inválido nesse momento. O estado será carregado antes de OnActivateAsync ser chamado.

Agora que a granularidade tem um estado persistente, podemos adicionar métodos para ler e gravar o 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 falha para operações de persistência

Modos de falha para operações de leitura

As falhas retornadas pelo provedor de armazenamento durante a leitura inicial dos dados de estado dessa granularidade específica falharão na operação de ativação dessa granularidade. Nesse caso, não haverá nenhuma chamada ao método de retorno de chamada do ciclo de vida OnActivateAsync() da granularidade. A solicitação original para a granularidade que causou a ativação será retornada ao chamador, da mesma forma que qualquer outra falha durante a ativação de granularidade. As falhas encontradas pelo provedor de armazenamento ao ler os dados de estado de determinada granularidade resultarão em uma exceção de ReadStateAsync()Task. A granularidade pode optar por tratar ou ignorar a exceção Task, assim como qualquer outra Task no Orleans.

Qualquer tentativa de envio de uma mensagem para uma granularidade que não foi carregada no momento da inicialização do silo devido a uma configuração ausente/inválida do provedor de armazenamento retornará o erro permanente BadProviderConfigException.

Modos de falha para operações de gravação

As falhas encontradas pelo provedor de armazenamento ao gravar os dados de estado de determinada granularidade resultarão em uma exceção gerada por WriteStateAsync()Task. Normalmente, isso significa que a exceção de chamada de granularidade será gerada novamente para o chamador do cliente, desde que WriteStateAsync()Task seja encadeada corretamente no Task final retornada desse método de granularidade. No entanto, em determinados cenários avançados, é possível escrever um código de granularidade para tratar especificamente esses erros de gravação, assim como eles podem lidar com qualquer outra Task com falha.

A granularidade que executa o tratamento de erro/o código de recuperação precisa capturar exceções/WriteStateAsync()Tasks com falha e não gerá-las novamente, para significar que ela lidou com o erro de gravação com sucesso.

Recomendações

Usar a serialização JSON ou outro formato de serialização tolerante à versões

O código evolui, e isso também inclui tipos de armazenamento. Para acomodar essas alterações, um serializador apropriado deve ser configurado. Para a maioria dos provedores de armazenamento, uma opção UseJson ou similar está disponível para usar o JSON como um formato de serialização. Verifique se, ao evoluir os contratos de dados, os dados já armazenados ainda poderão ser carregados.

Usando Grain<TState> para adicionar armazenamento a uma granularidade

Importante

O uso de Grain<T> para adicionar armazenamento a uma granularidade é considerado uma funcionalidade herdada: o armazenamento de granularidade deve ser adicionado por meio de IPersistentState<T>, como descrito anteriormente.

As classes de granularidade que herdam de Grain<T> (em que T é um tipo de dados de estado específico do aplicativo que precisa ser persistido) terão o estado carregado automaticamente do armazenamento especificado.

Essa granularidade é marcada com um StorageProviderAttribute que especifica uma instância nomeada de um provedor de armazenamento a ser usada para ler/gravar os dados de estado dessa granularidade.

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

A classe base Grain<T> definiu os seguintes métodos a serem chamados para as subclasses:

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

O comportamento desses métodos corresponde aos equivalentes em IPersistentState<TState> definidos anteriormente.

Como adicionar um provedor de armazenamento

Há duas partes nas APIs de persistência de estado: a API exposta à granularidade por meio de IPersistentState<T> ou de Grain<T>, e a API do provedor de armazenamento, que é centralizada em torno de IGrainStorage, a interface que os provedores de armazenamento precisam 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);
}

Crie um provedor de armazenamento personalizado implementando essa interface e registrando essa implementação. Para ver um exemplo de implementação de um provedor de armazenamento existente, confira AzureBlobGrainStorage.

Semântica do provedor de armazenamento

Um valor Etag opaco específico do provedor (string) poderá ser definido por um provedor de armazenamento como parte dos metadados de estado de granularidade preenchidos quando o estado foi lido. Alguns provedores poderão optar por manter isso como null se não usarem Etags.

Qualquer tentativa de executar uma operação de gravação quando o provedor de armazenamento detectar uma violação de restrição Etagdeve causar uma falha da Task de gravação com o erro transitório InconsistentStateException e encapsular a exceção de armazenamento subjacente.

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

Todas as outras condições de falha de uma operação de armazenamento precisam fazer com que a Task retornada seja interrompida com uma exceção que indica o problema de armazenamento subjacente. Em muitos casos, essa exceção pode ser gerada novamente para o chamador que disparou a operação de armazenamento chamando um método na granularidade. É importante considerar se o chamador poderá desserializar essa exceção. Por exemplo, talvez o cliente não tenha carregado a biblioteca de persistência específica que contém o tipo de exceção. Por esse motivo, é aconselhável converter as exceções em exceções que possam ser propagadas novamente para o chamador.

Mapeamento de dados

Os provedores de armazenamento individuais devem decidir a melhor maneira de armazenar o estado de granularidade: blob (vários formatos/formas serializadas) ou coluna por campo são opções óbvias.

Registrar um provedor de armazenamento

O runtime do Orleans resolverá um provedor de armazenamento do provedor de serviços (IServiceProvider) quando uma granularidade for criada. O runtime resolverá uma instância de IGrainStorage. Se o provedor de armazenamento for nomeado, por exemplo, por meio do atributo [PersistentState(stateName, storageName)], uma instância nomeada de IGrainStorage será resolvida.

Para registrar uma instância nomeada de IGrainStorage, use o método de extensão AddSingletonNamedService seguindo o exemplo do provedor AzureTableGrainStorage aqui.