粒紋持續性

粒紋可以有與其相關聯的多個具名的持續性資料物件。 這些狀態物件會在粒紋啟動期間從儲存體載入,以便在要求期間使用。 粒紋持續性會使用可延伸的外掛程式模型,以便使用任何資料庫的儲存體提供者。 此持續性模型的設計目的是為了簡單起見,並非意欲涵蓋所有資料存取模式。 粒紋也可以直接存取資料庫,而不需要使用粒紋持續性模型。

Grain persistence diagram

在上圖中,UserGrain 有設定檔狀態和購物車狀態,每個狀態都儲存在個別的儲存體系統中。

目標

  1. 每個粒紋有多個具名的持續性資料物件。
  2. 多個已設定的儲存提供者,每個提供者都可以有不同的設定,並由不同的儲存體系統支援。
  3. 儲存體提供者可由社群開發及發佈。
  4. 儲存體提供者可完全控制如何將粒紋狀態資料儲存在持續性備份存放區中。 推論:Orleans 未提供完整的 ORM 儲存體解決方案,而是允許自訂儲存提供者視需要支援特定的 ORM 需求。

套件

可以在 OrleansNuGet 上找到 粒紋儲存體提供者。 正式維護的封裝包括:

API

粒紋會使用 IPersistentState<TState> 與其持續性狀態的互動,其中 TState 是可序列化狀態型別:

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

IPersistentState<TState> 的執行個體會插入到粒紋中作為建構函式參數。 您可以使用 PersistentStateAttribute 屬性來標註這些參數,以識別要插入的狀態名稱,以及提供它的儲存體提供者的名稱。 下列範例會藉由將兩個具名狀態插入 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;
    }
}

不同的粒紋型別可以使用所設定的不同儲存提供者,即使兩者型別相同也一樣;例如,兩個不同的 Azure 表格儲存體提供者執行個體,連線到不同的 Azure 儲存體帳戶。

讀取狀態

粒紋狀態會在啟動粒紋時自動讀取,但粒紋會負責在必要時明確觸發任何已變更的粒紋狀態的寫入。

如果粒紋想要從備份存放區明確地重新讀取此粒紋的最新狀態,則粒紋應該呼叫 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 Runtime 會取得粒紋狀態資料物件的深層複本,供其在任何寫入作業期間使用。 實際上,執行階段可能會使用最佳化規則和啟發學習法,以避免在某些情況下執行部分或所有深層複本 (如果會保留預期的邏輯隔離語意的話)。

如需錯誤處理機制的詳細資料,請參閱下面的失敗模式一節。

清除狀態

ClearStateAsync 方法會清除儲存體中的粒紋狀態。 視提供者而定,此作業可能會選擇性地完全刪除粒紋狀態。

開始使用

在粒紋可以使用持續性之前,必須先在定址接收器上設定儲存體提供者。

首先,設定儲存體提供者,一個用於設定檔狀態,另一個用於購物車狀態:

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

現在,儲存體提供者已設定為名稱 "profileStore",我們可以從粒紋存取此提供者。

持續性狀態可以透過兩個主要方式新增至粒紋:

  1. 藉由插入 IPersistentState<TState> 到粒紋的建構函式。
  2. 藉由繼承自 Grain<TGrainState>

將儲存體新增至粒紋的建議方式,是將 IPersistentState<TState> 插入具有相關聯 [PersistentState("stateName", "providerName")] 屬性的粒紋建構函式。 如需 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 生命週期回呼方法的呼叫。 對導致啟動粒紋的原始要求會退回至呼叫端,正如同在粒紋啟動期間發生的任何其他失敗。 讀取特定粒紋的狀態資料時,儲存體提供者遇到的失敗會導致 ReadStateAsyncTask 發生例外狀況。 粒紋可以選擇處理或忽略 Task 例外狀況,正如同 Orleans 中 Task 的任何其他狀況。

任何嘗試將訊息傳送至因遺失/錯誤的儲存體提供者設定而無法在定址接收器啟動時載入的粒紋,都會傳回永久錯誤 BadProviderConfigException

寫入作業的失敗模式

寫入特定粒紋的狀態資料時,儲存體提供者遇到的失敗會導致 WriteStateAsync()Task 擲回例外狀況。 通常,這表示粒紋呼叫例外狀況會擲回給用戶端呼叫端,前提是 WriteStateAsync()Task 已正確鏈結至此粒紋方法的最終傳回 Task。 不過,在某些進階案例中,您可以撰寫粒紋程式碼來特別處理這類寫入錯誤,正如同它們可以處理任何其他發生錯誤的 Task

執行錯誤處理/復原程式碼的粒紋必須攔截例外狀況/錯誤的 WriteStateAsync()Task,而不是重新擲回,以表示它們已成功處理寫入錯誤。

建議

使用 JSON 序列化或其他版本容錯序列化格式

程式碼演進,而這通常也包含儲存體型別。 為適應這些變更,應該設定適當的序列化程式。 對於大部分的儲存體提供者,可以使用 UseJson 選項或類似選項來使用 JSON 作為序列化格式。 確保在演進的資料合約時,已儲存的資料仍可載入。

使用 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:

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

實作此介面並註冊該實作,以建立自訂儲存體提供者。 如需現有儲存體提供者實作的範例,請參閱 AzureBlobGrainStorage

儲存體提供者語意

不透明的提供者特定 Etag 值 (string) 可能由儲存體提供者在讀取狀態時隨著粒紋狀態中繼資料的一部分填入來設定。 有些提供者可能會選擇將此項目保留為 null (如果他們未使用 Etag)。

在儲存體提供者偵測到 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 的具名執行個體,請使用 AddSingletonNamedService 擴充方法,遵循這裡的 AzureTableGrainStorage 提供者範例。