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:
- Use a
http.Server
and declare aBaseContext
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.
- 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.