Condividi tramite


Persistenza dei grani

Ai grani possono essere associati più oggetti dati persistenti denominati. Questi oggetti di stato vengono caricati dall'archiviazione durante l'attivazione granulare, in modo che siano disponibili durante le richieste. La persistenza granulare usa un modello di plug-in estendibile, consentendo di usare provider di archiviazione per qualsiasi database. Questo modello di persistenza è progettato per semplicità e non è progettato per coprire tutti i modelli di accesso ai dati. I grani possono anche accedere direttamente ai database senza usare il modello di persistenza granulare.

Diagramma di persistenza granulare

Nel diagramma precedente UserGrain ha uno stato profilo e uno stato carrello , ognuno archiviato in un sistema di archiviazione separato.

Obiettivi

  1. Supporta più oggetti dati persistenti denominati per ogni grain.
  2. Consentire più provider di archiviazione configurati, ognuno con una configurazione diversa e supportato da un sistema di archiviazione diverso.
  3. Abilitare la community a sviluppare e pubblicare fornitori di archiviazione.
  4. Fornire ai provider di archiviazione il controllo completo su come archiviare i dati sullo stato delle istanze nella memoria persistente. Corollario: Orleans non offre una soluzione di archiviazione ORM completa, ma consente ai provider di archiviazione personalizzati di supportare requisiti ORM specifici secondo necessità.

Pacchetti

È possibile trovare fornitori di servizi di archiviazione dei dati su Orleans. I pacchetti gestiti ufficialmente includono:

API (Interfaccia di Programmazione delle Applicazioni)

I grani interagiscono con il relativo stato persistente usando IPersistentState<TState>, dove TState è il tipo di stato serializzabile:

public interface IPersistentState<TState> : IStorage<TState>
{
}

public interface IStorage<TState> : IStorage
{
    TState State { get; set; }
}

public interface IStorage
{
    string Etag { get; }

    bool RecordExists { get; }

    Task ClearStateAsync();

    Task WriteStateAsync();

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

    string Etag { get; }

    Task ClearStateAsync();

    Task WriteStateAsync();

    Task ReadStateAsync();
}

Orleans inserisce istanze di IPersistentState<TState> nel grain come parametri per il costruttore. È possibile annotare questi parametri con un PersistentStateAttribute attributo per identificare il nome dello stato inserito e il nome del provider di archiviazione che lo fornisce. Nell'esempio seguente viene illustrato inserendo due stati denominati nel UserGrain costruttore:

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

Tipi di grani diversi possono utilizzare diversi provider di archiviazione configurati, anche se entrambi sono dello stesso tipo (ad esempio, due diverse istanze del provider di Azure Table Storage connesse a diversi account di Azure Storage).

Stato di lettura

Lo stato del grano viene letto automaticamente quando il grano si attiva, ma i grani sono responsabili di attivare esplicitamente la scrittura per qualsiasi stato del grano modificato quando necessario.

Se un grain desidera rileggere in modo esplicito il suo stato più recente dalla memoria di backup, deve chiamare il metodo ReadStateAsync. Questo ricarica lo stato granulare dall'archivio permanente tramite il provider di archiviazione. La precedente copia in memoria dello stato del grano viene sovrascritta e sostituita al completamento di Task da ReadStateAsync().

Accedere al valore dello stato usando la State proprietà . Ad esempio, il metodo seguente accede allo stato del profilo dichiarato nel codice precedente:

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

Non è necessario chiamare ReadStateAsync() durante il normale funzionamento. Orleans Carica automaticamente lo stato durante l'attivazione. Tuttavia, è possibile usare ReadStateAsync() per aggiornare lo stato modificato esternamente.

Per informazioni dettagliate sui meccanismi di gestione degli errori, vedere la sezione Modalità di errore di seguito.

Scrivere stato

È possibile modificare lo stato tramite la State proprietà . Lo stato modificato non viene salvato automaticamente in modo permanente. Si decide invece quando rendere persistente lo stato chiamando il WriteStateAsync metodo . Ad esempio, il metodo seguente aggiorna una proprietà in State e mantiene lo stato aggiornato:

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

Concettualmente, il Orleans runtime esegue una copia completa dell'oggetto dati dello stato granulare per il relativo utilizzo durante qualsiasi operazione di scrittura. Sotto le quinte, il runtime potrebbe usare regole di ottimizzazione e euristica per evitare di eseguire alcune o tutte le copie complete in determinate circostanze, a condizione che la semantica di isolamento logico prevista venga mantenuta.

Per informazioni dettagliate sui meccanismi di gestione degli errori, vedere la sezione Modalità di errore di seguito.

Cancellare lo stato

Il metodo ClearStateAsync elimina lo stato del grano nell'archiviazione. A seconda del provider, questa operazione potrebbe facoltativamente eliminare completamente lo stato di granularità.

