グレインの永続性

グレインには、それに関連付けた複数の名前付き永続データ オブジェクトを含めることができます。 これらの状態オブジェクトは、要求中に使用できるように、グレインのアクティブ化中にストレージから読み込まれます。 グレインの永続化では、拡張可能なプラグイン モデルが使用されるため、任意のデータベースの記憶域プロバイダーを使用できます。 この永続化モデルは、わかりやすくするために設計されており、すべてのデータ アクセス パターンをカバーするためのものではありません。 グレインは、グレイン永続化モデルを使用せずにデータベースに直接アクセスすることもできます。

上の図では、UserGrain にプロファイル状態とカート状態があり、それぞれが個別のストレージ システムに格納されています。

目標

  1. グレインごとの複数の名前付き永続データ オブジェクト。
  2. それぞれが異なる構成を持ち、異なるストレージ システムでサポートされる可能性がある、複数の構成済み記憶域プロバイダー。
  3. 記憶域プロバイダーは、コミュニティが開発し、公開できます。
  4. 記憶域プロバイダーは、永続的なバッキング ストアにグレイン状態データを格納する方法を完全に制御できます。 結果: Orleans は包括的な ORM ストレージ ソリューションを提供していませんが、代わりにカスタム記憶域プロバイダーが必要に応じて特定の ORM 要件をサポートできるようにします。

パッケージ

Orleans グレイン記憶域プロバイダーは NuGet で検索できます。 公式管理パッケージは次のとおりです。

API

グレインは、TState が以下のようにシリアル化可能な状態の種類である IPersistentState<TState> を使用して永続的な状態と対話します。

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

IPersistentState<TState> のインスタンスは、コンストラクター パラメーターとしてグレインに挿入されます。 これらのパラメーターには、PersistentStateAttribute 属性を使用して注釈を付け、挿入される状態の名前と、それを提供する記憶域プロバイダーの名前を識別できます。 次の例では、UserGrain コンストラクターに 2 つの名前付き状態を挿入することでこれを示しています。

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

グレインの種類が異なると、両方が同じ種類である場合でも、構成済みの記憶域プロバイダーが異なる場合があります。たとえば、2 つの異なる Azure Table Storage プロバイダー インスタンスが、異なる Azure Storage アカウントに接続されています。

状態を読み取る

グレインの状態は、グレインがアクティブ化されると自動的に読み取られますが、グレインは、必要に応じて変更されたグレイン状態の書き込みを明示的にトリガーする必要があります。

グレインがバッキング ストアからこのグレインの最新の状態を明示的に再読み取りする場合、グレインは ReadStateAsync メソッドを呼び出す必要があります。 これにより、記憶域プロバイダーを介して永続ストアからグレイン状態を再読み込みし、グレイン状態の以前のメモリ内コピーが上書きされ、ReadStateAsync() から Task が完了すると置き換えられます。

状態の値には、State プロパティを使用してアクセスします。 たとえば、次のメソッドは、上記のコードで宣言されたプロファイルの状態にアクセスします。

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

通常の操作中に ReadStateAsync() を呼び出す必要はありません。状態はアクティブ化中に自動的に読み込まれます。 ただし、外部で変更された状態を更新するために ReadStateAsync() を使用できます。

エラー処理メカニズムの詳細については、以下の「エラー モード」セクションを参照してください。

状態を書き込む

状態は、State プロパティを介して変更できます。 変更された状態は自動的には保持されません。 代わりに、開発者は WriteStateAsync メソッドを呼び出して状態を永続化するタイミングを決定します。 たとえば、次のメソッドは State でプロパティを更新し、更新された状態を保持します。

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

概念的には、Orleans ランタイムは、書き込み操作中に使用するためにグレイン状態データ オブジェクトのディープ コピーを取得します。 この場合、ランタイムは最適化ルールとヒューリスティックを使用して、予期される論理分離セマンティクスが保持されている場合に、状況によってはディープ コピーの一部またはすべてを実行しないようにすることができます

エラー処理メカニズムの詳細については、以下の「エラー モード」セクションを参照してください。

状態をクリアする

ClearStateAsync メソッドは、ストレージでグレインの状態オブジェクトをクリアします。 プロバイダーによっては、必要に応じて、この操作でグレインの状態が完全に削除される可能性があります。

開始

グレインで永続化を使用する前に、記憶域プロバイダーをサイロで構成する必要があります。

まず、記憶域プロバイダーを構成します。1 つはプロファイル状態用、1 つはカート状態用です。

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

記憶域プロバイダーが "profileStore" という名前で構成され、グレインからこのプロバイダーにアクセスできるようになりました。

永続的な状態は、主に次の 2 つの方法でグレインに追加できます。

  1. グレインのコンストラクターに IPersistentState<TState> を挿入します。
  2. Grain<TGrainState> から継承します。

グレインにストレージを追加するために推奨される方法は、関連する [PersistentState("stateName", "providerName")] 属性を使用して IPersistentState<TState> をグレインのコンストラクターに挿入することです。 Grain<TState> の詳細については、以下を参照してください。 これは引き続きサポートされていますが、従来のアプローチと見なされます。

グレインの状態を保持する以下のクラスを宣言します。

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

    public Date DateOfBirth
}

グレインのコンストラクターに IPersistentState<ProfileState> を挿入します。

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

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

注意

プロファイルの状態は、コンストラクターに挿入された時点では読み込まれないため、その時点でのアクセスは無効になります。 状態は、OnActivateAsync が呼び出される前に読み込まれます。

