2019-03-04

gRPC × Rubyのチュートリアルをカスタムしてやってみた

gRPC公式のRuby版チュートリアルを参考に、手元で試してみた記録。
gRPCで開発をするときの全体感みたいなものをつかめたらいいなぁくらいのところからスタート。

https://grpc.io/docs/tutorials/basic/ruby.html

Protocol Buffersでサービスの定義を書く

まず一番最初にやることは、Protocl Buffersを用いてサービスの定義を書くことだ。チュートリアルのコードをそのまま使うのはおもしろくないので、自分で書いてみることにしよう。
ここでは、Darktree という拙作のフラッシュカードアプリケーションを想定し、Card というmessageをやりとりする定義を書く。

// proto/card.proto
syntax = "proto3";

message Card {
    int64 card_id = 1;
    string front = 2;
    string back = 3;
    enum Status {
        OK = 0;
        NG = 1;
    }
    Status status = 4;
}

message GetCardRequest {
    int64 card_id = 1;
}

message GetCardResponse {
    Card card = 1;
}

service Darktree {
    rpc GetCard(GetCardRequest) returns (GetCardResponse) {
    }
}

Card の属性のイメージは次の通り。

  • card_id : 連番の数値。Railsアプリケーションの id をイメージ
  • front : フラッシュカードの表側に書かれるデータ(例:富士山の標高は?
  • back : フラッシュカードの裏側に書かれるデータ(例:3776m
  • status : フラッシュカードの学習状況。OK は習得済み、NGは未習得を表す

そして Card を取得する GetCard というrpcを定義。
引数は GetCardRequest として中身はcard_idのみ、返り値は GetCardResponse としてCardメッセージ1件を含むものとした。

Protocol BuffersからRubyコードを生成する

Protocl Buffersで作成したサービス定義から、実際のRubyコードを生成する。
grpc_tools_ruby_protoc というコマンドが必要となるので、bundlerでこれをインストールする。

bundle init
echo 'gem "grpc"' >> Gemfile
echo 'gem "grpc-tools"' >> Gemfile
bundle install --path vendor/bundle

また、生成するコードを配置するディレクトリを事前に作っておく。 ここでは lib という名前のディレクトリにしておこう。

mkdir lib

そして、生成コマンドを実行する。

bundle exec grpc_tools_ruby_protoc --ruby_out=./lib --grpc_out=./lib ./proto/card.proto

lib 以下にコードが生成された。

$ tree lib 
lib
└── proto
    ├── card_pb.rb
    └── card_services_pb.rb

1 directory, 2 files

ちなみにここまでで、作業ディレクトリ全体は次の状態になっている。

$ tree -L 3
.
├── Gemfile
├── Gemfile.lock
├── lib
│   └── proto
│       ├── card_pb.rb
│       └── card_services_pb.rb
├── proto
│   └── card.proto
└── vendor
    └── bundle
        └── ruby  (省略)

サーバの実装を書く

コードは生成され、そのインターフェースはProtocol Buffersで書いた通りだが、実際のサーバの実装は存在しない。
なので、次はインターフェースを満たすサーバの実装を行う必要がある。server.rb という名前のファイルを新しく作り、そこに書いていこう。

# server.rb
$LOAD_PATH << File.expand_path("./lib")

require 'proto/card_services_pb'

class ServerImpl < Darktree::Service
  def get_card(req, _call)
    card = Card.new(card_id: req.card_id, front: '富士山の標高は?', back: '3776m', status: 'OK')
    GetCardResponse.new(card: card)
  end
end

server = GRPC::RpcServer.new
server.add_http2_port('0.0.0.0:50051', :this_port_is_insecure)
server.handle(ServerImpl.new)
server.run_till_terminated_or_interrupted([1, 'int', 'SIGQUIT'])

基本的にチュートリアルのコードをベースに書いているので詳細は省略するが、get_card というメソッドを定義して、インターフェース通りに GetCardResponse オブジェクトを返すようにしている。ファイルの下部は、サーバを起動して待ち受けるためのコードとなっている。

サーバは次のようにして起動し待受け状態にして、次に進もう。

bundle exec ruby server.rb

クライアントからサーバへrpcリクエストを送る

gRPCサーバを起動するところまで終わったので、あとはクライアントからrpcリクエストを投げるだけとなった。
client.rb という名前の新しいファイルを作り、そこに次のようなコードを書く。

# client.rb
$LOAD_PATH << File.expand_path("./lib")

require 'proto/card_services_pb'

client = Darktree::Stub.new('localhost:50051', :this_channel_is_insecure)
resp = client.get_card(GetCardRequest.new(card_id: 1))

pp resp

Darktree::Stub.new でクライアントオブジェクトを作り、#get_card メソッドをコールし、結果を pp するだけのコードだ。実行してみよう。

$ bundle exec ruby client.rb
<GetCardResponse: card: <Card: card_id: 1, front: "富士山の標高は?", back: "3776m", status: :OK>>

期待通り、GetCardReponse オブジェクトを取得できた!

※ 作業ディレクトリは最終的に次の状態になった。

$ tree -L 3
.
├── Gemfile
├── Gemfile.lock
├── client.rb
├── lib
│   └── proto
│       ├── card_pb.rb
│       └── card_services_pb.rb
├── proto
│   └── card.proto
├── server.rb
└── vendor
    └── bundle
        └── ruby

6 directories, 7 files

番外編:インターフェース定義に反するレスポンスを返すとどうなる?

おまけとして、定義したインターフェースに反するレスポンスをサーバ側が返した場合にどのようなことが起きるか試してみた。
server.rb をいじって、レスポンスに hoge という属性を追加してみる。

# 省略

class ServerImpl < Darktree::Service
  def get_card(req, _call)
    card = Card.new(card_id: req.card_id, front: '富士山の標高は?', back: '3776m', status: 'OK')
    GetCardResponse.new(card: card, hoge: 'hoge') # ★★ `hoge` という属性を勝手に追加 ★★
  end
end

# 省略

サーバを起動し..

bundle exec ruby server.rb

クライアントコードを実行する

$ bundle exec ruby client.rb
Traceback (most recent call last):

        # 省略

/xxx/vendor/bundle/ruby/2.6.0/gems/grpc-1.19.0-universal-darwin/src/ruby/lib/grpc/generic/active_call.rb:31:in `check_status': 2:ArgumentError: Unknown field name 'hoge' in initialization map entry. (GRPC::Unknown)

ArgumentError: Unknown field name 'hoge' in initialization map entry. (GRPC::Unknown) という例外が発生した。

続いて、Cardstatus を OKでもNGでもない値にしてクライアントを実行してみる。

# 省略

class ServerImpl < Darktree::Service
  def get_card(req, _call)
    card = Card.new(card_id: req.card_id, front: '富士山の標高は?', back: '3776m', status: 'HOGE') # ★★ statusをHOGEにする ★★
    GetCardResponse.new(card: card)
  end
end

# 省略
$ bundle exec ruby client.rb
Traceback (most recent call last):

        # 省略

/xxx/vendor/bundle/ruby/2.6.0/gems/grpc-1.19.0-universal-darwin/src/ruby/lib/grpc/generic/active_call.rb:31:in `check_status': 2:RangeError: Unknown symbol value for enum field. (GRPC::Unknown)

RangeError: Unknown symbol value for enum field. (GRPC::Unknown) という例外が発生した。

それぞれ実行時に例外が発生してくれるので、 テストを書いておけば 実装とインターフェースがずれてしまった場合に気がつけそうだ。