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

标签 go grpc grpc-go

我想编写一个单元测试,在其中我运行一个临时 gRPC 服务器,该服务器在测试中的一个单独的 Goroutine 中启动,并在测试运行后停止。为此,我尝试将本教程 (https://grpc.io/docs/languages/go/quickstart/) 中的“Hello, world”示例改编为其中的服务器和客户端,而不是具有单独的 main.gos,是一个单独的测试函数,它异步启动服务器,随后使用 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

我不知道为什么没有建立连接?在我看来,服务器已正确启动...

最佳答案

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

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()

的文档

Without this, Dial returns immediately and connecting the server happens in background.

使用这个选项,任何连接错误都应该直接从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.WithTransportCredentials(insecure.NewCredentials()),
        grpc.FailOnNonTempDialError(true),
        grpc.WithBlock(), 
    )

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

关于go - 使用 WithBlock() 选项创建 gRPC 客户端连接到异步启动的 gRPC 服务器会无限期地阻塞?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/62663990/

相关文章:

go - 正确等待全局变量初始化

http - 为什么以下代码中的最后一个错误处理程序出现无效的参数错误?

java - 在锁定 tomcat 临时文件夹的情况下配置 Glowroot 显示以下异常

go - 同时运行 grpc 和 http 服务器

go - RPC 错误 : code = Unimplemented desc = RPC method not implemented

go - fatal error - 所有 Goroutines 都在 sleep !僵局

azure - 如何使用 oauth2 token 在 MS Graph SDK 中进行身份验证?

kubernetes - 是否可以在 GKE 集群中同时运行 Istio 和 gRPC

Golang protobufs 有名称冲突

使用 gRPC Web 进入 WebAssembly