Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

reflection: improve server implementation #5197

Conversation

jhump
Copy link
Member

@jhump jhump commented Feb 16, 2022

This updates the server reflection implementation in two key ways:

  1. Support alternate source of descriptors, like for RPC servers that get their descriptors dynamically and are dynamic proxies.
    • Instead of always getting descriptors from the set of global registered files (when generated code is linked into the calling program), this allows caller to supply custom resolvers.
    • This also allows caller to supply a set of service names instead of supplying a *grpc.Server.
  2. Use the new protobuf API v2 stuff to get the descriptors, which is much more sane than the old APIs.
    • This allows much of the code to be eliminated since standard APIs now support symbol and descriptor lookup.
    • The potential downside of this simplification is that encoded descriptors (pre-serialized to bytes) are no longer retained, so descriptor protos are re-created and marshaled with each call. Given the expected frequency of reflection requests, this is likely a non-issue and not worth adding back some sort of cache.

RELEASE NOTES:

  • A new NewServer(ServerOptions) function has been added to the google.golang.org/grpc/reflection package that allows creating the reflection server with customizations to the list of advertised services and to the mechanism used to lookup descriptors and extensions.

@jhump
Copy link
Member Author

jhump commented Feb 16, 2022

With improvements like this, I could also incorporate my custom wrapper around the existing proto registries, like to augment the descriptors with source code info:
https://github.com/jhump/protoreflect/blob/master/desc/sourceinfo/registry.go#L37
Without a change like this, I have to fork the entire implementation of the reflection service.

But, as mentioned in the description, another use case would be a dynamic server/proxy, whose service descriptors might be retrieved dynamically.

@jhump jhump force-pushed the jh/use-new-protoreflect-and-support-alternate-src-of-descriptors branch from af3c5b2 to 4313fb4 Compare February 16, 2022 18:30
@jhump
Copy link
Member Author

jhump commented Feb 16, 2022

@dfawley, @menghanl, is this (or something not far from this) something you'd consider merging?

@menghanl menghanl self-requested a review February 16, 2022 19:19
@jhump jhump force-pushed the jh/use-new-protoreflect-and-support-alternate-src-of-descriptors branch 2 times, most recently from de01755 to 94c0e5d Compare February 16, 2022 19:47
@@ -348,7 +356,7 @@ func testFileContainingSymbol(t *testing.T, stream rpb.ServerReflection_ServerRe
{"grpc.testingv3.SearchResponseV3.Result.Value.val", fdTestv3Byte},
{"grpc.testingv3.SearchResponseV3.Result.Value.str", fdTestv3Byte},
{"grpc.testingv3.SearchResponseV3.State", fdTestv3Byte},
{"grpc.testingv3.SearchResponseV3.State.FRESH", fdTestv3Byte},
{"grpc.testingv3.SearchResponseV3.FRESH", fdTestv3Byte},
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was a bug in the previous formulation. In protobuf, enum values are defined at the same scope as their enclosing enum.

s.processEnum(fd, prefix, en)
// NewServer returns a reflection server implementation using the given options.
// It returns an error if the given options are invalid.
func NewServer(opts ServerOptions) (rpb.ServerReflectionServer, error) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious what your thoughts on this style of API, vs. Register where the server object is never exposed.

One advantage here is that it allows the caller to wrap/decorate the service. (Other formulation requires caller to use an interceptor to do so.)

Also, the options include an optional GRPCServer. But we'd have to have a server to do the actual Register... call. So it feels like a confusing API to have a required server argument and then also an optional field in the options struct.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or, we define an interface with func GetServiceInfo() (and you won't need the other field ServiceNames []string).

Did we talk about this interface option before? I don't remember if we got any conclusion.

Copy link
Member Author

@jhump jhump Feb 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is already an interface named GRPCServer. The list of ServiceNames is useful if the caller wants more control over exactly which services are advertised by the reflection service.

In a dynamic proxy case, even the list of service names could change over time. So providing the separate names and having this factory for a server (vs. the Register style function, where the server impl is never exposed) allows you to handle this:

  • The caller would create a wrapper for the service impl. The wrapper delegates to the service impl. But it can also swap out the underlying impl in response to an event that changes the set of services. (Like watching services in kubernetes or something like that).
  • The actual gRPC server handling is all implemented via an UnknownServiceHandler, that can dynamically proxy requests instead of returning "Not Implemented" errors.

Copy link
Contributor

@menghanl menghanl Feb 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, right. I forgot we already made the GRPCServer interface.

