使用 WithBlock() 选项创建 gRPC 客户端连接到异步启动的 gRPC 服务器无限期阻塞?
Creating a gRPC client connection with the WithBlock() option to an asynchronously started gRPC server blocks indefinitely?
我想编写一个单元测试,其中我 运行 一个临时 gRPC 服务器,它在测试中的一个单独的 Goroutine 中启动并在测试 运行 之后停止。为此,我尝试将本教程 (https://grpc.io/docs/languages/go/quickstart/) 中的 'Hello, world' 示例改编为一个示例,其中服务器和客户端具有单独的 main.go
s,只有一个异步启动服务器并随后使用 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.
因此 with 这个选项,任何连接错误都应该直接从 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
运行s 与背景上下文:
// 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(),
)
如果连接字符串错误,会立即失败并显示相应的错误消息。
我想编写一个单元测试,其中我 运行 一个临时 gRPC 服务器,它在测试中的一个单独的 Goroutine 中启动并在测试 运行 之后停止。为此,我尝试将本教程 (https://grpc.io/docs/languages/go/quickstart/) 中的 'Hello, world' 示例改编为一个示例,其中服务器和客户端具有单独的 main.go
s,只有一个异步启动服务器并随后使用 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.
因此 with 这个选项,任何连接错误都应该直接从 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
运行s 与背景上下文:
// 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(),
)
如果连接字符串错误,会立即失败并显示相应的错误消息。