Couchbase Server Java SDK解説: エラー処理の基礎

## はじめに

エラーは避けられません。そのため、SDKには、（一時的な）障害が発生した場合でも、アプリケーションを可能な限り最高の状態に保つという1つの目標に焦点を当てた非常に広範なエラー処理および再試行機能があります。
当然ながら、アプリケーションドメインを理解していないため、SDKだけでは、必要なすべての決定を下すことができません。ほとんどの場合、透過的な再試行が最善の選択になり得ますが、場合によっては、すぐに失敗して別のデータソースに切り替える必要があります。

ここでは、エラー処理の基礎について記します。

別の機会に、高度なエラー処理メカニズム、についても整理したいと考えています。

## エラー処理の基礎

次の基本事項は、SDKが再試行の決定を行う方法とエラーがどのように表示されるかを理解するのに役立ちます。

### リクエストのライフサイクル
次の画像は、リクエストライフサイクル中の高レベルのフェーズを示しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176567/ccfcd973-0090-8c73-2348-8cd31c34fc5e.png)


- プレディスパッチ　Pre-Dispatch： これは、リクエストのライフサイクルの初期段階です。リクエストが作成され、SDKは操作をディスパッチする適切なソケット/エンドポイントを見つけようとしています。

- ディスパッチ Dispatch： SDKは操作をネットワークに配置し、応答を待ちます。再試行可能性は要求のべき等性に依存するため、これはライフサイクルの重要なポイントです（後で説明します）。

- 応答到着　Response Arrived： サーバーから応答が到着すると、SDKはそれをどう処理するかを決定します（最良の場合、操作を正常に完了します）。

まず、例外の大まかな分類について概説します。

応答が到着し、それが失敗を示し、SDKがそれを再試行できないと判断した場合、明示的な例外を除いて操作は失敗します。たとえば、insert操作を実行し、ドキュメントがすでに存在する場合、SDKは`DocumentExistsException`を発生させて操作を失敗させます。

他のすべての場合、特にプレディスパッチまたはディスパッチ中に障害が発生すると、`TimeoutException`または`RequestCanceledException`のいずれかになります。

### タイムアウト例外

`TimeoutException`、その実装である`UnambiguousTimeoutException`と`AmbiguousTimeoutException`について解説します。

タイムアウトが問題の原因になることはなく、常に症状であるという考え方を確立することが重要です。

必要な際にタイムアウトを発生させることは重要です。そうしないと、代わりにスレッドが長時間ブロックされてしまうからです。

タイムアウト例外を使用すると、タイムアウトが発生したときに何が発生するかを制御でき、何らかの理由で操作を完了できない場合のセーフティネットと最後の手段が提供されます。

SDKは、サーバー側に副作用が発生していないことが確実でない限り、`AmbiguousTimeoutException`を発生させます（たとえば、次のセクションで説明するべき等操作がタイムアウトした場合）。ほとんどの場合、ジェネリックの`TimeoutException`を処理するだけで十分です。

タイムアウトが原因になることはなく、常に症状であるため、最初にタイムアウトの原因となった可能性のあるコンテキスト情報を提供することが重要です。最新世代のSDKでは、まさにそれを支援する`ErrorContext`の概念を導入しました。

`ErrorContext`上の方法として利用可能である`TimeoutException`を通じて`context()`ゲッターが、ログに出力するとき、最も重要なことは、自動的に例外出力に取り付けられています。`TimeoutException`コンテキストが添付されたそのようなの出力例を次に示します。

```
Exception in thread "main" com.couchbase.client.core.error.UnambiguousTimeoutException: GetRequest, Reason: TIMEOUT {"cancelled":true,"completed":true,"coreId":"0x5b36b0db00000001","idempotent":true,"reason":"TIMEOUT","requestId":22,"requestType":"GetRequest","retried":14,"retryReasons":["ENDPOINT_NOT_AVAILABLE"],"service":{"bucket":"travel-sample","collection":"_default","documentId":"airline_10226","opaque":"0x24","scope":"_default","type":"kv"},"timeoutMs":2500,"timings":{"totalMicros":2509049}}
	at com.couchbase.client.java.AsyncUtils.block(AsyncUtils.java:51)
//   ... (rest of stack omitted) ...
```

以下の情報を観察することができます

- `GetRequest`は、2500MS後にタイムアウトになった

- travel-sampleバケットのID`airline_10226`を持つドキュメントに関するリクエスト

- 15回再試行されましたが、その理由は常に`ENDPOINT_NOT_AVAILABLE`だった

`ENDPOINT_NOT_AVAILABLE`は、ソケットが接続されていない/使用できないため、ソケットを介して操作を送信できなかったことを示します。ソケットに問題があることがわかったので、ログを調べて、関連するものがないかどうかを確認できます。

```
2020-10-16T10:28:48.717+0200 WARN  endpoint:523 - [com.couchbase.endpoint][EndpointConnectionFailedEvent][2691us] Connect attempt 7 failed because of AnnotatedConnectException: finishConnect(..) failed: Connection refused: /127.0.0.1:11210 {"bucket":"travel-sample","channelId":"5B36B0DB00000001/000000006C7CDB48","circuitBreaker":"DISABLED","coreId":"0x5b36b0db00000001","local":"127.0.0.1:49895","remote":"127.0.0.1:11210","type":"KV"}
com.couchbase.client.core.deps.io.netty.channel.AbstractChannel$AnnotatedConnectException: finishConnect(..) failed: Connection refused: /127.0.0.1:11210
Caused by: java.net.ConnectException: finishConnect(..) failed: Connection refused
	at com.couchbase.client.core.deps.io.netty.channel.unix.Errors.throwConnectException(Errors.java:124)
//	... (rest of stack omitted) ...
```

サーバーに接続しようとしたようですが、接続が拒否されました。次のステップは、サーバー側でソケットの問題をトリアージすることになります。

### リクエストのキャンセル

`RequestCanceledException`は、次の場合にスローされます。

- `RetryStrategy`により、再試行されるべきではないと判断された（`RetryStrategy`については、別稿で解説します）
- あまりにも多くのリクエストが再試行されるのを待ってスタックしている（バックプレッシャーのシグナル）。
- 操作実行の際に、SDKはすでにシャットダウンされていた

潜在的に他の理由もありますが、それがどこから発生したかは、それが伝える情報ほど重要ではありません。`RequestCanceledException`を取得した場合は、SDKが操作をそれ以上再試行できず、タイムアウト間隔の前に終了したことを意味します。

透過的な再試行は、`RetryStrategy`がカスタマイズされており、再試行された操作がデータ損失につながる可能性のあるサーバー上の副作用を実行していないことが確実な場合にのみ実行される必要があります。ほとんどの場合、何が悪かったのかを把握するために、事後にログを検査する必要があります。

事後のデバッグを支援するために、に`RequestCanceledException`は、`ErrorContext`を持ち、上記のタイムアウトセクションで説明したものと非常によく似た内容が含まれています。

### 冪等(Idempotent)要求と非冪等(Non-Idempotent)要求

SDKを流れる操作は、冪等または非冪等のいずれかです。操作が冪等である場合、結果が変更される心配なしに、サーバーに複数回要求を送信できます。

この区別は、SDKがサーバーに操作を送信し、応答を受信する前にソケットが閉じられる場合に重要です。冪等でない場合、SDKは、サーバー側で副作用が発生したかどうかを確認できず、キャンセルする必要があります。この場合、アプリケーションは`RequestCanceledException`を受け取ります。

冪等である場合、SDKは最終的に成功する可能性があるため、透過的に操作を再試行します。リクエストの種類によっては、別のノードに送信したり、操作がタイムアウトする前にソケット接続を再確立したりできる場合があります。

ネットワークに送信する前、またはSDKが応答を受信した後の操作を再試行する必要がある場合、冪等性は重要ではなく、他の要因が考慮されます。次の図は、要求のライフサイクルで冪等性が重要な場合を示しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176567/ca19e4c0-f20d-7937-4bfe-966ab2a72d89.png)


SDKは、偶発的なデータ損失を回避したいので、どの操作が冪等であると見なされるかについて非常に保守的です。変更操作が誤って2回適用されたが、その間に別のアプリケーションサーバーが変更した状況を想像してみてください。その変更は、潜在的に回復する機会なしに失われます。

Couchbase Serverの次の操作は、冪等と見なされます。

- クラスター：　`search`、`ping`、`waitUntilReady`
- バケット： `view`、`ping`、`waitUntilReady`
- コレクション： `get`、`lookupIn`、`getAnyReplica`、`getAllReplicas`、`exists`

上記には、`query`と`analyticsQuery`コマンドの両方が含まれていません。
これは、SDKはクエリで行われている内容については関知しない(クエリ文字列を検査して、変更操作が含まれているかどうかを判別するようなことはしない)ためです。

クエリをリクエストする際に、データに対する更新を行っておらず、取得のみを行っていることを、オプションとしてクライアントに伝え、冪等性の保証による再試行の恩恵を受けることができます。

```java
QueryResult queryResult = cluster.query("SELECT * FROM `travel-sample`", queryOptions().readonly(true));

AnalyticsResult analyticsResult = cluster.analyticsQuery("SELECT * FROM `travel-sample`.inventory.airport",
    analyticsOptions().readonly(true));
```


## 参考情報

https://docs.couchbase.com/java-sdk/current/howtos/error-handling.html