Inizia subito

Prima che un grain possa usare la persistenza, è necessario configurare un provider di archiviazione nel silo.

Prima di tutto, configurare i provider di archiviazione, uno per lo stato del profilo e uno per lo stato del carrello:

using IHost host = new HostBuilder()
    .UseOrleans(siloBuilder =>
    {
        siloBuilder.AddAzureTableGrainStorage(
            name: "profileStore",
            configureOptions: options =>
            {
                // Configure the storage connection key
                options.ConfigureTableServiceClient(
                    "DefaultEndpointsProtocol=https;AccountName=data1;AccountKey=SOMETHING1");
            })
            .AddAzureBlobGrainStorage(
                name: "cartStore",
                configureOptions: options =>
                {
                    // Configure the storage connection key
                    options.ConfigureTableServiceClient(
                        "DefaultEndpointsProtocol=https;AccountName=data2;AccountKey=SOMETHING2");
                });
    })
    .Build();
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();

Importante

Microsoft consiglia di usare il flusso di autenticazione più sicuro disponibile. Se ci si connette ad Azure SQL, le Identità gestite per le risorse Azure sono il metodo di autenticazione consigliato.

Ora che è stato configurato un provider di archiviazione denominato "profileStore", è possibile accedere a questo provider da una granularità.

È possibile aggiungere uno stato permanente a un livello di granularità in due modi principali:

  1. Inserendo IPersistentState<TState> nel costruttore del grain.
  2. Ereditando da Grain<TGrainState>.

Il modo consigliato per aggiungere archiviazione a un grano è iniettando IPersistentState<TState> nel costruttore del grano con un attributo associato [PersistentState("stateName", "providerName")]. Per informazioni dettagliate su Grain<TState>, vedere Uso di Grain<TState> per aggiungere archiviazione a un grano di seguito. L'uso Grain<TState> è ancora supportato, ma considerato un approccio legacy.

Dichiarare una classe per mantenere lo stato della granularità:

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

    public Date DateOfBirth { get; set; }
}

Inserire IPersistentState<ProfileState> nel costruttore del grano:

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

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

Importante

Lo stato del profilo non verrà caricato al momento dell'inserimento nel costruttore, quindi l'accesso non è valido in quel momento. Lo stato verrà caricato prima che OnActivateAsync venga chiamato.

Ora che la granularità ha uno stato persistente, è possibile aggiungere metodi per leggere e scrivere lo stato:

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

Modalità di errore per le operazioni di persistenza

Modalità di errore per le operazioni di lettura

Gli errori restituiti dal provider di archiviazione durante la lettura iniziale dei dati sullo stato per un particolare granularità non riescono a eseguire l'operazione di attivazione per tale granularità. In questi casi, non verrà eseguita alcuna chiamata al metodo di callback del ciclo di vita di OnActivateAsync. La richiesta originale al grain che ha causato i guasti di attivazione viene restituita al chiamante indietro, proprio come qualsiasi altro fallimento durante l'attivazione del grain. Gli errori riscontrati dal provider di archiviazione durante la lettura dei dati sullo stato per un particolare grano generano un'eccezione da parte di ReadStateAsyncTask. Il grano può scegliere di maneggiare o ignorare l'eccezione Task, esattamente come qualsiasi altra Task in Orleans.

Qualsiasi tentativo di inviare un messaggio a un grano che non è riuscito a caricarsi all'avvio del silo per una configurazione del provider di archiviazione mancante o non valida restituisce l'errore permanente BadProviderConfigException.

Modalità di errore per le operazioni di scrittura

Gli errori riscontrati dal provider di archiviazione durante la scrittura dei dati di stato per un particolare grano generano un'eccezione WriteStateAsync()Task. In genere, ciò significa che l'eccezione chiamata grain viene rilanciata al chiamante client, a condizione che l'oggetto WriteStateAsync()Task sia concatenato correttamente nella restituzione Task finale per questo metodo grain. In alcuni scenari avanzati, tuttavia, è possibile scrivere codice granulare per gestire in modo specifico tali errori di scrittura, proprio come la gestione di qualsiasi altro errore Task.

I grain che eseguono la gestione degli errori o il codice di ripristino devono intercettare eccezioni o errori in errore e non rigenerarli, segnalando che hanno gestito correttamente l'errore di scrittura.

Consigli

Usare la serializzazione JSON o un altro formato di serializzazione a tolleranza di versione

Il codice si evolve e spesso include tipi di archiviazione. Per supportare queste modifiche, configurare un serializzatore appropriato. Per la maggior parte dei provider di archiviazione, è disponibile un'opzione UseJson o un formato simile per l'uso di JSON come formato di serializzazione. Assicurarsi che durante l'evoluzione dei contratti dati, i dati già archiviati possano comunque essere caricati.

