AWS SDK for PHP のリトライ・タイムアウト制御

AWS SDK for PHP を使用してエラーが発生した場合のリトライについて、簡単に整理しました。

## 基本的な戦略

参考) [AWS でのエラーの再試行とエクスポネンシャルバックオフ](https://docs.aws.amazon.com/ja_jp/general/latest/gr/api-retries.html)

- サーバーエラー(5xx)またはスロットリングエラー、または接続失敗やタイムアウトなどのネットワークエラーといった、リトライにより問題が解消する可能性があるものは、リトライする。
- クライアントエラー(4xx)などのリクエスト内容自体に問題があるものは、直ちにエラーとする。

## リトライ・タイムアウト設定

参考) [Configuration Options - AWS SDK for PHP](https://docs.aws.amazon.com/sdk-for-php/v3/developer-guide/guide_configuration.html)

### 設定項目

Client 生成時のパラメーターまたは環境変数で指定します。

|クライアント設定|環境変数|説明|未設定時|
|:--|:--|:--|--:|
|http.connect_timeout||接続タイムアウト時間 (秒)|0 (無期限)|
|http.timeout||タイムアウト時間 (秒)|0 (無期限)|
|retries.mode|AWS_RETRY_MODE|リトライモード|legacy|
|retries.max_attempts|AWS_MAX_ATTEMPTS|最大リクエスト試行回数|3|

設定例)

```php
$s3 = new Aws\S3\S3Client([
    'region' => 'ap-northeast-1',
    'version' => 'latest',
    'http' => [
        'connect_timeout' => 5,
        'timeout' => 60,
    ],
    'retries' => [
        'mode' => 'standard',
        'max_attempts' => 3,
    ],
]);
```

### http.connect_timeout : 接続タイムアウト時間(秒)

- ネットワークの問題によりサーバーとの通信ができない場合に、エラーになるまでの時間。
- 未設定時: 0 (無限)
- CURLOPT_CONNECTTIMEOUT に相当する。([参考](https://www.php.net/manual/en/function.curl-setopt.php))
- http.timeout より大きな値を指定することはできない。
- 0 (無限) でも一定の時間でタイムアウトはするようです。このあたりは OS か環境によって変わるようなので、期待すべき動作ではありません。

### http.timeout : タイムアウト時間(秒)

- リクエストを開始してからこの時間経過するまでにレスポンスが受信が完了しなければエラーとする。
- 未設定時: 0 (無限)
- CURLOPT_TIMEOUT に相当する。([参考](https://www.php.net/manual/en/function.curl-setopt.php))
- 大きなデータを送受信する場合は、サイズと回線速度を考慮して指定する必要がある。
- サーバーへの接続後応答が受信できない場合、タイムアウト時間を設定しないと無期限で待ち続けます。リトライ自体も発生しないため、その後にネットワーク障害が回復したとしても永遠にブロックしてしまうこともあるので注意が必要です。

### retries.mode : リトライモード

リトライ戦略として、以下のモードが用意されています。

- legacy (デフォルト)
- standard : 成功する可能性の低い再試行を防ぐために、再試行のクォータシステムを追加
- adaptive : standard + クライアント側のレートリミッターを追加 (experimental)

adaptive はまだ実験的機能とのこと。legacy は [RetryMiddleware](https://github.com/aws/aws-sdk-php/blob/master/src/RetryMiddleware.php)、standard および adaptive は [RetryMiddlewareV2](https://github.com/aws/aws-sdk-php/blob/master/src/RetryMiddlewareV2.php) で実装されています。

以下のようなリトライ要因が発生した場合にリトライし、最後に発生したエラーを例外として返します。

- 接続タイムアウト (connect_timeout 設定)
- タイムアウト (timeout 設定)
- サーバーエラー受信 (500, 502, 503, 504 のみ)
- スロットリング関連のエラー受信 (RequestLimitExceeded, Throttling など)

### retries.max_attempts : 最大リクエスト試行回数

- エラーが発生した場合にリクエストを試行する最大回数
- 未設定時: retries.mode によって異なる。
- 環境変数で指定する場合: AWS_MAX_ATTEMPTS
- 初回を**含む**、リクエスト試行の最大回数 (Python 版の retries.total_max_attempts に相当する)

## 例外

エラー発生時の例外は AWSException をベースとした例外が throw されます。

通信上のエラー(接続失敗やタイムアウト)がありレスポンスの受信ができなかった場合、`AWSException->isConnectionError()` が true となり、`AWSException->getAwsErrorCode()` は空になります。`AWSException->getPrevious()` で Guzzle の例外を取得してエラー要因を知ることができます。

エラーレスポンスを受信した場合、`AWSException->isConnectionError()` は false、`AWSException->getAwsErrorCode()` でエラー要因(文字列)が返されます。`AWSException->getPrevious()` で Guzzle の例外を取得して HTTP のエラー要因を知ることもできます。

Guzzle の例外についての詳細は[こちら](https://docs.guzzlephp.org/en/stable/quickstart.html#exceptions)。

|Case|isConnectionError()|getAwsErrorCode()|getPrevious()|
|:--|:--|:--|:--|
|通信エラー|true|-|GuzzleHttp\Exception\ConnectException|
|エラーレスポンス受信|false|エラーコード(文字列)|GuzzleHttp\Exception\RequestException|