grpc / grpc-go

The Go language implementation of gRPC. HTTP/2 based RPC

Home Page:https://grpc.io

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

New ServerOption to set cancel insensitive "values" context.

thallgren opened this issue · comments

Use case(s) - what problem will this feature solve?

The use case is essentially the same as outlined by @derekperkins in issue #5016. The difference is that the provided context will not affect cancellation. It will only provide values. I thought it might be relevant to bring this up again, because this is now very simple to accomplish thanks to the new stdlib function context.WithoutCancel(), a function that wasn't available when #5016 was discussed,

Proposed Solution

Add ValuesContext(context.Context) as grpc.ServerOption. If given, then let the server use the result of context.WithoutCancel(vc) on that provided context instead of the context.Background(), both for unary and stream calls.

Alternatives Considered

There are two viable options:

  1. Use a http.Server and declare a BaseContext there and use the grpc server as a handler. This isn't ideal because:
    • ServeHTTP does not support some gRPC features available through grpc-go's HTTP/2 server.
    • The context cannot be guaranteed to be non-cancelling.
  2. Use merged contexts in Chain[Unary/Stream]Interceptor functions. Works fine, but it requires knowledge about interceptors and about +30 lines of code that would be great to be able to avoid. Example:
type mergedCtx struct {
	context.Context
	valCtx context.Context
}

func (m *mergedCtx) Value(i interface{}) interface{} {
	if v := m.Context.Value(i); v != nil {
		return v
	}
	return m.valCtx.Value(i)
}

type mergedStream struct {
	grpc.ServerStream
	ctx mergedCtx
}

func (s *mergedStream) Context() context.Context {
	return &s.ctx
}

func NewServer(valuesCtx context.Context) *grpc.Server {
	unaryInterceptor := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
		return handler(&mergedCtx{Context: ctx, valCtx: valuesCtx}, req)
	}
	streamInterceptor := func(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
		return handler(srv, &mergedStream{
			ServerStream: ss,
			ctx:          mergedCtx{Context: ss.Context(), valCtx: valuesCtx},
		})
	}
	return grpc.NewServer(grpc.ChainUnaryInterceptor(unaryInterceptor), grpc.ChainStreamInterceptor(streamInterceptor))
}

All of which could be reduced to the caller just doing:

	s := grpc.NewServer(grpc.ValuesContext(ctx))

As you mentioned in the alternatives, interceptors can be used to achieve the required behaviour. The stdlib's http server supports adding a base context, however it doesn't have the concept of interceptors similar to gRPC-go.

After discussing with @dfawley, we don't plan to add this feature unless there is a stronger reason to do this.