Usare Grain<TState> per aggiungere spazio di archiviazione a un grain

Importante

L'uso di Grain<T> per aggiungere spazio di archiviazione a un grain è considerato una funzionalità legacy. Aggiungere lo stoccaggio del grano usando IPersistentState<T> come descritto in precedenza.

Le classi granulari che ereditano da Grain<T> (dove T è un tipo di dati di stato specifico dell'applicazione che richiede la persistenza) hanno il relativo stato caricato automaticamente dalla risorsa di archiviazione specificata.

Contrassegnare tali grani con una StorageProviderAttribute che specifica un'istanza denominata di un provider di archiviazione da usare per la lettura e scrittura dello stato per questo grano.

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

La Grain<T> classe base definisce i metodi seguenti per le sottoclassi da chiamare:

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

Il comportamento di questi metodi corrisponde alle loro controparti su IPersistentState<TState> definiti in precedenza.

Creare un provider di archiviazione

Esistono due parti delle API di persistenza dello stato: l'API esposta alla granularità tramite IPersistentState<T> o Grain<T>e l'API del provider di archiviazione, centrata intorno IGrainStorage, i provider di archiviazione dell'interfaccia devono implementare:

/// <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="stateName">Name of the state for this grain</param>
    /// <param name="grainId">Grain ID</param>
    /// <param name="grainState">State data object to be populated for this grain.</param>
    /// <typeparam name="T">The grain state type.</typeparam>
    /// <returns>Completion promise for the Read operation on the specified grain.</returns>
    Task ReadStateAsync<T>(
        string stateName, GrainId grainId, IGrainState<T> grainState);

    /// <summary>Write data function for this storage instance.</summary>
    /// <param name="stateName">Name of the state for this grain</param>
    /// <param name="grainId">Grain ID</param>
    /// <param name="grainState">State data object to be written for this grain.</param>
    /// <typeparam name="T">The grain state type.</typeparam>
    /// <returns>Completion promise for the Write operation on the specified grain.</returns>
    Task WriteStateAsync<T>(
        string stateName, GrainId grainId, IGrainState<T> grainState);

    /// <summary>Delete / Clear data function for this storage instance.</summary>
    /// <param name="stateName">Name of the state for this grain</param>
    /// <param name="grainId">Grain ID</param>
    /// <param name="grainState">Copy of last-known state data object for this grain.</param>
    /// <typeparam name="T">The grain state type.</typeparam>
    /// <returns>Completion promise for the Delete operation on the specified grain.</returns>
    Task ClearStateAsync<T>(
        string stateName, GrainId grainId, IGrainState<T> grainState);
}
/// <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);
}

Creare un provider di archiviazione personalizzato implementando questa interfaccia e registrando tale implementazione . Per un esempio di implementazione di un provider di archiviazione esistente, vedere AzureBlobGrainStorage.

Semantica del provider di archiviazione

Un valore opaco specifico del provider (Etag) (string) può essere impostato da un provider di archiviazione come parte dei metadati dello stato del "grain", popolati quando lo stato è stato letto. Alcuni provider possono scegliere di lasciarlo come null se non usano Etag.

Qualsiasi tentativo di eseguire un'operazione di scrittura quando il provider di archiviazione rileva una Etag violazione del vincolo dovrebbe causare che la scrittura Task vada in errore con un errore InconsistentStateException temporaneo, avvolgendo l'eccezione di archiviazione sottostante.

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

Qualsiasi altra condizione di errore di un'operazione di archiviazione deve causare l'interruzione dell'oggetto restituito Task con un'eccezione che indica il problema di archiviazione sottostante. In molti casi, questa eccezione potrebbe essere generata nuovamente al chiamante che ha attivato l'operazione di archiviazione chiamando un metodo sulla granularità. È importante considerare se il chiamante può deserializzare questa eccezione. Ad esempio, il client potrebbe non aver caricato la libreria di persistenza specifica contenente il tipo di eccezione. Per questo motivo, è consigliabile convertire le eccezioni in eccezioni che possono propagarsi al chiamante.

Mappatura dei dati

I singoli provider di archiviazione devono decidere il modo migliore per archiviare lo stato di granularità: BLOB (vari formati/moduli serializzati) o colonne per campo sono scelte ovvie.

Registrare un provider di archiviazione

Il runtime Orleans risolve un provider di archiviazione dal provider di servizi IServiceProvider quando viene creato un granello. Il runtime risolve un'istanza di IGrainStorage. Se il provider di archiviazione è denominato (ad esempio, tramite l'attributo [PersistentState(stateName, storageName)]), viene risolta un'istanza denominata di IGrainStorage.

Per registrare un'istanza denominata di IGrainStorage, usare il AddSingletonNamedService metodo di estensione, seguendo l'esempio del provider AzureTableGrainStorage qui.