1. 程式人生 > >Orleans 2.0官方文件(閆輝的個人翻譯)——4.8.1 grain持久化的目標

Orleans 2.0官方文件(閆輝的個人翻譯)——4.8.1 grain持久化的目標

grain持久化的目標

  1. 允許不同的grain型別,使用不同型別的儲存提供程式(例如,一個使用Azure表,一個使用ADO.NET),或相同型別的儲存提供程式但具有不同的配置(例如,兩者都使用Azure表,但一個使用儲存帳戶#1和一個使用儲存帳戶#2)
  2. 允許只更改配置檔案而不需要更改程式碼,就能交換儲存提供程式例項(例如,Dev-Test-Prod)。
  3. 提供一個框架,以便以後可以由Orleans團隊或其他人,編寫其他的儲存提供程式。
  4. 提供最少一組的生產級儲存提供程式
  5. 儲存提供程式可以完全控制如何在持久化後端儲存中,儲存grain的狀態資料。結果就是,Orleans沒有提供全面的ORM儲存解決方案,但允許自定義儲存提供程式在需要時支援特定的ORM要求。

grain 持久化 API

可以通過以下兩種方式之一宣告grain型別:

  • 繼承Grain:如果它們沒有任何持久化狀態,或者它們自己處理所有的持久化狀態。
  • 繼承Grain<T>:如果它們有一些持久化狀態,這些狀態希望由Orleans執行時來處理。換句話說,通過繼承Grain<T>,grain型別自動選擇加入Orleans系統管理的永續性框架。

對於本節的其餘部分,我們將僅考慮第2種宣告方式 (擴充套件Grain<T>) ,因為第1種宣告方式,grain將繼續像現在一樣執行,而不會有任何行為更改。

grain 狀態儲存

Grain<T>繼承的grain類,(其中T是需要被持久化的、特定於應用程式的狀態資料型別)將從指定的儲存中自動載入其狀態。

grain將標記一個[StorageProvider]屬性,該屬性指定儲存提供程式的命名例項,用於讀取/寫入此grain的狀態資料。

[StorageProvider(ProviderName="store1")]
public class MyGrain<MyGrainState> ...
{
  ...
}

Orleans框架提供了一種機制,來指定和註冊不同的儲存提供程式,並使用ISiloHostBuilder來配置它們。

var silo = new SiloHostBuilder()
    .AddMemoryGrainStorage("DevStore")
    .AddAzureTableGrainStorage("store1", options => options.ConnectionString = "DefaultEndpointsProtocol=https;AccountName=data1;AccountKey=SOMETHING1")
    .AddAzureBlobGrainStorage("store2", options => options.ConnectionString = "DefaultEndpointsProtocol=https;AccountName=data2;AccountKey=SOMETHING2")
    .Build();

配置IGrainStorage提供程式

Orleans本身支援一系列的IGrainStorage實現,您可以將這些實現用於您的應用程式,來儲存grain的狀態。在本節中,我們將介紹如何在一個silo中,配置AzureTableGrainStorageAzureBlobGrainStorageDynamoDBGrainStorageMemoryGrainStorage,和AdoNetGrainStorage在。其他IGrainStorage提供程式的配置類似。

AzureTableGrainStorage提供程式

var silo = new SiloHostBuilder()
    .AddAzureTableGrainStorage("TableStore", options => options.ConnectionString = "UseDevelopmentStorage=true")
    ...
    .Build();

通過AzureTableGrainStorageOptions,以下設定可用於配置AzureTableGrainStorage提供程式:

    /// <summary>
    /// Configuration for AzureTableGrainStorage
    /// </summary>
public class AzureTableStorageOptions
{
        /// <summary>
        /// Azure table connection string
        /// </summary>
        [RedactConnectionString]
        public string ConnectionString { get; set; }

        /// <summary>
        /// Table name where grain stage is stored
        /// </summary>
        public string TableName { get; set; } = DEFAULT_TABLE_NAME;
        public const string DEFAULT_TABLE_NAME = "OrleansGrainState";

        /// <summary>
        /// Indicates if grain data should be deleted or reset to defaults when a grain clears it's state.
        /// </summary>
        public bool DeleteStateOnClear { get; set; } = false;

        /// <summary>
        /// Stage of silo lifecycle where storage should be initialized.  Storage must be initialzed prior to use.
        /// </summary>
        public int InitStage { get; set; } = DEFAULT_INIT_STAGE;
        public const int DEFAULT_INIT_STAGE = ServiceLifecycleStage.ApplicationServices;

        #region json serialization
        public bool UseJson { get; set; }
        public bool UseFullAssemblyNames { get; set; }
        public bool IndentJson { get; set; }
        public TypeNameHandling? TypeNameHandling { get; set; }
        #endregion json serialization
}

注意:狀態大小不應超過64KB,這是Azure Table Storage強加的限制。

AzureBlobGrainStorage提供程式

var silo = new SiloHostBuilder()
    .AddAzureBlobGrainStorage("BlobStore", options => options.ConnectionString = "UseDevelopmentStorage=true")
    ...
    .Build();

通過AzureBlobStorageOptions,以下設定可用於配置AzureBlobGrainStorage提供程式:

public class AzureBlobStorageOptions
{
        /// <summary>
        /// Azure connection string
        /// </summary>
        [RedactConnectionString]
        public string ConnectionString { get; set; }

        /// <summary>
        /// Container name where grain stage is stored
        /// </summary>
        public string ContainerName { get; set; } = DEFAULT_CONTAINER_NAME;
        public const string DEFAULT_CONTAINER_NAME = "grainstate";

        /// <summary>
        /// Stage of silo lifecycle where storage should be initialized.  Storage must be initialzed prior to use.
        /// </summary>
        public int InitStage { get; set; } = DEFAULT_INIT_STAGE;
        public const int DEFAULT_INIT_STAGE = ServiceLifecycleStage.ApplicationServices;

        #region json serialization
        public bool UseJson { get; set; }
        public bool UseFullAssemblyNames { get; set; }
        public bool IndentJson { get; set; }
        public TypeNameHandling? TypeNameHandling { get; set; }
        #endregion json serialization
}

DynamoDBGrainStorage提供程式

var silo = new SiloHostBuilder()
    .AddDynamoDBGrainStorage("DDBStore", options =>
    {
        options.AccessKey = "MY_ACCESS_KEY";
        options.SecretKey = "MY_SECRET_KEY";
        options.Service = "us-wes-1";
    })
    ...
    .Build();

通過DynamoDBStorageOptions,以下設定可用於配置DynamoDBGrainStorage提供程式:

public class DynamoDBStorageOptions
{
        /// <summary>
        /// Gets or sets a unique identifier for this service, which should survive deployment and redeployment.
        /// </summary>
        public string ServiceId { get; set; } = string.Empty;

        /// <summary>
        /// AccessKey string for DynamoDB Storage
        /// </summary>
        [Redact]
        public string AccessKey { get; set; }

        /// <summary>
        /// Secret key for DynamoDB storage
        /// </summary>
        [Redact]
        public string SecretKey { get; set; }

        /// <summary>
        /// DynamoDB Service name 
        /// </summary>
        public string Service { get; set; }

        /// <summary>
        /// Read capacity unit for DynamoDB storage
        /// </summary>
        public int ReadCapacityUnits { get; set; } = DynamoDBStorage.DefaultReadCapacityUnits;

        /// <summary>
        /// Write capacity unit for DynamoDB storage
        /// </summary>
        public int WriteCapacityUnits { get; set; } = DynamoDBStorage.DefaultWriteCapacityUnits;

        /// <summary>
        /// DynamoDB table name.
        /// Defaults to 'OrleansGrainState'.
        /// </summary>
        public string TableName { get; set; } = "OrleansGrainState";

        /// <summary>
        /// Indicates if grain data should be deleted or reset to defaults when a grain clears it's state.
        /// </summary>
        public bool DeleteStateOnClear { get; set; } = false;

        /// <summary>
        /// Stage of silo lifecycle where storage should be initialized.  Storage must be initialzed prior to use.
        /// </summary>
        public int InitStage { get; set; } = DEFAULT_INIT_STAGE;
        public const int DEFAULT_INIT_STAGE = ServiceLifecycleStage.ApplicationServices;

        #region JSON Serialization
        public bool UseJson { get; set; }
        public bool UseFullAssemblyNames { get; set; }
        public bool IndentJson { get; set; }
        public TypeNameHandling? TypeNameHandling { get; set; }
        #endregion
    }

ADO.NET grain儲存提供程式

ADO .NET grain 儲存提供程式,允許您在關係資料庫中儲存grain狀態。目前支援以下資料庫:

  • SQL Server
  • MySQL/ MariaDB
  • PostgreSQL
  • Oracle

首先,安裝基礎包:

Install-Package Microsoft.Orleans.Persistence.AdoNet

還原專案的nuget包後,您就會找到支援的資料庫供應商的不同SQL指令碼,這些指令碼被複制到專案目錄\OrleansAdoNetContent中,其中每個受支援的ADO.NET擴充套件都有自己的目錄。您也可以從Orleans.Persistence.AdoNet庫中獲取它們。建立資料庫,然後執行相應的指令碼來建立表。

接下來的步驟是,安裝第二個特定於所需資料庫供應商的NuGet包(參見下表),並以程式設計方式或通過XML,配置儲存提供程式。

資料庫 指令碼 NuGet包 AdoInvariant 備註
SQL Server SQLServer的-Persistence.sql System.Data.SqlClient System.Data.SqlClient  
MySQL / MariaDB MySQL-Persistence.sql MySql.Data MySql.Data.MySqlClient  
PostgreSQL PostgreSQL-Persistence.sql Npgsql Npgsql  
Oralce Oracle-Persistence.sql ODP.net Oracle.DataAccess.Client 不支援.net core

以下是如何通過ISiloHostBuilder配置ADO.NET儲存提供程式的示例:

var siloHostBuilder = new SiloHostBuilder()
    .AddAdoNetGrainStorage("OrleansStorage", options=>
    {
        options.Invariant = "<Invariant>";
        options.ConnectionString = "<ConnectionString>";
        options.UseJsonFormat = true;
    });

實質上,您只需要設定特定於資料庫供應商的連線字串和標識供應商的 Invariant(參見上表)。您還可以選擇儲存資料的格式,可以是二進位制(預設),JSON或XML。雖然二進位制是最緊湊的選項,但它是不透明的,您將無法讀取或處理資料。建議使用JSON。

您可以通過AdoNetGrainStorageOptions,設定以下屬性:


/// <summary>
/// Options for AdonetGrainStorage
/// </summary>
public class AdoNetGrainStorageOptions
{
        /// <summary>
        /// Connection string for AdoNet storage.
        /// </summary>
        [Redact]
        public string ConnectionString { get; set; }

        /// <summary>
        /// Stage of silo lifecycle where storage should be initialized.  Storage must be initialzed prior to use.
        /// </summary>
        public int InitStage { get; set; } = DEFAULT_INIT_STAGE;
        /// <summary>
        /// Default init stage in silo lifecycle.
        /// </summary>
        public const int DEFAULT_INIT_STAGE = ServiceLifecycleStage.ApplicationServices;

        /// <summary>
        /// The default ADO.NET invariant used for storage if none is given. 
        /// </summary>
        public const string DEFAULT_ADONET_INVARIANT = AdoNetInvariants.InvariantNameSqlServer;
        /// <summary>
        /// The invariant name for storage.
        /// </summary>
        public string Invariant { get; set; } = DEFAULT_ADONET_INVARIANT;

        #region json serialization related settings
        /// <summary>
        /// Whether storage string payload should be formatted in JSON.
        /// <remarks>If neither <see cref="UseJsonFormat"/> nor <see cref="UseXmlFormat"/> is set to true, then BinaryFormatSerializer will be configured to format storage string payload.</remarks>
        /// </summary>
        public bool UseJsonFormat { get; set; }
        public bool UseFullAssemblyNames { get; set; }
        public bool IndentJson { get; set; }
        public TypeNameHandling? TypeNameHandling { get; set; }
        #endregion
        /// <summary>
        /// Whether storage string payload should be formatted in Xml.
        /// <remarks>If neither <see cref="UseJsonFormat"/> nor <see cref="UseXmlFormat"/> is set to true, then BinaryFormatSerializer will be configured to format storage string payload.</remarks>
        /// </summary>
        public bool UseXmlFormat { get; set; }
}

ADO.NET持久化具有:版本資料,以及使用任意應用程式規則和流定義任意(反)序列化程式的功能,但目前沒有方法將它們暴露給應用程式程式碼。參見ADO.NET Persistence Rationale中的更多資訊。

MemoryGrainStorage

MemoryGrainStorage是一個簡單的grain儲存實現,它背後實際上並沒有使用持久化資料儲存。便於快速學習使用grain 儲存,但不要打算用於生產場景。

注意:此提供程式將狀態持久化為易失性記憶體,該記憶體將在silo關閉時被刪除。僅用於測試。

以下通過ISiloHostBuilder,如何設定記憶體儲存提供程式 。

var siloHostBuilder = new SiloHostBuilder()
    .AddMemoryGrainStorage("OrleansStorage", options=>options.NumStorageGrains = 10);

您可以通過MemoryGrainStorageOptions,設定以下屬性:

/// <summary>
/// Options for MemoryGrainStorage
/// </summary>
public class MemoryGrainStorageOptions
{
        /// <summary>
        /// Default number of queue storage grains.
        /// </summary>
        public const int NumStorageGrainsDefaultValue = 10;
        /// <summary>
        /// Number of store grains to use.
        /// </summary>
        public int NumStorageGrains { get; set; } = NumStorageGrainsDefaultValue;

        /// <summary>
        /// Stage of silo lifecycle where storage should be initialized.  Storage must be initialzed prior to use.
        /// </summary>
        public int InitStage { get; set; } = DEFAULT_INIT_STAGE;
        /// <summary>
        /// Default init stage
        /// </summary>
        public const int DEFAULT_INIT_STAGE = ServiceLifecycleStage.ApplicationServices;
}

儲存提供程式的注意事項

如果沒有為一個Grain<T>的grain類指定[StorageProvider]屬性,則將搜尋名為Default的提供程式。如果未找到,則將其視為缺少儲存提供程式。

如果在配置時,未將grain類引用的儲存提供程式新增到silo中,則該型別的grain將無法在執行時啟用,並且對它們的呼叫將失敗,異常資訊Orleans.Storage.BadProviderConfigException指出此grain型別未被載入。但其餘的grain型別不會受到影響。

不同的grain型別,可以使用不同的已配置的儲存提供程式,即使兩者都是相同的型別:例如,兩個不同的Azure表儲存提供程式例項,它們連線到不同的Azure儲存帳戶(請參閱上面的配置檔案示例)。

儲存提供程式的所有詳細配置的資訊都是通過ISiloHostBuilder定義的。目前沒有提供動態更新或更改一個silo所使用的儲存提供程式列表的機制。但是,這是一個優先順序/工作負載約束,而不是一個基本設計約束。

狀態持久化 API

狀態持久化API有兩個部分:grain狀態API和儲存提供程式API。

grain狀態API

Orleans 執行時中的grain狀態儲存功能,將提供讀寫操作,以自動填充/儲存該grain的資料物件GrainState。在幕後,這些功能將連線(在Orleans client-gen工具生成的程式碼中)到為該grain配置的適當的持久化提供程式。

grain狀態的讀/寫功能

當一個grain被啟用時,grain狀態會被自動讀取,不過,grain負責在必要時顯式地觸發任何改變的grain狀態的寫入。有關錯誤處理機制的詳細資訊,請參閱下面的“ 故障模式”部分。

在為該啟用呼叫OnActivateAsync()方法之前GrainState被自動讀取(等同於使用base.ReadStateAsync())。 在任何方法呼叫一個grain之前,除非該grain被這個呼叫啟用,否則GrainState不會被重新整理。

在任何grain方法呼叫期間,grain都可以通過呼叫base.WriteStateAsync(),請求Orleans執行時將該啟用體的當前grain狀態資料,寫入指定的儲存提供程式。當對其狀態資料進行重大更新時,grain負責顯式地執行寫操作。最常見的做法是,grain方法把base.WriteStateAsync() 的Task,作為從該grain方法返回的最終結果Task,但不是必須要遵循此模式。在任何grain方法呼叫之後,執行時不會自動更新儲存的grain狀態。

在grain中的任何grain方法或定時器回撥處理方法中,grain通過呼叫base.ReadStateAsync(),請求Orleans執行時從指定的儲存提供程式中,重新讀取當前grain狀態資料,以進行該啟用操作。這將使用從持久化儲存中讀取的最新狀態資料,完全覆蓋儲存在grain狀態物件中的當前的狀態資料。

一個隱性的特定於提供程式的Etag值(string),當狀態被讀取時,可以由儲存提供程式設定為佔據grain狀態的元資料的一部分。某些提供程式如果不使用Etag的話,可以選擇將其保留為null

從概念上講,Orleans 執行時會持有grain狀態資料物件的深拷貝,以供自己在任何寫操作時使用。在幕後,執行時可以使用優化規則和啟發式方法,來避免在某些情況下執行部分或全部的深拷貝,前提是預留了預期的邏輯隔離語義。

grain狀態讀/寫操作的示例程式碼

grain必須繼承Grain<T>類,才能參與Orleans的grain狀態持久化機制。上述定義中的T將被替換為該grain的特定於應用程式的grain狀態類;見下面的例子。

grain類還應該使用一個[StorageProvider]屬性進行註解,該屬性告訴執行時,哪個儲存提供程式(例項)與此型別的grain一起使用。

public class MyGrainState
{
  public int Field1 { get; set; }
  public string Field2 { get; set; }
}

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

grain狀態讀取

在呼叫grain的OnActivateAsync()方法之前,Orleans執行時將自動進行grain狀態的初始讀取;不需要任何應用程式程式碼即可實現此操作。從這一刻起,grain的狀態可以通過grain類內的屬性Grain<T>.State來獲得。

grain狀態寫入

在對grain的記憶體狀態進行任何適當的更改之後,grain應該呼叫base.WriteStateAsync()方法,通過為此grain型別定義的儲存提供程式,將更改寫入持久化儲存。此方法是非同步的,並返回一個Task,此Task通常被grain方法作為它自己的完成Task返回。

public Task DoWrite(int val)
{
  State.Field1 = val;
  return base.WriteStateAsync();
}

grain狀態重新整理

如果grain希望從後端儲存顯式地重讀此grain的最新狀態,那麼grain應該呼叫base.ReadStateAsync()方法。這將通過為該grain型別定義的儲存提供程式,從持久化儲存中重新載入grain狀態,當ReadStateAsync() Task完成時,將覆蓋並替換在先前的記憶體中的grain狀態副本。

public async Task<int> DoRead()
{
  await base.ReadStateAsync();
  return State.Field1;
}

grain狀態持久化操作失敗的模式

grain狀態讀取操作失敗的模式

在初始讀取特定grain的狀態資料期間,儲存提供程式返回的失敗,將導致該grain的啟用操作失敗; 在這種情況下,不會呼叫該grain的生命週期回撥方法OnActivateAsync()。對引發該grain啟用的原始請求,將以與grain啟用期間的任何其他失敗相同的方式,以失敗的形式被返回給呼叫者。儲存提供程式在讀取特定grain的狀態資料時,所遭遇的失敗將導致ReadStateAsync() Task失敗。grain可以選擇處理或忽略失敗的Task,就像Orleans的其他任何Task一樣。

在silo啟動時,某個grain由於儲存提供程式配置的缺少/錯誤,而未能成功載入,此時嘗試將訊息傳送到該grain,將返回永久性錯誤Orleans.BadProviderConfigException

grain狀態寫入操作失敗的模式

儲存提供程式寫入特定grain的狀態資料時遇到的失敗,將導致WriteStateAsync() Task失敗。通常,這意味著,grain呼叫將以失敗的形式,被返回給客戶端呼叫程式,前提是WriteStateAsync() Task被正確地鏈入到此grain方法的最終返回的Task。但是,某些高階場景下,可能會編寫grain程式碼來專門處理此類寫入錯誤,就像它們可以處理任何其他失敗的Task一樣。

執行錯誤處理/恢復程式碼的grain,必須捕獲異常/失敗的WriteStateAsync() Task,而不是重新丟擲,以表示它們已成功處理寫入錯誤。

儲存提供程式API

有一個服務提供程式API,用於編寫額外的持久化提供程式 —— IGrainStorage

Persistence Provider API涵蓋了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);
}

