Ruby で gRPC のエラーハンドリング をする際のTips

Ruby で gRPC クライアントを実装する際の、エラーハンドリングのTipsをまとめる

# 前提
- Ruby 2.3.8
- Rails 5.0.7.2
- grpc gem 1.2.8

# tl;dr
- gRPC コールに対するエラーレスポンスは `GRPC::BadStatus` 型の例外なので、gRPC コールする際は必ず resuce する
- 捕捉した Exception の詳細情報を取得したい場合、 `to_rpc_status` メソッドで `Google::Rpc::Status` 型にキャストする
- gRPC サーバーから error details が返ってきた場合、Any 型で返ってくるので、適切な型に unpack する必要がある
- gem のエラークラス https://github.com/grpc/grpc/blob/master/src/ruby/lib/grpc/errors.rb

# まずはエラーを捕捉する
gRPC コールのレスポンスは基本的には以下の Code のどれかが返ってくる。

```
enum Code {
  OK = 0;
  CANCELLED = 1;
  UNKNOWN = 2;
  INVALID_ARGUMENT = 3;
  DEADLINE_EXCEEDED = 4;
  NOT_FOUND = 5;
  ALREADY_EXISTS = 6;
  PERMISSION_DENIED = 7;
  UNAUTHENTICATED = 16;
  RESOURCE_EXHAUSTED = 8;
  FAILED_PRECONDITION = 9;
  ABORTED = 10;
  OUT_OF_RANGE = 11;
  UNIMPLEMENTED = 12;
  INTERNAL = 13;
  UNAVAILABLE = 14;
  DATA_LOSS = 15;
}
```
https://github.com/googleapis/googleapis/blob/master/google/rpc/code.proto
gRPC サーバーの実装としては正常系は Code OK(0) を返し、エラーであれば OK 以外の Code(1~15) を返すよう作るかと思う。

エラーが返ってきた場合、grpc gem 側で勝手に exception を発生させるらしく、rescue しないとそこでプログラムがストップしてしまう。
なので、以下のように rescue で例外を拾ってあげる必要がある。

```rb
    begin
      stub = HogeProto::HogeService::Stub.new("localhost:50051", :this_channel_is_insecure)
      res = stub.get_hoge
    rescue GRPC::BadStatus => ex
      res = "error"
    rescue ex
      res = "unexpected error"
    end
```

gem で定義されている [エラー class](https://github.com/grpc/grpc/blob/master/src/ruby/lib/grpc/errors.rb) を見ると、全ての class が `BadStatus` class を継承していることがわかる。

実際に、発生した Exception の継承ツリーを見ると

```rb
p ex.class.ancestors
# =>[GRPC::InvalidArgument, GRPC::BadStatus, GRPC::Core::StatusCodes, StandardError, Exception...
```
となっており、基本的に `GRPC::BadStatus` で全ての Exception を捕捉できる。

もっと詳細に条件分けして処理したい場合は `GRPC::InvalidArgument` のような単位で拾うこともできる。

# 捕捉した Exception から情報を取り出す
捕捉した Exception から情報を取り出してみよう。
`BadStatus` インスタンスには4つのフィールドがある。
https://github.com/grpc/grpc/blob/d48d39c4324f06a6da24bb4f67e8ef21166ba65b/src/ruby/lib/grpc/errors.rb#L49-L52

例えば、Go で実装した gRPC サーバーから以下のようにエラーを作成して ruby に返してみる。

```server.go
  func (s *Server) set_name(name string) {
    st := status.New(codes.InvalidArgument, "invalid username")
    return nil, st.Err()
  }
```

```client.rb
    begin
      stub = HogeProto::HogeService::Stub.new("localhost:50051", :this_channel_is_insecure)
      res = stub.set_name("hoge1")
    rescue GRPC::BadStatus => ex
      p ex.code # => 3
      p ex.message # => "3:invalid username"
      p ex.details # => "invalid username"
      res = "error"
    rescue ex
      res = "unexpected error"
    end
```

Status Code `3` と `"invalid username"` というメッセージが取得できた

# error details を取得したい場合は、 to_rpc_status メソッドを使う
上記までで、Status Code とエラーメッセージが取得できた。
しかし、実際に作り込む場合、エラーの詳細を渡すために error details を使うことも多いと思う。
https://christina04.hatenablog.com/entry/grpc-error-details
https://grpc.io/docs/guides/error/#richer-error-model

もし、 error details 含めた詳細な情報が欲しければ、 `to_rpc_status` メソッドを使うことでより詳細な情報を取得することができる。
`to_rpc_status` の実装は以下になるが、これを使うことで、`Google::Rpc::Status` 型にキャストでき、trailer メタデータを含めた詳細な情報を取り出すことができる。
https://github.com/grpc/grpc/blob/d48d39c4324f06a6da24bb4f67e8ef21166ba65b/src/ruby/lib/grpc/errors.rb#L63-L75

先ほどの実装例に、error details を追加してみた。

```server.go
  func (s *Server) set_name(name string) {
    st := status.New(codes.InvalidArgument, "invalid username")

    // error details の情報を作ってセットする
    desc := "The username must only contain alphanumeric characters"
    v := &errdetails.BadRequest_FieldViolation{
		Field:       "username",
		Description: desc,
    }
    br := &errdetails.BadRequest{}
    br.FieldViolations = append(br.FieldViolations, v)
    st, _ = st.WithDetails(br)
    
    return nil, st.Err()
  }
```

```client.rb
require 'google/rpc/error_details_pb'

    begin
      stub = HogeProto::HogeService::Stub.new("localhost:50051", :this_channel_is_insecure)
      res = stub.set_name("hoge1")
    rescue GRPC::BadStatus => ex
      p ex.class # => GRPC::InvalidArgument
      p ex.to_rpc_status.class # => Google::Rpc::Status
      p ex.to_rpc_status # => <Google::Rpc::Status: code: 3, message: "invalid username", details: [<Google::Protobuf::Any: type_url: "type.googleapis.com/google.rpc.BadRequest", value: "\nC\n\tusername\x126The username must only contain alphanumeric characters">]>

      ex.to_rpc_status.details.each do |detail|
        p detail.type_url # => "type.googleapis.com/google.rpc.BadRequest"
        p detail.unpack(Google::Rpc::BadRequest) # => <Google::Rpc::BadRequest: field_violations: [<Google::Rpc::BadRequest::FieldViolation: field: "username", description: "The username must only contain alphanumeric characters">]>
      end
      res = "error"
    rescue ex
      res = "unexpected error"
    end
```

注目すべきは details `details: [<Google::Protobuf::Any: type_url: "type.googleapis.com/google.rpc.BadRequest", value: "\nC\n\tusername2\x126The username must only contain alphanumeric characters">]` の部分。
これが、trailer として送られてきた error details のデータであり、なにやら `Google::Protobuf::Any` 型のデータと value というデータがある。
error details は複数設定でき、配列で返ってくるので、取り出すには each で回してあげる必要がある。
`type_url` というのは error details の型の定義場所が格納されていて、Google がデフォルトで用意した型を使用していれば、`"type.googleapis.com/google.rpc.BadRequest"` となる。もちろん独自で定義した型を error details としてセットすることもできるので、その場合は自分がprotoファイルで定義した場所が格納される。

また、返ってきたインスタンスは `Google::Protobuf::Any` 型となっていて、このままだとデータがシリアライズされた状態でうまく取り出せない。そこで、使用するのが `unpack` メソッド。
`type_url` で型を判定し、`unpack` で戻したい型にキャストしてあげることで、ようやく、 error details を取り出すことができた。
ちなみに、`Google::Rpc::BadRequest` の型定義を参照するには、`google/rpc/error_details_pb` をインポートしておく必要がある点に注意。

# 所感
error details を取り出すのに結構手間取った。
https://grpc.io/docs/languages/ruby/quickstart/ のサンプルコードにはここまで詳しく載ってないので、結局はいちいち gem の中身まで見にいったりしなければならない状況なのがちょっと辛い。