Skip to content

Commit

Permalink
[grpc-protoc] Add an option to generate default service methods (#3110)
Browse files Browse the repository at this point in the history
### Motivation

When evolving a service definition that has multiple implementations it is ideal not to break these implementations. Therefore it would be nice if the protoc generator was able to generate default implementations on the service interface to ensure implementations always conform.

### Modifications

Add an option, `defaultServiceMethods` to the grpc-protoc stub compiler to generate these default implementations. The result is that service interfaces will provide a default, throwing `UNIMPLEMENTED` exception, implementation of all service RPC interfaces that they implement.
  • Loading branch information
mgodave authored Dec 7, 2024
1 parent 3a45a23 commit 1bcf8ea
Show file tree
Hide file tree
Showing 14 changed files with 494 additions and 226 deletions.
3 changes: 3 additions & 0 deletions servicetalk-examples/grpc/protoc-options/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ protobuf {
option 'typeNameSuffix=St'
// Option to tell the compiler to exclude all Deprecated fields, types, and methods from the output
option 'skipDeprecated=true'
// Option to generate default throwing service definitions on the service interfaces. This will allow
// teams to evolve their codebases and not break dependent libraries.
option 'defaultServiceMethods=true'
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,22 @@
*/
package io.servicetalk.examples.grpc.protocoptions;

import io.servicetalk.grpc.api.GrpcServiceContext;
import io.servicetalk.grpc.netty.GrpcServers;

import io.grpc.examples.helloworld.GreeterSt.BlockingGreeterService;
import io.grpc.examples.helloworld.HelloReply;
import io.grpc.examples.helloworld.HelloRequest;

public final class BlockingProtocOptionsServer {
public static void main(String[] args) throws Exception {
GrpcServers.forPort(8080)
.listenAndAwait((BlockingGreeterService) (ctx, request) ->
HelloReply.newBuilder().setMessage("Hello " + request.getName()).build())
.listenAndAwait(new BlockingGreeterService() {
@Override
public HelloReply sayHello(GrpcServiceContext ctx, HelloRequest request) {
return HelloReply.newBuilder().setMessage("Hello " + request.getName()).build();
}
})
.awaitShutdown();
}
}
2 changes: 2 additions & 0 deletions servicetalk-grpc-netty/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ protobuf {
grpc {}
servicetalk_grpc {
outputSubDir = "java"
// this will eventually become the default behavior
option "defaultServiceMethods=true"
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
import io.servicetalk.client.api.ServiceDiscoverer;
import io.servicetalk.client.api.ServiceDiscovererEvent;
import io.servicetalk.concurrent.api.Publisher;
import io.servicetalk.concurrent.api.Single;
import io.servicetalk.grpc.api.GrpcServerContext;
import io.servicetalk.grpc.api.GrpcServiceContext;
import io.servicetalk.transport.api.HostAndPort;

import io.grpc.examples.helloworld.Greeter.BlockingGreeterClient;
Expand Down Expand Up @@ -52,8 +54,13 @@ void forAddress() throws Exception {
String greetingPrefix = "Hello ";
String name = "foo";
try (GrpcServerContext serverContext = GrpcServers.forAddress(localAddress(0))
.listenAndAwait((GreeterService) (ctx, request) ->
succeeded(HelloReply.newBuilder().setMessage(greetingPrefix + request.getName()).build()));
.listenAndAwait(new GreeterService() {
@Override
public Single<HelloReply> sayHello(GrpcServiceContext ctx, HelloRequest request) {
return succeeded(HelloReply.newBuilder()
.setMessage(greetingPrefix + request.getName()).build());
}
});
// Use "localhost" to demonstrate that the address will be resolved.
BlockingGreeterClient client = GrpcClients.forAddress("localhost",
serverHostAndPort(serverContext).port(), ON_NEW_CONNECTION)
Expand All @@ -69,8 +76,13 @@ void withCustomSd() throws Exception {
String greetingPrefix = "Hello ";
String name = "foo";
try (GrpcServerContext serverContext = GrpcServers.forAddress(localAddress(0))
.listenAndAwait((GreeterService) (ctx, request) ->
succeeded(HelloReply.newBuilder().setMessage(greetingPrefix + request.getName()).build()))) {
.listenAndAwait(new GreeterService() {
@Override
public Single<HelloReply> sayHello(GrpcServiceContext ctx, HelloRequest request) {
return succeeded(HelloReply.newBuilder()
.setMessage(greetingPrefix + request.getName()).build());
}
})) {
// Use "localhost" to demonstrate that the address will be resolved.
HostAndPort hostAndPort = HostAndPort.of("localhost", serverHostAndPort(serverContext).port());
@SuppressWarnings("unchecked")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/
package io.servicetalk.grpc.netty;

import io.servicetalk.concurrent.api.Single;
import io.servicetalk.grpc.api.GrpcServiceContext;
import io.servicetalk.http.api.HttpProtocolConfig;
import io.servicetalk.test.resources.DefaultTestCerts;
import io.servicetalk.transport.api.ClientSslConfigBuilder;
Expand Down Expand Up @@ -69,8 +71,13 @@ void tlsNegotiated(ProtocolTestMode testMode) throws Exception {
.sslConfig(new ServerSslConfigBuilder(DefaultTestCerts::loadServerPem,
DefaultTestCerts::loadServerKey).build())
.protocols(testMode.serverConfigs))
.listenAndAwait((Greeter.GreeterService) (ctx, request) ->
succeeded(HelloReply.newBuilder().setMessage(greetingPrefix + request.getName()).build()));
.listenAndAwait(new Greeter.GreeterService() {
@Override
public Single<HelloReply> sayHello(GrpcServiceContext ctx, HelloRequest request) {
return succeeded(HelloReply.newBuilder()
.setMessage(greetingPrefix + request.getName()).build());
}
});
BlockingGreeterClient client = forResolvedAddress(serverContext.listenAddress())
.initializeHttp(builder -> builder
.sslConfig(new ClientSslConfigBuilder(DefaultTestCerts::loadServerCAPem)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import io.servicetalk.context.api.ContextMap.Key;
import io.servicetalk.grpc.api.DefaultGrpcClientMetadata;
import io.servicetalk.grpc.api.GrpcClientMetadata;
import io.servicetalk.grpc.api.GrpcServiceContext;
import io.servicetalk.grpc.api.GrpcStatusCode;
import io.servicetalk.grpc.api.GrpcStatusException;
import io.servicetalk.http.api.HttpResponseStatus;
Expand Down Expand Up @@ -87,8 +88,12 @@ class GrpcProxyTunnelTest {
.initializeHttp(httpBuilder -> httpBuilder
.sslConfig(new ServerSslConfigBuilder(DefaultTestCerts::loadServerPem,
DefaultTestCerts::loadServerKey).build()))
.listenAndAwait((Greeter.BlockingGreeterService) (ctx, request) ->
HelloReply.newBuilder().setMessage(GREETING_PREFIX + request.getName()).build());
.listenAndAwait(new Greeter.BlockingGreeterService() {
@Override
public HelloReply sayHello(GrpcServiceContext ctx, HelloRequest request) {
return HelloReply.newBuilder().setMessage(GREETING_PREFIX + request.getName()).build();
}
});
client = GrpcClients.forAddress(serverHostAndPort(serverContext))
.initializeHttp(httpBuilder -> httpBuilder.proxyConfig(forAddress(proxyAddress))
.sslConfig(new ClientSslConfigBuilder(DefaultTestCerts::loadServerCAPem)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,21 +103,21 @@ void tearDown() throws Exception {
}
}

@ParameterizedTest(name = "httpVersion={0) streamingService={0)")
@ParameterizedTest(name = "httpVersion={0} streamingService={1}")
@MethodSource("params")
void testAggregated(HttpProtocolVersion httpProtocol, boolean streamingService) throws Exception {
setUp(httpProtocol, streamingService);
assertResponse(client.test(newRequest()));
}

@ParameterizedTest(name = "httpVersion={0) streamingService={0)")
@ParameterizedTest(name = "httpVersion={0} streamingService={1}")
@MethodSource("params")
void testRequestStream(HttpProtocolVersion httpProtocol, boolean streamingService) throws Exception {
setUp(httpProtocol, streamingService);
assertResponse(client.testRequestStream(Arrays.asList(newRequest(), newRequest())));
}

@ParameterizedTest(name = "httpVersion={0) streamingService={0)")
@ParameterizedTest(name = "httpVersion={0} streamingService={1}")
@MethodSource("params")
void testBiDiStream(HttpProtocolVersion httpProtocol, boolean streamingService) throws Exception {
setUp(httpProtocol, streamingService);
Expand All @@ -127,7 +127,7 @@ void testBiDiStream(HttpProtocolVersion httpProtocol, boolean streamingService)
}
}

@ParameterizedTest(name = "httpVersion={0) streamingService={0)")
@ParameterizedTest(name = "httpVersion={0} streamingService={1}")
@MethodSource("params")
void testResponseStream(HttpProtocolVersion httpProtocol, boolean streamingService) throws Exception {
setUp(httpProtocol, streamingService);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package io.servicetalk.grpc.netty;

import io.servicetalk.concurrent.api.Single;
import io.servicetalk.grpc.api.GrpcServiceContext;
import io.servicetalk.grpc.api.GrpcStatusException;
import io.servicetalk.http.api.FilterableStreamingHttpClient;
import io.servicetalk.http.api.HttpExecutionStrategy;
Expand Down Expand Up @@ -87,8 +88,12 @@ void serverFilterNeverRespondsAppliesDeadline(boolean appendNonOffloading, boole
builder.appendServiceFilter(NEVER_SERVER_FILTER);
}
})
.listenAndAwait((GreeterService) (ctx, request) ->
succeeded(HelloReply.newBuilder().setMessage("hello " + request.getName()).build()));
.listenAndAwait(new GreeterService() {
@Override
public Single<HelloReply> sayHello(GrpcServiceContext ctx, HelloRequest request) {
return succeeded(HelloReply.newBuilder().setMessage("hello " + request.getName()).build());
}
});
BlockingGreeterClient client = forResolvedAddress(serverContext.listenAddress())
.defaultTimeout(clientAppliesTimeout ? DEFAULT_TIMEOUT : null, clientAppliesTimeout)
.buildBlocking(new Greeter.ClientFactory())) {
Expand All @@ -102,8 +107,11 @@ void serverFilterNeverRespondsAppliesDeadline(boolean appendNonOffloading, boole
void clientFilterNeverRespondsAppliesDeadline(boolean builderEnableTimeout) throws Exception {
try (ServerContext serverContext = forAddress(localAddress(0))
.defaultTimeout(null, false)
.listenAndAwait((GreeterService) (ctx, request) -> {
throw new IllegalStateException("client using never filter, server shouldn't read response");
.listenAndAwait(new GreeterService() {
@Override
public Single<HelloReply> sayHello(GrpcServiceContext ctx, HelloRequest request) {
throw new IllegalStateException("client using never filter, server shouldn't read response");
}
});
BlockingGreeterClient client = forResolvedAddress(serverContext.listenAddress())
.defaultTimeout(DEFAULT_TIMEOUT, builderEnableTimeout)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/
package io.servicetalk.grpc.netty;

import io.servicetalk.concurrent.api.Single;
import io.servicetalk.grpc.api.GrpcServiceContext;
import io.servicetalk.transport.api.IoExecutor;
import io.servicetalk.transport.api.ServerContext;

Expand Down Expand Up @@ -61,8 +63,13 @@ void udsRoundTrip() throws Exception {
String name = "foo";
try (ServerContext serverContext = forAddress(newSocketAddress())
.initializeHttp(builder -> builder.ioExecutor(ioExecutor))
.listenAndAwait((GreeterService) (ctx, request) ->
succeeded(HelloReply.newBuilder().setMessage(greetingPrefix + request.getName()).build()));
.listenAndAwait(new GreeterService() {
@Override
public Single<HelloReply> sayHello(GrpcServiceContext ctx, HelloRequest request) {
return succeeded(HelloReply.newBuilder()
.setMessage(greetingPrefix + request.getName()).build());
}
});
BlockingGreeterClient client = forResolvedAddress(serverContext.listenAddress())
.buildBlocking(new ClientFactory())) {
HelloRequest request = HelloRequest.newBuilder().setName(name).build();
Expand Down
Loading

0 comments on commit 1bcf8ea

Please sign in to comment.