LoginSignup
2
1

More than 3 years have passed since last update.

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

Posted at

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 で例外を拾ってあげる必要がある。

    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 を見ると、全ての class が BadStatus class を継承していることがわかる。

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

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 の中身まで見にいったりしなければならない状況なのがちょっと辛い。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1