What I meant is a smaller interface, with just GetServiceInfo() map[string]grpc.ServiceInfo.
You can still pass it the grpc server (that's the default behavior), or the dynamic factory.
Or if you want a static []string, you can pass it an implementation that returns the list (and you won't need the other field in ServerOptions).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, I think I see what you mean.

Would you object to me adding a utility function, like ServiceInfoFromNames, that takes a slice of names ([]string) and returns this new interface, that provides the service info?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. (I went ahead and added the utility function I asked about. Let me know what you think.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI, I realized that the whole sync.Once initialization was not necessary, since this is no longer trying to accumulate an index. So I've removed it, which I think improves things a bit.

@jhump jhump force-pushed the jh/use-new-protoreflect-and-support-alternate-src-of-descriptors branch from 94c0e5d to 7c1ba55 Compare February 16, 2022 19:57
1. Support alternate source of descriptors, like for RPC servers that get
   their descriptors dynamically and are dynamic proxies
2. Use the new protobuf API v2 stuff to get the descriptors, which is much
   more sane than the old APIs
@jhump jhump force-pushed the jh/use-new-protoreflect-and-support-alternate-src-of-descriptors branch from 7c1ba55 to 743e65d Compare February 16, 2022 20:50
Comment on lines 100 to 103
// Optional resolver used to load descriptors.
DescriptorResolver protodesc.Resolver
// Optional resolver used to query for known extensions.
ExtensionResolver ExtensionResolver
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's document the default value for these. If unset, protoregistry.GlobalFiles will be used

Copy link
Member Author

@jhump jhump Feb 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. These were documented above. So I rearranged a little and moved the relevant comments down to the fields' Go doc.

// NewServer returns a reflection server implementation using the given options.
// It returns an error if the given options are invalid.
func NewServer(opts ServerOptions) (rpb.ServerReflectionServer, error) {
if opts.Server != nil && len(opts.ServiceNames) > 0 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Related to the other comment, if we merge these two fields)
We can remove this error. Also clean up the code in Register.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment on lines 118 to 121
// ServiceInfoFromNames returns a ServiceInfoProvider backed by the given
// slice of service names. This allows for creating a reflection server
// with exactly the given advertised services.
func ServiceInfoFromNames(serviceNames []string) ServiceInfoProvider {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me know if you object to this helper. Though I think it's useful since it might not otherwise be obvious how to customize the set of advertised services.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally think this isn't necessary.

If your concern is that the interface is confusing (since only the map keys are used), we can change the interface to return []string, and wrap *grpc.Server to implement the interface.

@dfawley WDYT

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay. I went ahead and removed it and added some extra documentation to the Services field in ServerOptions.

@jhump jhump force-pushed the jh/use-new-protoreflect-and-support-alternate-src-of-descriptors branch from 011b877 to 5999441 Compare February 17, 2022 21:33
Copy link
Contributor

@menghanl menghanl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall LGTM. Thanks for the change!

for _, msg := range fd.MessageType {
s.processMessage(fd, prefix, msg)
// NewServer returns a reflection server implementation using the given options.
// It returns an error if the given options are invalid.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's document that Register() is still recommended unless the user wants to customize the server behaviors.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And to be a bit more cautious, mark the new functions/types as experimental?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

Comment on lines 118 to 121
// ServiceInfoFromNames returns a ServiceInfoProvider backed by the given
// slice of service names. This allows for creating a reflection server
// with exactly the given advertised services.
func ServiceInfoFromNames(serviceNames []string) ServiceInfoProvider {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally think this isn't necessary.

If your concern is that the interface is confusing (since only the map keys are used), we can change the interface to return []string, and wrap *grpc.Server to implement the interface.

@dfawley WDYT

func fileDescWithDependencies(fd *dpb.FileDescriptorProto, sentFileDescriptors map[string]bool) ([][]byte, error) {
r := [][]byte{}
queue := []*dpb.FileDescriptorProto{fd}
func (s *serverReflectionServer) fileDescWithDependencies(fd protoreflect.FileDescriptor, sentFileDescriptors map[string]struct{}) ([][]byte, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On map[string]bool vs map[string]struct{}.

  1. I'm not sure which is more memory efficient, but I think the difference is small enough that we wouldn't care
  2. In gRPC-Go, we've mostly (but not as an official policy) adopted map[string]bool as "set"s. Since if the entry doesn't exist, the map read returns the default bool (which is false). And we don't need to write struct{}{} when adding entries.

Let me know what you think.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. I can walk that part back.

My habit is map[string]struct{} from my early experiences with Go... Admittedly, since it only elides a single byte from the map entry, thanks to pointer word alignment, it may have zero impact on memory usage 🤷

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@menghanl menghanl added this to the 1.45 Release milestone Feb 17, 2022
@menghanl menghanl assigned dfawley and unassigned menghanl Feb 17, 2022
jhump added a commit to jhump/protoreflect that referenced this pull request Feb 17, 2022
…ckage; but sourceinfo.GlobalFiles needs to implement add'l methods to serve extension info
@jhump
Copy link
Member Author

jhump commented Feb 18, 2022

ping @dfawley

Copy link
Member

@dfawley dfawley left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Just a couple small things, though.

//
// The reflection service is only interested in the service names, but the
// signature is this way so that *grpc.Server implements it.
type ServiceInfoProvider interface {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add an Experimental comment here, too, and on all of the newly added exported symbols.

I'd like to give this some time for users to starting using this and provide feedback before declaring anything stable.

Copy link
Member Author

@jhump jhump Feb 18, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Added here and to the ExtensionResolver interface, too.

Comment on lines 99 to 100
// It is okay for a custom implementation to return zero values for the
// grpc.ServiceInfo values in the map.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we move this comment to ServiceInfoProvider?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll remove this and put more clarification over in the other Go docs.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@dfawley dfawley assigned jhump and unassigned dfawley Feb 18, 2022
@jhump
Copy link
Member Author

jhump commented Feb 18, 2022

I re-ordered some elements in the file in my last commit, hoping to make the file layout a little more intuitive. All of the new experimental stuff now appears after the preferred non-experimental API elements.

I think I've addressed all comments. PTAL and let me know.

And thanks for the consideration/review!

Copy link
Member

@dfawley dfawley left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the code reorganization, thank you for the extra care.

@dfawley dfawley changed the title improve server reflection implementation reflection: improve server implementation Feb 18, 2022
@dfawley dfawley merged commit 18564ff into grpc:master Feb 18, 2022
@jhump
Copy link
Member Author

jhump commented Feb 18, 2022

Thank you, @dfawley and @menghanl!

jhump added a commit to jhump/protoreflect that referenced this pull request Feb 19, 2022
…ckage; but sourceinfo.GlobalFiles needs to implement add'l methods to serve extension info
jhump added a commit to jhump/protoreflect that referenced this pull request Feb 19, 2022
Thanks to grpc/grpc-go#5197, no longer necessary
But sourceinfo.GlobalFiles does need to implement add'l methods to serve extension info
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Aug 18, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants