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 に返してみる。
func (s *Server) set_name(name string) {
st := status.New(codes.InvalidArgument, "invalid username")
return nil, st.Err()
}
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 を追加してみた。
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()
}
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 の中身まで見にいったりしなければならない状況なのがちょっと辛い。