Cosmos DBに対する .NET での再試行ロジック

## はじめに

Cosmos DB の再試行ロジックについてMicrosoft Docsの情報と.NET SDKのソースから自分が理解した内容を残します。
また、本内容に誤りなどありましたら、コメントいただければ幸いです。
なお、2022年6月時点の情報となりますので、今後、仕様が変わる可能性があります。

## SDK の接続モード
Cosmos DBの.NET SDKには2つの接続モードが用意されており、いずれの接続モードもSDKにて再試行ロジックが組み込まれています。
![接続モード](https://docs.microsoft.com/ja-JP/azure/cosmos-db/sql/media/performance-tips/connection-policy.png)
※イメージの引用元「 [Azure Cosmos DB SQL SDK connectivity modes - Available connectivity modes](https://docs.microsoft.com/en-us/azure/cosmos-db/sql/sql-sdk-connection-modes#available-connectivity-modes "Available connectivity modes") 」

### ゲートウェイ モード（Gateway mode）
  * すべての SDK プラットフォームでサポート
  * プロトコル：HTTPS
  * 単一の DNS エンドポイント
  * ファイアウォールの厳しい制限がある企業ネットワーク内でアプリケーションを実行する場合に最適
  * パフォーマンスはダイレクト モードより不利
    * ネットワーク ホップ：HttpClient → (HTTPS)→ Gateway Service → (TCP) → Replica
### ダイレクト モード（Direct mode）
  * .NET と Java SDK プラットフォームでのみサポート
  * プロトコル：TCP（初期認証とトラフィックの暗号化に TLS を使用）
  * アプリケーションからレプリカに直接接続
  * パフォーマンスはゲートウェイ モードより有利
    * ネットワーク ホップ：Transport Client → (TCP) → Replica

## SDK にて再試行するエラー時のステータス コード
何れの接続モードでも SDK を利用することで、特定のエラー条件で自動的に再試行されます。

| ステータス コード | 再試行の追加が必要か | SDK で再試行するか | 説明 |
|---|---|---|---|
| 400 | いいえ | いいえ | 正しくない要求 |
| 401 | いいえ | いいえ | 許可されていません |
| 403 | 省略可能 | いいえ | Forbidden |
| 404 | いいえ | いいえ | リソースが見つかりません |
| 408 | はい | はい | 要求がタイムアウトしました |
| 409 | いいえ | いいえ | 競合エラーは、書き込み操作でリソースに指定された ID (ID とパーティション キー) が既存のリソースによって取得された場合、または一意キー制約に違反した場合に発生します。 |
| 410 | はい | はい | Gone 例外 (SLA に違反しない一時的な障害) |
| 412 | いいえ | いいえ | サーバーで利用できるバージョンとは異なる eTag が操作によって指定される場合、前提条件エラーが発生します。<br>これは、オプティミスティック同時実行制御エラーです。 リソースの最新バージョンを読み取り、要求の eTag を更新した後、要求を再試行してください。 |
| 413 | いいえ | いいえ | 要求のエンティティが大きすぎます |
| 429 | はい | はい | 429 の場合は再試行しても問題ありません。<br>[HTTP 429 のトラブルシューティングを行うためのガイド](https://docs.microsoft.com/ja-JP/azure/cosmos-db/sql/troubleshoot-request-rate-too-large?tabs=resource-specific)をご確認ください。 |
| 449 | はい | はい | 書き込み操作でのみ発生する一時的なエラーであり、再試行しても問題ありません。<br>これは、Cosmos DB の同じオブジェクトを更新しようとしている同時操作が多すぎるという、設計上の問題を示している可能性があります。 |
| 500 | いいえ | いいえ | 予期しないサービス エラーのため、操作に失敗しました。 Azure サポートの問題を提出して、サポートにお問い合わせください。 |
| 503 | はい | はい | サービスを利用できません |

※表の引用元：Microsoft Docs「[Azure Cosmos DB SDK を使用した回復性があるアプリケーションの設計](https://docs.microsoft.com/ja-JP/azure/cosmos-db/sql/conceptual-resilient-sdk-applications)」

## 要求率が大きすぎるエラー（ステータス コード 429）
ステータス コード「429」は「要求率（RU/秒）が大きすぎる」エラー時のコードとなります。
これは Cosmos DB に対する要求がレート制限されていることを示します。

Cosmos DB では、読み取り、書き込み、クエリなどのサービスに対するデータベース操作に、一定量の要求ユニット (RU) が消費されます。
  ※要求ユニットについては「[Azure Cosmos DB の要求ユニット](https://docs.microsoft.com/ja-jp/azure/cosmos-db/request-units)」参照。

そして、Cosmos DB アカウントの種類が「[プロビジョニング スループット モード](https://docs.microsoft.com/ja-JP/azure/cosmos-db/set-throughput)」の場合、スループットを要求率（RU/秒）で設定するため、設定した要求率を超える場合に本エラーが発生します。

大量データの一括登録などで容易に発生するため、本エラーに対する再試行ロジックについて後述します。

## 要求率が大きすぎるエラーに対する SDK での再試行ロジック
#### 前提知識
* 最大再試行回数は、`CosmosClientOptions`の[`MaxRetryAttemptsOnRateLimitedRequests`プロパティ](https://docs.microsoft.com/ja-jp/dotnet/api/microsoft.azure.cosmos.cosmosclientoptions.maxretryattemptsonratelimitedrequests?view=azure-dotnet)で保持します。
  * デフォルト値は9回であり、エラーがアプリケーションに返される前に、同じ要求がサーバーに最大10回発行されることを意味します。
* 最大再試行待機時間は、`CosmosClientOptions`の[`MaxRetryWaitTimeOnRateLimitedRequests`プロパティ](https://docs.microsoft.com/ja-jp/dotnet/api/microsoft.azure.cosmos.cosmosclientoptions.maxretrywaittimeonratelimitedrequests?view=azure-dotnet)で保持します。
  * デフォルト値は30秒です。
  * 最小間隔は秒で、これよりも小さい間隔は無視されます。
  * 累積待機時間がこの値を超えると、クライアントは再試行を停止し、エラーをアプリケーションに返します。

### 再試行ロジック
アプリケーションが Cosmos DB SDK を利用して、Cosmos DB に要求を発行した際に「要求率が大きすぎる（429）」エラーが発生した場合の動作概要です。
1. Cosmos DB から「要求率が大きすぎる（429）」の応答を返す。
2. Cosmos DB SDK が、応答内の `x-ms-retry-after-ms` ヘッダーにある待機時間分、待機後、再試行する。
    a. 待機時間の累積が`CosmosClientOptions`の最大再試行待機時間を超える場合、アプリケーションに「要求率が大きすぎる（429）」エラーを返します。
    b. 再試行回数が`CosmosClientOptions`の最大再試行回数を超える場合、アプリケーションに「要求率が大きすぎる（429）」エラーを返します。
3. Cosmos DB から成功の応答を返す。
4. Cosmos DB SDK が、アプリケーションに成功の応答を返す。

アプリケーションが「要求率が大きすぎる（429）」エラーを受け取る場合、Cosmos DB SDK の再試行ロジックでは対処しきれなかった結果のため、以下の対応を検討します。
* `CosmosClientOptions`の最大再試行回数と最大再試行待機時間を増やすなど調整する。
  * ソースのイメージ
    ``` cs
    // CosmosClientオブジェクトを初期化
    CosmosClient cosmosClient = new CosmosClient(EndpointUrl, AuthorizationKey,
         new CosmosClientOptions() {
                 /* オプション設定・・・ */, 
                 MaxRetryAttemptsOnRateLimitedRequests = 19, // 最大再試行回数を増やす（9回 → 19回）
                 MaxRetryWaitTimeOnRateLimitedRequests = new TimeSpan(0, 2, 0) // 最大再試行待機時間を増やす（30秒 → 2分）
         });
    ```
* 指数バックオフ アルゴリズム、または「要求率が大きすぎる（429）」エラーでの再試行を拡張するよう、クライアントを構成する。
  * 再試行パターンについての参考
    * Microsoft Docs「再試行パターン」
      * https://docs.microsoft.com/ja-jp/azure/architecture/patterns/retry
    * Microsoft Docs「IHttpClientFactory ポリシーと Polly ポリシーで指数バックオフを含む HTTP 呼び出しの再試行を実装する」
      * https://docs.microsoft.com/ja-jp/dotnet/architecture/microservices/implement-resilient-applications/implement-http-call-retries-exponential-backoff-polly
      * ゲートウェイ モードの場合に使えそう。    

## 参考
* Azure サービスの再試行ガイダンス
  * https://docs.microsoft.com/ja-jp/azure/architecture/best-practices/retry-service-specific#cosmos-db
* Azure Cosmos DB SDK を使用した回復性があるアプリケーションの設計
  * https://docs.microsoft.com/ja-jp/azure/cosmos-db/sql/conceptual-resilient-sdk-applications
* Azure Cosmos DB SQL SDK の接続モード
  * https://docs.microsoft.com/ja-JP/azure/cosmos-db/sql/sql-sdk-connection-modes#available-connectivity-modes
* Azure Functions での接続の管理 - 接続の制限
  * https://docs.microsoft.com/ja-JP/azure/azure-functions/manage-connections?tabs=csharp#connection-limit
* Azure Cosmos DB の要求ユニット
  * https://docs.microsoft.com/ja-jp/azure/cosmos-db/request-units
* Bulk import data to Azure Cosmos DB SQL API account by using the .NET SDK
  * https://docs.microsoft.com/en-us/azure/cosmos-db/sql/tutorial-sql-api-dotnet-bulk-import
* Cosmos DB .NET SDK API のリファレンス
  * https://docs.microsoft.com/ja-JP/dotnet/api/microsoft.azure.cosmos?view=azure-dotnet
* Microsoft Azure Cosmos DB .NET SDK Version 3
  * https://github.com/Azure/azure-cosmos-dotnet-v3