儲存提供程式的語義

當儲存提供程式檢測到Etag約束衝突時,任何執行寫入操作的嘗試,都應該導致寫入Task失敗,並出現瞬時錯誤Orleans.InconsistentStateException,幷包裝底層的儲存異常。

public class InconsistentStateException : AggregateException
{
  /// <summary>The Etag value currently held in persistent storage.</summary>
  public string StoredEtag { get; private set; }
  /// <summary>The Etag value currently held in memory, and attempting to be updated.</summary>
  public string CurrentEtag { get; private set; }

  public InconsistentStateException(
    string errorMsg,
    string storedEtag,
    string currentEtag,
    Exception storageException
    ) : base(errorMsg, storageException)
  {
    this.StoredEtag = storedEtag;
    this.CurrentEtag = currentEtag;
  }

  public InconsistentStateException(string storedEtag, string currentEtag, Exception storageException)
    : this(storageException.Message, storedEtag, currentEtag, storageException)
  { }
}

寫入操作的任何其他失敗,都應該導致寫入Task中斷,並帶有一個異常資訊,此異常資訊包含底層的儲存異常。

資料對映

各個儲存提供程式應該決定如何最好地儲存grain狀態 ——blob(各種格式/序列化形式)或column-per-field是顯而易見的選擇。

