Kurt Peek
Kurt Peek

Reputation: 57741

How to use the bufconn package with grpc.NewClient?

I have a unit test that is similar to the following (adapted from https://github.com/castaneai/grpc-testing-with-bufconn/blob/master/server/server.go):

package main

import (
    "context"
    "log"
    "net"

    "google.golang.org/grpc"
    "google.golang.org/grpc/examples/helloworld/helloworld"
    "google.golang.org/grpc/test/bufconn"
)

func main() {
    lis := bufconn.Listen(1024 * 1024)
    s := grpc.NewServer()
    helloworld.RegisterGreeterServer(s, &server{})
    go func() {
        if err := s.Serve(lis); err != nil {
            log.Fatal(err)
        }
    }()

    conn, err := grpc.DialContext(context.Background(), "bufnet", grpc.WithContextDialer(func(ctx context.Context, s string) (net.Conn, error) {
        return lis.Dial()
    }), grpc.WithInsecure())
    if err != nil {
        log.Fatalf("dial: %v", err)
    }

    client := helloworld.NewGreeterClient(conn)
    resp, err := client.SayHello(context.Background(), &helloworld.HelloRequest{
        Name: "Mary Contrary",
    })
    if err != nil {
        log.Fatalf("say hello: %v", err)
    }
    log.Println("response message:", resp.GetMessage())
}

type server struct {
    helloworld.UnimplementedGreeterServer
}

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

When I run it, it prints the expected output:

> go run main.go
2024/05/15 10:20:19 Received: Mary Contrary
2024/05/15 10:20:19 response message: Hello, Mary Contrary

However, DialContext is deprecated and replaced by NewClient. If I try to replace it like so,

    conn, err := grpc.NewClient("bufnet", grpc.WithContextDialer(func(ctx context.Context, s string) (net.Conn, error) {
        return lis.Dial()
    }), grpc.WithInsecure())

The program now errors:

> go run main.go
2024/05/15 10:17:37 say hello: rpc error: code = Unavailable desc = name resolver error: produced zero addresses
exit status 1

I suspect that this has something to do with this documentation comment:

One subtle difference between NewClient and Dial and DialContext is that the former uses "dns" as the default name resolver, while the latter use "passthrough" for backward compatibility. This distinction should not matter to most users, but could matter to legacy users that specify a custom dialer and expect it to receive the target string directly.

Indeed if I look at the definition of DialContext,

func DialContext(ctx context.Context, target string, opts ...DialOption) (conn *ClientConn, err error) {
    // At the end of this method, we kick the channel out of idle, rather than
    // waiting for the first rpc.
    opts = append([]DialOption{withDefaultScheme("passthrough")}, opts...)
    cc, err := NewClient(target, opts...)
    if err != nil {
        return nil, err
    }
    ...

It seems that it just calls NewClient under the hood and overrides the default scheme with an non-exported option. How can I get this example to work using NewClient?

Upvotes: 6

Views: 3100

Answers (2)

Alexander
Alexander

Reputation: 111

Specify the passthrough scheme when creating a new client. The passthrough resolver will not attempt to resolve anything:

conn, err := grpc.NewClient("passthrough://bufnet", ...)

It's better not to use SetDefaultScheme to avoid thread safety issues and to keep the global state clean. From docs:

// NOTE: this function must only be called during initialization time (i.e. in
// an init() function), and is not thread-safe. The scheme set last overrides
// previously set values.

Upvotes: 10

Kurt Peek
Kurt Peek

Reputation: 57741

It turns out the default scheme can be overridden globally by calling resolver.SetDefaultScheme. The full updated program is:

package main

import (
    "context"
    "log"
    "net"

    "google.golang.org/grpc"
    "google.golang.org/grpc/examples/helloworld/helloworld"
    "google.golang.org/grpc/resolver"
    "google.golang.org/grpc/test/bufconn"
)

func main() {
    lis := bufconn.Listen(1024 * 1024)
    s := grpc.NewServer()
    helloworld.RegisterGreeterServer(s, &server{})
    go func() {
        if err := s.Serve(lis); err != nil {
            log.Fatal(err)
        }
    }()

    resolver.SetDefaultScheme("passthrough")
    conn, err := grpc.NewClient("bufnet", grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) {
        return lis.Dial()
    }), grpc.WithInsecure())
    if err != nil {
        log.Fatalf("dial: %v", err)
    }

    client := helloworld.NewGreeterClient(conn)
    resp, err := client.SayHello(context.Background(), &helloworld.HelloRequest{
        Name: "Mary Contrary",
    })
    if err != nil {
        log.Fatalf("say hello: %v", err)
    }
    log.Println("response message:", resp.GetMessage())
}

type server struct {
    helloworld.UnimplementedGreeterServer
}

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

Which again prints the expected output:

> go run main.go
2024/05/15 11:34:34 Received: Mary Contrary
2024/05/15 11:34:34 response message: Hello, Mary Contrary

Upvotes: 2

Related Questions