6
votes

I am building an API using gRPC and in server side, I want to receive a notification when a client disconnects, identify it and perform some tasks based on that.

So far, I was able to detect client disconnection using grpc.StatsHandler method HandleConn. I tried passing values using context, but they can't be accessed from server side.

Client side:

conn, err := grpc.DialContext(
    context.WithValue(context.Background(), "user_id", 1234),
    address,
    grpc.WithInsecure(),
)

Server side:

// Build stats handler
type serverStats struct {}

func (h *serverStats) TagRPC(ctx context.Context, info *stats.RPCTagInfo) context.Context {
    return ctx
}

func (h *serverStats) HandleRPC(ctx context.Context, s stats.RPCStats) {}

func (h *serverStats) TagConn(ctx context.Context, info *stats.ConnTagInfo) context.Context {
    return context.TODO()
}

func (h *serverStats) HandleConn(ctx context.Context, s stats.ConnStats) {
    fmt.Println(ctx.Value("user_id")) // Returns nil, can't access the value

    switch s.(type) {
    case *stats.ConnEnd:
        fmt.Println("client disconnected")
        break
    }
}


// Build server
s := grpc.NewServer(grpc.StatsHandler(&serverStats{}))

I want to access the value passed from client side in server side. What is the right way to do it, or is there any other way to identify the client that has disconnected?

2
I tried passing values using context I'm not sure of the specifics but this might if you use GRPC metadata: (github.com/grpc/grpc-go/blob/master/Documentation/…), but i'm not sure the sequence of GRPC method invocations and the specifics of when/how/where metadata is sent in nGRPC protocol (ie will it always be availble in HandleConn??? What's your use case for this? There might be alternatives to handling disconnect by making operations idempotent (or potentially other approaches).dm03514

2 Answers

1
votes

I don't think you may pass that value when you are dialling, but you may tag the connection and than all next client requests will have the same value in its context:

In your serverStats implementation:

func (h *serverStats) TagConn(ctx context.Context, info *stats.ConnTagInfo) context.Context {
    return context.WithValue(ctx, "user_counter", userCounter++)
}

In your service implementation

func (s *service) SetUserId(ctx context.Context, v MyMessage) (r MyResponse, err Error) {

   s.userIdMap[ctx.Value("user_counter")] = v.userId
...

}

In the other methods:


func (s *service) AnotherMethod(ctx context.Context, v MyMessage) (r MyResponse, err Error) {

   userId := s.userIdMap[ctx.Value("user_counter")]
...

}
func (h *serverStats) HandleConn(ctx context.Context, s stats.ConnStats) {

    switch s.(type) {
    case *stats.ConnEnd:
        fmt.Printf("client %d disconnected", s.userIdMap[ctx.Value("user_counter")])
        break
    }
}

Please let me know if you find a way to pass values in the dialling step.

0
votes

I tried passing values using context, but they can't be accessed from server side.

You need to set metadata fields to the client context explicitly:

ctx := context.Background()
ctx = metadata.AppendToOutgoingContext(ctx, "user_id", "1234")
conn, err := grpc.DialContext(cxt, address)

On server side you can retrieve them like this:

md, ok := metadata.FromIncomingContext(ctx)

What is the right way to do it, or is there any other way to identify the client that has disconnected?

This really depends on your use case. I think that GRPC Stats API is good for some simple tasks (for example, to compute latency or aggregate network stats), but it's not very useful when some business logic should work when client leaves. I would suggest using defer calls in GRPC handlers directly for that. One more option is to implement custom GRPC interceptor.