Azure Table的基本儲存提供程式,使用Orleans二進位制序列化,將狀態資料欄位編碼為單個表列。

ADO.NET持久化基礎理論

ADO.NET支援的持久化儲存的原則是:

  1. 在資料,資料格式和程式碼的演變過程中,保護關鍵業務資料的安全。
  2. 利用供應商和特定於儲存的功能。

實際上,這意味著遵循ADO.NET的實現目標,並在特定於ADO.NET的儲存提供程式中,新增一些實現邏輯,以使儲存中的資料形態不斷演化。

除了通常的儲存提供程式功能外,ADO.NET提供程式還具有內建功能:

  1. 在往返狀態時,將儲存資料格式從一種格式更改為另一種格式(例如,從JSON到二進位制)。
  2. 以任意的方式,塑造要儲存或從儲存中讀取的型別。這有助於改進版本狀態。
  3. 以流資料的形式,從資料庫中獲取資料。

1.和2.可以被應用任意的決策引數,例如grain IDgrain型別有效載荷資料

這樣做是為了選擇一種格式,例如簡單二進位制編碼(SBE),並實現了 IStorageDeserializerIStorageSerializer。內建的(反)序列化器是使用此方法構建的。該OrleansStorageDefault(反)序列化 可以用作如何實現其他格式的示例。

當(de)序列化器被實現後,它們需要新增到AdoNetGrainStorage中StorageSerializationPicker屬性中。這是IStorageSerializationPicker的一個實現。預設情況下, 將使用StorageSerializationPicker。在RelationalStorageTests中,可以看到更改資料儲存格式或使用(反)序列化器的示例。

目前沒有方法將此公開給Orleans應用程式消費,因為沒有方法可以訪問框架建立的AdoNetGrainStorage