Carpe Diem

備忘録

gRPC Unary Interceptorを複数設定した時の実行順序

概要

以前

christina04.hatenablog.com

にてgRPCのUnary Interceptorの基本的な使い方を説明しました。
このInterceptorを複数設定したい時に使われるのがgo-grpc-middlewareの出している

です。READMEにありますが、以下のように複数のInteceptorをセットできます。

import "github.com/grpc-ecosystem/go-grpc-middleware"

myServer := grpc.NewServer(
    grpc.StreamInterceptor(grpc_middleware.ChainStreamServer(
        grpc_ctxtags.StreamServerInterceptor(),
        grpc_opentracing.StreamServerInterceptor(),
        grpc_prometheus.StreamServerInterceptor,
        grpc_zap.StreamServerInterceptor(zapLogger),
        grpc_auth.StreamServerInterceptor(myAuthFunction),
        grpc_recovery.StreamServerInterceptor(),
    )),
    grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(
        grpc_ctxtags.UnaryServerInterceptor(),
        grpc_opentracing.UnaryServerInterceptor(),
        grpc_prometheus.UnaryServerInterceptor,
        grpc_zap.UnaryServerInterceptor(zapLogger),
        grpc_auth.UnaryServerInterceptor(myAuthFunction),
        grpc_recovery.UnaryServerInterceptor(),
    )),
)

ここで実行順序について疑問に思ったので確認してみました。

環境

  • go 1.12.7
  • go-grpc-middleware master

実行順序

それではUnaryServerInterceptorの挙動を確認してみます。

Interceptorが1つの場合

前の記事でも紹介したように1つの場合は以下です。

func LogBodyInterceptor() grpc.UnaryServerInterceptor {
    return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
        // 1. request bodyをprint
        fmt.Printf("%+v\n", req)

        // 2. リクエストされたgrpcメソッドを実行
        resp, err := handler(ctx, req)
        if err != nil {
            return nil, err
        }

        // 3. response bodyをprint
        fmt.Printf("%+v\n", resp)

        return res, nil
    }
}

1, 2, 3と順に実行されます。

重要なのはhandlerの後に書けばgrpcメソッドを実行した後に処理される、という点です。

複数にした場合

では複数にした場合どうなるでしょうか?

Interceptor

func ServerInterceptor(prefix string) grpc.UnaryServerInterceptor {
    return func(ctx context.Context,
        req interface{},
        info *grpc.UnaryServerInfo,
        handler grpc.UnaryHandler,
    ) (interface{}, error) {
        fmt.Println(prefix, "before handler")
        // call handler
        resp, err := handler(ctx, req)
        fmt.Println(prefix, "after handler")

        return resp, err
    }
}

このような形のInterceptorを用意します。

セット

サーバ側では以下のようにセットします。
分かりやすいようにセットする順にserver-interN-というprefixを渡してます。

   s := grpc.NewServer(
        grpc.UnaryInterceptor(interceptor.ServerInterceptor("server-inter1-")),
        grpc.UnaryInterceptor(interceptor.ServerInterceptor("server-inter2-")),
        grpc.UnaryInterceptor(interceptor.ServerInterceptor("server-inter3-")),
    )

結果

結果は以下です。

server-inter1- before handler
server-inter2- before handler
server-inter3- before handler
handler
server-inter3- after handler
server-inter2- after handler
server-inter1- after handler

ポイント

  • handlerの前に書いたらChainした順に実行される
  • handlerの後に書いたらdeferのように最後から順に実行される

handler前の処理は想定通りだと思いますが、handler後の処理はdeferのように逆順になるところが注意です。

handlerの後に実行する処理としてよくあるのは?

  • アクセスlogを出力
  • errorハンドリングの統合(通常のエラーをgRPCエラーへ変換)
  • panicのハンドリング

などがあります。
今回の順序を把握していないとハマるのでご注意ください。

Client Interceptorは?

UnaryClientInterceptorが複数のケースも確認してみます。

Interceptor

以下のようにinvoker()の前後にログを入れます。

func ClientInterceptor(prefix string) grpc.UnaryClientInterceptor {
    return func(
        ctx context.Context,
        method string,
        req interface{},
        reply interface{},
        cc *grpc.ClientConn,
        invoker grpc.UnaryInvoker,
        opts ...grpc.CallOption,
    ) error {
        fmt.Println(prefix, "before invoker")
        err := invoker(ctx, method, req, reply, cc, opts...)
        fmt.Println(prefix, "after invoker")
        return err
    }
}

Client実行側

分かりやすいようにセットする順にclient-interN-というprefixを渡してます。

func run() {
    conn, err := grpc.Dial(
        address,
        grpc.WithInsecure(),
        grpc.WithUnaryInterceptor(interceptor.ClientInterceptor("client-inter1-")),
        grpc.WithUnaryInterceptor(interceptor.ClientInterceptor("client-inter2-")),
        grpc.WithUnaryInterceptor(interceptor.ClientInterceptor("client-inter3-")),
    )

結果

client-inter1- before invoker
client-inter2- before invoker
client-inter3- before invoker
client-inter3- after invoker
client-inter2- after invoker
client-inter1- after invoker
2019/07/11 19:44:59 Reply:  Hello alice

ポイント

  • invokerの前に書いたらChainした順に実行される
  • invokerの後に書いたらdeferのように最後から順に実行される

こちらもServerの時同様の挙動ですね。

サンプルコード

今回検証したコードはこちらです

github.com

まとめ

Server/ClientのInterceptorを複数設定した時の挙動について検証しました。