GrufからGritzへの移行

既存Controllerの継承先をGritz::Compat::Gruf::Controllerに変え、まずworkers 0でRPCを確認する。 互換レイヤはGruf 2.22.0のController・Interceptor APIのうち、下表の範囲を扱う。 Gruf本体への実行時依存や、グローバルなGruf定数の置き換えは行わない。

既存コード 移行後
Gruf::Controllers::Baseの継承 Gritz::Compat::Gruf::Controllerを継承
bind Service 同じ定義を維持し、設定でControllerを登録
request.message / request.messages Unaryの値、client streamingのmessage.call { ... }、Enumerableによるbidiを維持
server/bidiの返却Enumerator 互換Controller内で列挙して送信。Middlewareも列挙終了まで有効
request.metadata / active_call.metadata 受信メタデータを参照
request.context RPC内で共有。[]、[]=、fetch、key?、merge!はSymbol/Stringキーを共通化
fail!(code, app_code, message, metadata) 引数を維持。gRPC statusとerror-internal-binのJSONを返す
add_field_error / has_field_errors? / set_debug_info アプリケーションが明示したエラー情報を維持
ServerInterceptor#callのyield Gritz::Compat::Gruf.interceptorでMiddlewareへ接続

request.contextはActiveSupportのHashWithIndifferentAccess全体を再実装してはいない。 独自のserializer、Grufのグローバル設定・logger、Controllerの独自call/process_action、C-core固有のactive_call操作は個別に移す。 トレーラ中のアプリケーションコードを読む既存クライアントは維持できるが、Gritzの標準リッチエラー形式とは別のJSON形式である。

Controllerと起動設定を移す

アプリケーションのGemfileへgritzを追加する。Railsの起動・autoload・Executor連携にはgritz-railsも追加する。 Controllerでは継承先だけを変更できる。

require "gritz/compat/gruf"

class ProductsController < Gritz::Compat::Gruf::Controller
  bind Rpc::Products::Service

  def get_product
    product = Product.find(request.message.id)
    Rpc::GetProductResp.new(product: product.to_proto)
  rescue ActiveRecord::RecordNotFound
    fail!(:not_found, :product_not_found, "Product not found")
  end
end

config/gritz.rbで生成済みprotobufとControllerを読み込み、register_controller ProductsControllerを設定する。 Railsのautoloadを使う場合はRails統合の起動設定に従う。Grufのbindが行うグローバルサービス登録は引き継がない。 最初は次のようなシングルプロセス設定にする。

workers 0
bind "0.0.0.0:9001"
register_controller ProductsController

既存のリクエストを実際に送り、レスポンス、順序、ステータス、エラートレーラを比較してからfork対応へ進む。 Grufの3番目のfail!引数はメッセージである。Gritz標準Controllerのfail!(code, message, ...)へ置き換える際は、位置引数をそのまま移さない。

Interceptorを明示的に接続する

独自Interceptorの継承先をGritz::Compat::Gruf::ServerInterceptorへ変更する。 initialize(request, error, options = {})とcall内のyieldを維持し、設定でアダプタを登録する。

class TokenAuthentication < Gritz::Compat::Gruf::ServerInterceptor
  def call
    fail!(:unauthenticated, :invalid_token, "Invalid token") unless request.["token"] == options.fetch(:token)
    yield
  end
end

middleware do |stack|
  stack.use(Gritz::Compat::Gruf.interceptor(TokenAuthentication), token: ENV.fetch("RPC_TOKEN"))
end

登録順に外側から実行され、同じRPCのRequest・エラー状態を共有する。 InterceptorがUnaryリクエストを先に読んでも、互換Controllerと標準Gritz::Controllerのどちらでも二重に消費・計上しない。 Gruf組み込みInterceptorは自動では登録されない。必要な実装を確認して自分のアプリケーションへ移すか、Gritz/Railsの標準機能へ置き換える。 認証設定を抜いたまま起動せず、不正な認証情報を拒否するテストも移す。

静的な設定だけを変換する

gritz-migrate-grufは入力を実行せず、Ruby標準のRipperで構文を解析する。 単一のGruf.configure do |c| ... endまたはGruf.configure { |c| ... }に含まれる、次のリテラル代入を変換する。

Gruf設定 Gritz設定
server_binding_url bind
rpc_server_options[:pool_size] threads
rpc_server_options[:max_waiting_requests] max_waiting_requests
use_ssl = true、ssl_crt_file、ssl_key_file tls({ cert: ..., key: ... })
server_argsのgrpc.max_receive_message_length / grpc.max_send_message_length / grpc.max_metadata_size 対応するメッセージ・メタデータ上限
grpc.max_connection_age_ms / grpc.max_connection_age_grace_ms / grpc.keepalive_time_ms 対応する秒単位の設定

rpc_server_optionsはHash全体への代入を使う。たとえばc.rpc_server_options = { pool_size: 8 }である。 出力にはworkers 0とController登録の確認コメントが入る。 明示されなかった設定はGritzの既定値になるため、元のGrufの既定値や環境変数による設定も確認する。 TLSファイルの内容は読み込まない。生成後にファイルの存在・権限と、証明書・秘密鍵の対応を確認する。

bundle exec gritz-migrate-gruf gruf-literals.rb
bundle exec gritz-migrate-gruf gruf-literals.rb --output config/gritz.rb

指定先が存在すれば上書きを拒否する。未対応の項目や式があれば終了コード1で停止し、設定を黙って省略しない。 requireなどblock外の実行コード、ENV.fetch、メソッド呼び出し、文字列の補間・エスケープ、Proc、条件分岐、Hashの展開、重複設定は対象外である。 default_client_hostや認証・serializer・Interceptor・poll_periodなどは手作業で移す。 これらを含む実際のinitializerは変換エラーになる。元の設定を確認して静的な項目を専用ファイルへ抜き出し、残る項目を移行先で設定する。

forkを有効にする前に比較する

  1. 4種類のRPCの値・ストリーム順序を、既存クライアントで比較する。
  2. 認証の成功・拒否、NotFound、入力エラー、アプリケーションコードのトレーラを比較する。
  3. RailsのDB接続、外向きクライアント、ThreadをMasterで作っていないことを確認する。protobufのロードとクライアント定義はMasterで行える。
  4. bundle exec gritz check -C config/gritz.rbを実行し、fork前に作られたリソースをWorker側の初期化へ移す。
  5. 固定ポートとworkers Nに変更し、起動、終了、Worker交換時の挙動を確認する。

確認後、互換APIを必要な箇所からGritz標準Controller・Middlewareへ置き換える。 明示したset_debug_infoは互換JSONに含まれるため、外部に返してよい情報かも確認する。

公式サンプルで確認した範囲

Gruf READMEのDemo Rails Appが紹介するbigcommerce/gruf-demoを使った。 Gruf 2.22.0とDemoの固定SHA、元ファイルごとのSHA256、MITライセンスはNativeの検証fixtureに保存している。 ProductsControllerは継承先1行だけを変更し、4種類の実RPC、NotFound、Basic認証、ActiveRecordモデルのvalidationを元のGrufと比較する。 比較ではSQLiteを使い、元アプリケーション全体のRails起動・MySQL・画面までは扱わない。 この検証を自分のアプリケーションの移行テストへ追加し、固有のInterceptorやエラー形式も確認する。