使用WithBlock()选项创建到异步启动的gRPC服务器的gRPC客户端连接会无限期地阻塞吗?

时间:2020-06-30 18:31:33

标签: go grpc grpc-go

我想编写一个单元测试,其中运行一个短暂的gRPC服务器,该服务器在测试中的单独Goroutine中启动,并在测试运行后停止。为此,我尝试将本教程(https://grpc.io/docs/languages/go/quickstart/)中的“ Hello,world”示例改编为一个示例,在该示例中,只有一个单独的main.go而不是服务器和客户端测试功能,该功能异步启动服务器,随后使用grpc.WithBlock()选项建立客户端连接。

我已将简化示例放在此存储库中,https://github.com/kurtpeek/grpc-helloworld;在这里是main_test.go

package main

import (
    "context"
    "fmt"
    "log"
    "net"
    "testing"
    "time"

    "github.com/stretchr/testify/require"
    "google.golang.org/grpc"
    "google.golang.org/grpc/examples/helloworld/helloworld"
)

const (
    port = ":50051"
)

type server struct {
    helloworld.UnimplementedGreeterServer
}

func (s *server) SayHello(ctx context.Context, in *helloworld.HelloRequest) (*helloworld.HelloReply, error) {
    log.Printf("Received: %v", in.GetName())
    return &helloworld.HelloReply{Message: "Hello " + in.GetName()}, nil
}

func TestHelloWorld(t *testing.T) {
    lis, err := net.Listen("tcp", port)
    require.NoError(t, err)

    s := grpc.NewServer()
    helloworld.RegisterGreeterServer(s, &server{})
    go s.Serve(lis)
    defer s.Stop()

    log.Println("Dialing gRPC server...")
    conn, err := grpc.Dial(fmt.Sprintf("localhost:%s", port), grpc.WithInsecure(), grpc.WithBlock())
    require.NoError(t, err)
    defer conn.Close()
    c := helloworld.NewGreeterClient(conn)

    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()

    log.Println("Making gRPC request...")
    r, err := c.SayHello(ctx, &helloworld.HelloRequest{Name: "John Doe"})
    require.NoError(t, err)
    log.Printf("Greeting: %s", r.GetMessage())
}

问题是,当我运行此测试时,它超时:

> go test -timeout 10s ./... -v
=== RUN   TestHelloWorld
2020/06/30 11:17:45 Dialing gRPC server...
panic: test timed out after 10s

我无法查看为什么未建立连接?在我看来,服务器已正确启动...

1 个答案:

答案 0 :(得分:3)

您在此处发布的代码似乎有错字:

fmt.Sprintf("localhost:%s", port)

如果我在不使用grpc.WithBlock()选项的情况下运行测试功能,则c.SayHello会出现以下错误:

rpc error: code = Unavailable desc = connection error: desc = "transport: Error while dialing dial tcp: address localhost::50051: too many colons in address"

罪魁祸首似乎是localhost::50051

const声明中删除多余的冒号(或从fmt.Sprintf("localhost:%s", port)中删除,如果需要的话),测试通过。

const (
    port = "50051" // without the colon
)

输出:

2020/06/30 23:59:01 Dialing gRPC server...
2020/06/30 23:59:01 Making gRPC request...
2020/06/30 23:59:01 Received: John Doe
2020/06/30 23:59:01 Greeting: Hello John Doe

但是,根据grpc.WithBlock()

的文档

否则,Dial将立即返回,并且在后台进行服务器连接。

因此,使用此选项,应该从grpc.Dial调用中立即返回所有连接错误:

conn, err := grpc.Dial("bad connection string", grpc.WithBlock()) // can't connect
if err != nil {
    panic(err) // should panic, right?
}

那为什么您的代码会挂起?

通过查看grpc包的源代码(我针对v1.30.0构建了测试):

    // A blocking dial blocks until the clientConn is ready.
    if cc.dopts.block {
        for {
            s := cc.GetState()
            if s == connectivity.Ready {
                break
            } else if cc.dopts.copts.FailOnNonTempDialError && s == connectivity.TransientFailure {
                if err = cc.connectionError(); err != nil {
                    terr, ok := err.(interface {
                        Temporary() bool
                    })
                    if ok && !terr.Temporary() {
                        return nil, err
                    }
                }
            }
            if !cc.WaitForStateChange(ctx, s) {
                // ctx got timeout or canceled.
                if err = cc.connectionError(); err != nil && cc.dopts.returnLastError {
                    return nil, err
                }
                return nil, ctx.Err()
            }
        }

因此,s目前确实处于TransientFailure状态,但是FailOnNonTempDialError选项默认为false,并且当上下文过期时WaitForStateChange为false ,这不会发生,因为Dial与背景上下文一起运行:

// Dial creates a client connection to the given target.
func Dial(target string, opts ...DialOption) (*ClientConn, error) {
    return DialContext(context.Background(), target, opts...)
}

目前,我不知道这是否是预期的行为,因为v1.30.0中的某些API被标记为实验性的。

无论如何,最终要确保在Dial上捕获到此类错误,您还可以将代码重写为:

    conn, err := grpc.Dial(
        "localhost:50051", 
        grpc.WithInsecure(),
        grpc.FailOnNonTempDialError(true),
        grpc.WithBlock(), 
    )

如果连接字符串错误,则会立即失败并显示相应的错误消息。