グレインの状態が永続的になったため、状態を読み書きするためのメソッドを追加できるようになりました。

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

永続化操作 向けのエラー モード

読み取り操作のエラー モード

その特定のグレイン向けの状態データの最初の読み取り中に記憶域プロバイダーによって返されたエラーは、そのグレインのアクティブ化操作に失敗します。このような場合、そのグレインの OnActivateAsync() ライフ サイクル コールバック メソッドが呼び出されることはありません。 アクティブ化の原因となったグレインに対する元の要求は、グレインのアクティブ化中の他のエラーと同じように、呼び出し元にエラーが返されます。 記憶域プロバイダーが特定のグレインの状態データを読み取る場合にエラーが発生すると、ReadStateAsync()Task から例外が発生します。 グレインは、Orleans の他の Task の例外と同様に、Task 例外の処理または無視を選択できます。

記憶域プロバイダーの構成が見つからないか、または不適切であるため、サイロの起動時に読み込みに失敗したグレインにメッセージを送信しようとすると、永続的なエラー BadProviderConfigException が返されます。

書き込み操作でのエラー モード

記憶域プロバイダーが特定のグレインの状態データを書き込む場合にエラーが発生すると、WriteStateAsync()Task から例外がスローされます。 通常、これは、WriteStateAsync()Task がこのグレイン メソッドの Task の最後の戻り値に正しくチェーンされている場合は、グレイン呼び出しの例外がクライアント呼び出し元にスローされることを意味します。 ただし、特定の高度なシナリオでは、他のエラーが発生した Task と同様に、このような書き込みエラーを具体的に処理するグレイン コードを記述できます。

エラー処理/リカバリー コードを実行するグレインは、書き込みエラーを正常に処理したことを示すために、例外/エラーWriteStateAsync()Taskをキャッチし、再スローしないようにする必要があります

Recommendations

JSON シリアル化または別のバージョン トレラントシリアル化形式を使用する

コードは進化しており、多くの場合、これにはストレージの種類も含まれます。 これらの変更に対応するには、適切なシリアライザーを構成する必要があります。 ほとんどの記憶域プロバイダーでは、JSON をシリアル化形式として使用するための UseJson オプションまたはそれと類似したものを使用できます。 既に格納されているデータ コントラクトを進化させる場合でも、読み込み可能であることを確認します。

Grain <TState> を使用してグレインにストレージを追加する

重要

Grain<T> を使用してグレインにストレージを追加することは、従来の機能と見なされます。グレイン ストレージは、前に説明したように IPersistentState<T> を使用して追加する必要があります。

Grain<T> を継承するグレイン クラス (T が永続化する必要があるアプリケーション固有の状態データ型である場所) では、その状態が指定されたストレージから自動的に読み込まれます。

このようなグレインは、このグレインの状態データの読み取り/書き込みに使用する記憶域プロバイダーの名前付きインスタンスを指定する StorageProviderAttribute でマークされます。

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

Grain<T> 基本クラスで、サブクラスが呼び出す次のメソッドを定義しました。

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

これらのメソッドの動作は、前に定義した IPersistentState<TState> に相当するメソッドに対応します。

記憶域プロバイダーの作成

状態永続化 API には、IPersistentState<T> または Grain<T> を介してグレインに公開される API と、IGrainStorage の中心となる記憶域プロバイダー API の 2 つの部分があります。これは、記憶域プロバイダーで実装する必要があるインターフェイスです。

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

このインターフェイスを実装し、その実装を登録して、カスタム記憶域プロバイダーを作成します。 既存の記憶域プロバイダーの実装の例については、「AzureBlobGrainStorage」を参照してください。

記憶域プロバイダーのセマンティクス

不透明なプロバイダー固有 Etag の値 (string) は、状態を読み取る場合に設定されるグレイン状態メタデータの一部として、記憶域プロバイダーで設定される 場合があります。 一部のプロバイダーは、Etag を使用しない場合に null としてこれを残すことを選択できます。

記憶域プロバイダーが Etag 制約違反を検出したときに Task の書き込み操作を実行しようとすると、書き込みが一時的なエラー InconsistentStateException でエラーが発生し、基になるストレージ例外がラップされる可能性があります

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

ストレージ操作のその他のエラー状態では、Task が返され、基になるストレージの問題を示す例外が発生する必要があります。 多くの場合、グレインのメソッドを呼び出してストレージ操作をトリガーした呼び出し元にこの例外がスローされる場合があります。 呼び出し元がこの例外を逆シリアル化できるかどうかを検討することが重要です。 たとえば、クライアントは、例外の種類を含む特定の永続化ライブラリを読み込んでいない可能性があります。 このため、例外を呼び出し元に反映することができる例外に変換することをお勧めします。

データ マッピング

個々の記憶域プロバイダーは、グレイン状態を格納する最適な方法を決定する必要があります。BLOB (さまざまな形式/シリアル化されたフォーム) またはフィールドごとの列は当然の選択です。

記憶域プロバイダーの登録

Orleans ランタイムは、グレインの作成時にサービス プロバイダー (IServiceProvider) から記憶域プロバイダーを解決します。 ランタイムは IGrainStorage のインスタンスを解決します。 たとえば、[PersistentState(stateName, storageName)] 属性を介して記憶域プロバイダーの名前が付けられている場合は、IGrainStorage の名前付きインスタンスが解決されます。

IGrainStorage の名前付きインスタンスを登録するには、こちらの AzureTableGrainStorage プロバイダーの例に従って AddSingletonNamedService 拡張メソッドを使用します。