go-grpc-middleware/recoveryを利用してpanicから回復する

goでgRPCサーバ実装する際に、panic起きた際に落ちずに回復させる方法です。 go-grpc-middlewaregrpc_recoveryを利用します。

事前準備

protoとgo-grpc-middlewareのインストール

$ go get github.com/golang/protobuf/proto
$ go get github.com/grpc-ecosystem/go-grpc-middleware

grpc_recoveryを使わない場合

こちらをご利用ください。

$ git clone https://github.com/ybalexdp/sample-grpc
$ cd schema
$ protoc --go_out=plugins=grpc:../pb sample.proto

panicを起こす処理を追記

service/service_sample.go

func (s *SampleService) GetSample(ctx context.Context, message *pb.GetSampleRequest) (*pb.SampleResponse, error) {
    if message.Name == "panic" {
        panic("failed")
        return nil, grpc.Errorf(codes.Internal, "Unexpected error")
    }
    return &pb.SampleResponse{
        Message: "Hello :" + message.Name,
    }, nil
}

サーバ起動

$ cd ..
$ go run main.go

panicを起こしてみる

$ evans --path schema --port 2007 sample.proto

  ______
 |  ____|
 | |__    __   __   __ _   _ __    ___
 |  __|   \ \ / /  / _. | | '_ \  / __|
 | |____   \ V /  | (_| | | | | | \__ \
 |______|   \_/    \__,_| |_| |_| |___/

 more expressive universal gRPC client


pb.Sample@127.0.0.1:2007> call GetSample
name (TYPE_STRING) => panic
command call: failed to send a request: rpc error: code = Unavailable desc = transport is closing

エラーです。
その後再度API叩いてみると、サーバが落ちていることが確認できます。

pb.Sample@127.0.0.1:2007> call GetSample
name (TYPE_STRING) => test
command call: failed to send a request: rpc error: code = Unavailable desc = all SubConns are in TransientFailure, latest connection error: connection error: desc = "transport: Error while dialing dial tcp 127.0.0.1:2007: connect: connection refused"

サーバ側

下記のように落ちています。

$ go run main.go                                                                                                                                        (git)-[master]
panic: failed

goroutine 4 [running]:
github.com/ybalexdp/sample-grpc/service.(*SampleService).GetSample(0x1843840, 0x15263a0, 0xc0001681b0, 0xc0001681e0, 0x1843840, 0xc000168150, 0x142b5e0)
    /Users/ybalexdp/go/src/github.com/ybalexdp/sample-grpc/service/service_sample.go:16 +0xf0
github.com/ybalexdp/sample-grpc/pb._Sample_GetSample_Handler(0x14224a0, 0x1843840, 0x15263a0, 0xc0001681b0, 0xc000164300, 0x0, 0x0, 0x0, 0xc000026128, 0x7)
    /Users/ybalexdp/go/src/github.com/ybalexdp/sample-grpc/pb/sample.pb.go:180 +0x23e
google.golang.org/grpc.(*Server).processUnaryRPC(0xc000084900, 0x1528160, 0xc000084d80, 0xc00018e000, 0xc0000a3e30, 0x1816e90, 0x0, 0x0, 0x0)
    /Users/ybalexdp/go/src/google.golang.org/grpc/server.go:1007 +0x485
google.golang.org/grpc.(*Server).handleStream(0xc000084900, 0x1528160, 0xc000084d80, 0xc00018e000, 0x0)
    /Users/ybalexdp/go/src/google.golang.org/grpc/server.go:1287 +0xdf9
google.golang.org/grpc.(*Server).serveStreams.func1.1(0xc0000a8890, 0xc000084900, 0x1528160, 0xc000084d80, 0xc00018e000)
    /Users/ybalexdp/go/src/google.golang.org/grpc/server.go:722 +0x9f
created by google.golang.org/grpc.(*Server).serveStreams.func1
    /Users/ybalexdp/go/src/google.golang.org/grpc/server.go:720 +0xa1
exit status 2

grpc_recoveryを使ってサーバを落とさせない

ソースもあげておきます。
go run main.goでgRPCのサーバが立ち上がります。

grpc_recoveryを実装する

main.go

package main

import (
    "fmt"
    "log"
    "net"

    grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
    grpc_recovery "github.com/grpc-ecosystem/go-grpc-middleware/recovery"
    "github.com/ybalexdp/sample-grpc_recovery/pb"
    "github.com/ybalexdp/sample-grpc_recovery/service"

    "google.golang.org/grpc"
    "google.golang.org/grpc/codes"
)

func main() {

    opts := []grpc_recovery.Option{
        grpc_recovery.WithRecoveryHandler(recoveryFunc),
    }

    server := grpc.NewServer(
        grpc_middleware.WithUnaryServerChain(
            grpc_recovery.UnaryServerInterceptor(opts...),
        ),
    )

    listenPort, err := net.Listen("tcp", ":2007")
    if err != nil {
        log.Fatalln(err)
    }
    sampleService := &service.SampleService{}
    pb.RegisterSampleServer(server, sampleService)
    server.Serve(listenPort)
}

func recoveryFunc(p interface{}) error {
    fmt.Printf("p: %+v\n", p)
    return grpc.Errorf(codes.Internal, "Unexpected error")
}

サーバ起動

$ cd ..
$ go run main.go

panicを起こした後、再度APIを実行する

pb.Sample@127.0.0.1:2007> call GetSample
name (TYPE_STRING) => panic
command call: failed to send a request: rpc error: code = Internal desc = Unexpected error

pb.Sample@127.0.0.1:2007> call GetSample
name (TYPE_STRING) => ybalexdp
{
  "message": "Hello :ybalexdp"
}

サーバが落ちていないことが確認できました。

サーバ側

実装したエラー出力のみで、サーバは落ちていないことが確認できます。

$ go run main.go
p: failed

参考

GoDoc

go-grpc-middlewareを一通り試してみる - Qiita

GoのgRPC ServerのInterceptor(recovery/auth/zap/prometheus) - sambaiz-net