Gridwich Azure ストレージ サービス (Gridwich.SagaParticipants.Storage.AzureStorage) では、Gridwich 用に構成されている Azure ストレージ アカウントの BLOB とコンテナーを操作できます。 ストレージ操作の例として、BLOB の作成、コンテナーの削除、BLOB のコピー、ストレージ層の変更などがあります。
Gridwich では、そのストレージ メカニズムが Azure Storage のブロック BLOB とコンテナーの両方で機能する必要があります。 BLOB とコンテナーに対して別個のクラスとストレージ サービス操作が存在するため、特定のストレージ操作が BLOB とコンテナーのどちらに関連するかについて、あいまいさがありません。 この記事は、特に明記しない限り、BLOB とコンテナーの両方に適用されます。
Gridwich では、ほとんどのストレージ操作が saga 参加要素Storage.AzureStorage
内の外部システムに公開されます。 他の saga 参加要素では、エンコード ワークフローの設定時に異なるコンテナーまたはアカウント間で BLOB をコピーするなどのタスクにストレージ サービスが使用されます。
この記事では、Gridwich Azure ストレージ サービスがどのようにソリューションの要件を満たし、イベント ハンドラーなどのメカニズムと統合されるかについて説明します。 リンクは、対応するソース コードを指しています。ソース コードには、コンテナー、クラス、メカニズムに関する詳しい解説が含まれています。
Azure Storage SDK
Gridwich では、REST 要求を手動で作成するのではなく、Azure Storage SDK のクラスを使用して Azure Storage とやり取りします。 ストレージ プロバイダー内で、BlobBaseClient クラスと BlobContainerClient クラスはストレージ要求を管理します。
現在、これらの SDK クライアント クラスでは、Gridwich で操作する必要がある 2 つの HTTP ヘッダー (操作コンテキストを表す x-ms-client-request-id
とオブジェクト バージョンを表す ETag
) への間接アクセスのみが許可されています。
Gridwichでは、プロバイダー クラスのペアが BlobBaseClientProvider と BlobContainerClientProvider の機能を スリーブと呼ばれるユニットに分配されます。 スリーブの詳細については、ストレージ スリーブに関する記事をご覧ください。
次の図は、SDK と Gridwich クラスの構造、およびインスタンスの相互関係を示しています。 矢印は「~への参照がある」ことを示しています。
パイプライン ポリシー
クライアント インスタンスを作成するときは、HTTP ヘッダーを操作するためのフックをパイプライン ポリシー インスタンスとして設定します。 このポリシーはクライアント インスタンスの作成時にのみ設定でき、ポリシーを変更することはできません。 クライアントを使用するストレージ プロバイダー コードは、実行中にヘッダー値を操作できる必要があります。 課題は、ストレージ プロバイダーとパイプラインのやり取りを正常に行うことです。
Gridwich パイプライン ポリシーについては、BlobClientPipelinePolicy クラスを参照してください。
ストレージ サービスのキャッシュ
TCP 接続の確立と認証では、SDK クライアント オブジェクト インスタンスが最初の要求を Azure Storage に送信するときにオーバーヘッドが発生します。 外部システム要求で同じ BLOB に対して複数の呼び出し (たとえば、メタデータの取得に続いて BLOB の削除) が行われた場合、オーバーヘッドは増加します。
オーバーヘッドを軽減するため、Gridwich では、操作コンテキストで使用される SDK クラスに応じて、ストレージ BLOB またはコンテナーごとに 1 つのクライアント インスタンスのキャッシュが保持されます。 Gridwich では、このクライアント インスタンスを保持し、外部システム要求の間に行われる同じ BLOB またはコンテナーに対する複数の Azure Storage 操作で、このインスタンスを使用できます。
Azure SDK が提供するクライアント クラスでは、SDK クライアント オブジェクト インスタンスが作成時に 1 つの BLOB またはコンテナーに固有である必要があります。 また、インスタンスを異なるスレッドで同時に使用できる保証もありません。 操作コンテキストは 1 つの要求を表しているため、Gridwich では BLOB またはコンテナーの名前と操作コンテキストの組み合わせに基づいてキャッシュが行われます。
このインスタンスを Azure Storage SDK クライアント構造体と組み合わせて再利用する場合は、効率とコードのわかりやすさのバランスを取るために、追加のサポート コードが必要になります。
コンテキスト引数
ほぼすべての Gridwich ストレージ サービス操作で、StorageClientProviderContext 型の特殊なコンテキスト引数が必要になります。 このコンテキスト引数は、次の要件を満たしています。
外部システムが Gridwich 要求で指定した、各要求に固有の JSON ベースの操作コンテキスト値を含む応答が、外部システムに提供されます。 詳細については、「Operation context (操作コンテキスト)」を参照してください。
Gridwich イベント ハンドラーなどのストレージ サービスの呼び出し元が、外部システムで参照できる応答を制御できます。 この制御により、サービスから外部システムに無関係な通知イベントが大量に送信されることを防止できます。 詳細については、「コンテキストのミュート」を参照してください。
Azure Storage の慣例に従って、並列のリーダーとライターを混在させることができる環境で要求と応答の一貫性が保証されます。 たとえば、ETag 追跡 がサポートされています。 詳細については、ETag に関する記事をご覧ください。
ストレージ コンテキスト
BLOB とコンテナーのどちらのストレージの種類でも、コンテキストは StorageClientProviderContext であり、次のような内容です。
string ClientRequestID { get; }
JObject ClientRequestIdAsJObject { get; }
bool IsMuted { get; set; }
string ETag { get; set; }
bool TrackingETag { get; set; }
最初の 2 つのプロパティは、StorageClientProviderContext インスタンスを初期化するために使用された操作コンテキストの異なる表現です。 このクラスには、コピー コンストラクターを含むさまざまなコンストラクターがあります。 追加のメソッドには、インプレース状態の複製を可能にする ResetTo
や、問題のある初期化で例外がスローされないようにする CreateSafe
静的メソッドなどがあります。
このクラスには、GUID と空の文字列に基づいてコンテキストを作成するための特別な処理も含まれています。 BLOB の Created および Deleted に対する Azure Storage 通知ハンドラーは、外部エージェントから発生する通知も処理するため、GUID 形式を必要とします。
コンテキストのミュート
IsMuted
プロパティは、アプリケーションがサービスに対して、生成された通知を呼び出し元 (外部システムなど) に公開することを期待するどうかを制御します。 ミュートされた操作では、サービスは生成されたイベントを公開しません。
例として、エンコーダーがエンコード タスクへの入力として Azure Storage 内に BLOB を配置するために実行する BLOB のコピーなどがあります。 外部システムにとっての関心事は、これらの詳細ではなく、エンコード ジョブの状態とエンコードされた出力を取得できる場所だけです。 こうした関心事を反映して、エンコーダーは次の操作を行います。
要求の操作コンテキストに基づいて、ミュートされていないストレージ コンテキスト (
ctxNotMuted
など) を作成します。コンテキスト クラスのコピー コンストラクターを使用するか、新しいインスタンスを作成して、ミュートされたストレージ コンテキスト (
ctxMuted
など) を作成します。 どちらのオプションでも、同じ操作コンテキスト値が使用されます。エンコードのセットアップに含まれるストレージ操作には、
ctxMuted
を指定します。 外部システムには、これらの操作が発生していることは示されません。エンコードの完了を反映するストレージ操作 (出力ファイルのターゲット コンテナーへのコピーなど) には、
ctxNotMuted
コンテキストを指定します。 Gridwich ハンドラーは、生成された Azure Storage 通知イベントを外部システムに公開します。
呼び出し元は、操作の最終的な可視性を制御します。 ミュートされた操作とミュートされていない操作は、どちらも同等の operationContext
値に基づいています。 コンテキストのミュートの目的は、イベント トレース ログから問題の診断を簡単に実行できるようにすることです。イベント トレース ログでは、操作のミュート状態に関係なく、要求に関連するストレージ操作が表示されるためです。
ResponseBaseDTO には、ブール型プロパティ DoNotPublish
があります。このプロパティは、公開するかどうかの最終的な決定を指示するためにイベントのディスパッチで使用されます。 イベントのディスパッチでは、コンテキストの IsMuted
プロパティに基づいて DoNotPublish
プロパティが設定されます。
このサービスによってミュート設定が Azure Storage に送信され、Azure Storage では 2 つの Gridwich ハンドラー (Created と Deleted) に提示されるストレージ通知イベントに clientRequestId
が設定されます。 これら 2 つのハンドラーでは、呼び出し元が要求したミュートを反映するように DoNotPublish
が設定されます。
ターゲットの整合性を保つ ETag
Azure Storage では、ターゲットの整合性を保つ必要がある要求シーケンスで HTTP の ETag
ヘッダーが使用されます。 例として、メタデータの取得およびメタデータの更新ストレージ操作の間で BLOB が変更されていないことの確認があります。
標準の HTTP の使用方法に合わせるため、このヘッダーには、ヘッダー値が変更された場合は基になるオブジェクトも変更されたと解釈される非透過の値が含まれます。 要求によってオブジェクトの現在の ETag
値が送信され、ストレージ サービスの現在の ETag
値と一致しない場合、要求はすぐに失敗します。 要求に ETag
値が含まれていない場合、Azure Storage ではそのチェックはスキップされ、要求はブロックされません。
ストレージ サービスの ETag
Gridwich では、ETag
は Gridwich ストレージ サービスと Azure Storage の間の内部詳細です。 他のコードで ETag
を認識する必要はありません。 このストレージ サービスでは、BLOB メタデータの取得操作や BlobDelete Event
要求を処理する BLOB の削除操作などのシーケンスで ETag
が使用されます。 ETag
を使用することで、BLOB の削除操作のターゲットがメタデータの取得操作と正確に同じバージョンの BLOB に設定されるようになります。
前の例で ETag
を使用するには、次の手順を実行します。
- 空の
ETag
とともにメタデータの取得要求を送信します。 - 応答の
ETag
値を保存します。 - 保存された
ETag
値を BLOB の削除要求に追加します。
2 つの ETag
値が異なる場合、削除操作は失敗します。 この失敗は、手順 2 と 3 の間で BLOB が他の操作によって変更されたことを意味します。 手順 1 からプロセスを繰り返します。
ETag
は、コンストラクターのパラメーターと StorageClientProviderContext クラスの文字列プロパティです。 Gridwich 固有の BlobClientPipelinePolicy のみが ETag
値を操作します。
ETag の使用を制御する
TrackingETag
プロパティは、次の要求で ETag
値を送信するかどうかを制御します。 値 true
は、ETag
が使用可能な場合にサービスがそれを送信することを意味します。
Azure Storage 要求の ETag
値がサブジェクトの BLOB またはコンテナーと一致しない場合、操作は失敗します。 この失敗は、ETag
は "要求がターゲットとしている正確なバージョン" を表現するための HTTP の標準的な手段であるため、仕様によるものです。要求には ETags
が一致する必要があることを示す TrackingETag
プロパティを含めることができ、ETag
値が重要でないことを示す TrackingETag
プロパティを含めることはできません。
パイプラインは、その REST 応答に Azure Storage 操作の ETag
値が存在する場合、常にそれを取得します。 パイプラインは、可能であれば、常に最後の操作の時点でコンテキストの ETag
プロパティを更新します。 TrackingETag
フラグは、同じクライアント インスタンスからの次の要求で ETag
プロパティの値が送信されるかどうかのみを制御します。 ETag
値が null または空の場合、TrackingETag
の値に関係なく、現在の要求で HTTP の ETag
値は設定されません。
ストレージのスリーブ
Gridwich では、そのストレージ メカニズムが Azure Storage のブロック BLOB とコンテナーの両方で機能する必要があります。 BLOB とコンテナーに対して別個のクラスとストレージ サービス操作が存在するため、特定のストレージ操作が BLOB とコンテナーのどちらに関連するかについて、あいまいさがありません。
プロバイダー クラスのペア (BLOB 用のクラスとコンテナー用のクラス) によって、2 つの機能セットがスリーブと呼ばれる単位で分配されます。 スリーブには、Azure SDK の一部であるストレージ ヘルパー クラスのインスタンスが含まれています。 ストレージ サービスを初期化すると、プロバイダーが作成され、ストレージ サービスのメソッドでそれらを直接使用できるようになります。
スリーブの構造
スリーブは、SDK クライアント オブジェクト インスタンスとストレージ コンテキストのコンテナーです。 ストレージ プロバイダーの関数は、Client
と Context
の 2 つのプロパティでスリーブを参照します。 BLOB 用のスリーブタイプと コンテナー用のスリーブタイプがあり、Client
タイプのプロパティ BlobBaseClient
と BlobContainerClient
がそれぞれあります。
BLOB 用のスリーブの一般的な構造は、次のようになっています。
BlobBaseClient Client { get; }
BlobServiceClient Service { get; }
StorageClientProviderContext Context { get; }
スリーブの Service
プロパティは便利です。 SDK BlobServiceClient クラスを使用する最終的なエンコーダー関連操作の中には、ストレージ アカウントの資格情報を必要とするものがあります。 この要件のため、別個のプロバイダーを作成する代わりに、既存の 2 種類のスリーブにサービス クライアント インスタンスが追加されました。
スリーブの使用方法
クライアント ストレージ プロバイダーは、スリーブ インスタンスを分配します。 ストレージ サービスのコードは、次の注釈付きのコード シーケンスのようになります (わかりやすくするために型を省略せずに記述しています)。
public bool DeleteBlob(Uri sourceUri, StorageClientProviderContext context)
{
. . .
StorageBlobClientSleeve sleeve = _blobBaseClientProvider.GetBlobBaseClientForUri(sourceUri, context); // Line A
BlobProperties propsIncludingMetadata = sleeve.Client.GetProperties(); // Line B
sleeve.Context.TrackingETag = true; // Send ETag from GetProperties()
var wasDeleted = sleeve.Client.DeleteBlob(); // Line C
sleeve.Context.TrackingETag = false;
var someResult = sleeve.Client.AnotherOperation(); // Line D
. . .
}
- 行 A では、Gridwich によって操作コンテキストがスリーブ コンテキストに自動的に設定されます。
TrackingETag
の既定値は false です。 - 行 B の後、
sleeve.Context
に行 A のETag
が格納され、同じClientRequestID
値が保持されます。 - 行 C では、行 B の
ClientRequestId
値とETag
値の両方が送信されます。 - 行 C の後、
Delete()
の応答で返された新しいETag
値がコンテキストに渡されます。 - 行 D では、
AnotherOperation()
の要求に対してETag
値が送信されません。 - 行 D の後、
AnotherOperation()
の応答で返された新しいETag
値がコンテキストに渡されます。
このストレージ サービスは現在、依存関係の挿入の構成で Transient
として設定されています。これは、スリーブベースのキャッシュが要求ごとに行われることを意味します。 詳細については、「ストレージ サービスと依存関係の挿入」を参照してください。
ストレージ サービスの代替手段
以下のセクションでは、現在の Gridwich ストレージ ソリューションには含まれない別のアプローチについて説明します。
サブクラス化によってパイプライン ポリシーを非表示にする
SDK クライアント型をサブクラス化すると、クライアントに 2 つ (各 HTTP ヘッダー値に対して 1 つ) のシンプルなプロパティが追加され、パイプライン ポリシーとのやり取りが完全に非表示になります。 しかし、Moq に深刻なバグがあるため、これらの派生型の mock
を使用して単体テストを作成することはできません。 Gridwich では Moq が使用されているため、このサブクラス化のアプローチは使用しないでください。
Moq のバグは、内部スコープの仮想関数が存在する状況でアセンブリ間のサブクラス化が誤って処理されていることに関連しています。 SDK クライアント クラスは、通常の外部ユーザーには見えない内部スコープの型を含む内部スコープの仮想関数を使用します。 Moq がいずれかの Gridwich アセンブリに存在するサブクラスの mock
を作成しようとすると、Gridwich クラスの派生元である SDK クライアント クラスで内部スコープの仮想関数が見つからないため、テストの実行時に失敗します。 Moq Castle プロキシの生成に変更を加える以外に、回避策はありません。
ストレージ サービスと依存関係の挿入
Gridwich では現在、ストレージ サービスが Transient
の依存関係の挿入サービスとして登録されています。 つまり、サービスに対して依存関係の挿入が要求されるたびに、新しいインスタンスが作成されます。 現在のコードは、要求 (たとえば、外部システムの要求) ごとに 1 つのインスタンスを意味する Scoped
に登録が変更された場合でも、正常に機能します。
しかし、登録が Singleton
(Gridwich 関数アプリ全体で 1 つのインスタンス) に変更された場合は、問題が発生します。 その場合、Gridwich のスリーブとデータのバイト範囲に対するキャッシュ メカニズムでは、異なる要求が区別されません。 また、キャッシュ モデルがチェックアウト モデルでないため、Gridwich ではインスタンスの使用中にキャッシュからそのインスタンスが削除されません。 SDK クライアント クラスがスレッドセーフである保証はないため、調整には多くの変更が必要になります。
このような理由から、Gridwich ストレージ サービスの依存関係の挿入の登録を Singleton
に変更しないで (現状のままにして) ください。 Gridwich では、依存関係の挿入の登録でこの規則に従っており、それを適用する単体テスト (CheckThatStorageServiceIsNotASingleton) が用意されています。
次の手順
製品ドキュメント:
Microsoft Learn モジュール: