3
votes

I have a GRPC API where, following a refactor, a few packages were renamed. This includes the package declaration in one of our proto files that defines the API. Something like this:

package foo;

service BazApi {
    rpc FooEventStream(stream Ack) returns (stream FooEvent);
}

which was changed to

package bar;

service BazApi {
    rpc FooEventStream(stream Ack) returns (stream FooEvent);
}

The server side is implemented using grpc-java with scala and monix on top.

This all works fine for clients that use the new proto files, but for old clients that were built on top of the old proto files, this causes problems: UNIMPLEMENTED: Method not found: foo.BazApi/FooEventStream.

The actual data format of the messages passed over the GRPC API has not changed, only the package.

Since we need to keep backwards compatibility, I've been looking into a way to make the old clients work while keeping the name change.

I was hoping to make this work with a generic ServerInterceptor which would be able to inspect an incoming call, see that it's from an old client (we have the client version in the headers) and redirect/forward it to the renamed service. (Since it's just the package name that changed, this is easy to figure out e.g. foo.BazApi/FooEventStream -> bar.BazApi/FooEventStream)

However, there doesn't seem to be an elegant way to do this. I think it's possible by starting a new ClientCall to the correct endpoint, and then handling the ServerCall within the interceptor by delegating to the ClientCall, but that will require a bunch of plumbing code to properly handle unary/clientStreaming/serverStreaming/bidiStreaming calls.

Is there a better way to do this?

2

2 Answers

5
votes

If you can easily change the server, you can have it support both names simultaneously. You can consider a solution where you register your service twice, with two different descriptors.

Every service has a bindService() method that returns a ServerServiceDefinition. You can pass the definition to the server via the normal serverBuilder.addService().

So you could get the normal ServerServiceDefinition and then rewrite it to the new name and then register the new name.

BazApiImpl service = new BazApiImpl();
serverBuilder.addService(service); // register "bar"

ServerServiceDefinition barDef = service.bindService();
ServerServiceDefinition fooDefBuilder = ServerServiceDefinition.builder("foo.BazApi");
for (ServerMethodDefinition<?,?> barMethodDef : barDef.getMethods()) {
  MethodDescriptor desc = barMethodDef.getMethodDescriptor();
  String newName = desc.getFullMethodName().replace("foo.BazApi/", "bar.BazApi/");
  desc = desc.toBuilder().setFullMethodName(newName).build();
  foDefBuilder.addMethod(desc, barMethodDef.getServerCallHandler());
}
serverBuilder.addService(fooDefBuilder.build()); // register "foo"
3
votes

Using the lower-level "channel" API you can make a proxy without too much work. You mainly just proxy events from a ServerCall.Listener to a ClientCall and the ClientCall.Listener to a ServerCall. You get to learn about the lower-level MethodDescriptor and the rarely-used HandlerRegistry. There's also some complexity to handle flow control (isReady() and request()).

I made an example a while back, but never spent the time to merge it to grpc-java itself. It is currently available on my random branch. You should be able to get it working just by changing localhost:8980 and by re-writing the MethodDescriptor passed to channel.newCall(...). Something akin to:

MethodDescriptor desc = serverCall.getMethodDescriptor();
if (desc.getFullMethodName().startsWith("foo.BazApi/")) {
  String newName = desc.getFullMethodName().replace("foo.BazApi/", "bar.BazApi/");
  desc = desc.toBuilder().setFullMethodName(newName).build();
}
ClientCall<ReqT, RespT> clientCall
    = channel.newCall(desc, CallOptions.DEFAULT);