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

[grpc-protoc] Add an option to generate default service methods #3110

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
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'
mgodave marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/
package io.servicetalk.examples.grpc.protocoptions;

import io.grpc.examples.helloworld.HelloRequest;
import io.servicetalk.grpc.api.GrpcServiceContext;
import io.servicetalk.grpc.netty.GrpcServers;

import io.grpc.examples.helloworld.GreeterSt.BlockingGreeterService;
Expand All @@ -23,8 +25,12 @@
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() {
idelpivnitskiy marked this conversation as resolved.
Show resolved Hide resolved
@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
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@
import static io.servicetalk.grpc.api.GrpcStatusCode.INTERNAL;
import static io.servicetalk.grpc.api.GrpcStatusCode.INVALID_ARGUMENT;
import static io.servicetalk.grpc.api.GrpcStatusCode.UNAUTHENTICATED;
import static io.servicetalk.grpc.api.GrpcStatusCode.UNIMPLEMENTED;
import static io.servicetalk.grpc.api.GrpcStatusCode.UNKNOWN;
import static io.servicetalk.grpc.internal.DeadlineUtils.GRPC_TIMEOUT_HEADER_KEY;
import static io.servicetalk.http.api.HttpExecutionStrategies.offloadNone;
Expand Down Expand Up @@ -610,6 +611,50 @@ void clientH2ReturnStatus() throws Exception {
}
}

@Test
void grpcJavaToServiceTalkUnimplementedService() throws Exception {
try (TestServerContext server = serviceTalkServerBlocking(ErrorMode.STATUS, false, null, null);
CompatClient client = grpcJavaClient(server.listenAddress(), null, false, null)) {
final Single<CompatResponse> response =
client.unimplementedServerCall(CompatRequest.newBuilder().setId(1).build());
Copy link
Member

@idelpivnitskiy idelpivnitskiy Nov 22, 2024

Choose a reason for hiding this comment

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

Instead of adding a new unimplementedServerCall in the proto, consider adding service parameter for serviceTalkServerBlocking, serviceTalkServer, and grpcJavaServer. There you can pass an unimplemented service like new BlockingCompatService() { }. That way you can validate all 4 API flavors in every test method. Problems highlighted in above messages will be automatically validated.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Always throwing will still break the default behavior, especially in cases where the user has implemented the GrpcResponseWriter variant.

Copy link
Member

Choose a reason for hiding this comment

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

Looks like a reply to a wrong thread, but you are right. We should still skip those that already have a default.

validateGrpcErrorInResponse(response.toFuture(), false, UNIMPLEMENTED,
"Method grpc.netty.Compat/unimplementedServerCall is unimplemented");
}
}

@Test
void grpcJavaToGrpcJavaUnimplementedService() throws Exception {
try (TestServerContext server = grpcJavaServer(ErrorMode.NONE, false, null, null);
CompatClient client = grpcJavaClient(server.listenAddress(), null, false, null)) {
final Single<CompatResponse> response =
client.unimplementedServerCall(CompatRequest.newBuilder().setId(1).build());
validateGrpcErrorInResponse(response.toFuture(), false, UNIMPLEMENTED,
"Method grpc.netty.Compat/unimplementedServerCall is unimplemented");
}
}

@Test
void serviceTalkToGrpcJavaUnimplementedService() throws Exception {
try (TestServerContext server = grpcJavaServer(ErrorMode.NONE, false, null, null);
CompatClient client = serviceTalkClient(server.listenAddress(), false, null, null)) {
final Single<CompatResponse> response =
client.unimplementedServerCall(CompatRequest.newBuilder().setId(1).build());
validateGrpcErrorInResponse(response.toFuture(), false, UNIMPLEMENTED,
"Method grpc.netty.Compat/unimplementedServerCall is unimplemented");
}
}

@Test
void serviceTalkToServiceTalkUnimplementedService() throws Exception {
try (TestServerContext server = serviceTalkServerBlocking(ErrorMode.NONE, false, null, null);
Copy link
Member

Choose a reason for hiding this comment

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

For grpcJavaToServiceTalkUnimplementedService and serviceTalkToServiceTalkUnimplementedService consider making them @ParameterizedTest to also run with serviceTalkServer(..., new CompatService() { })

CompatClient client = serviceTalkClient(server.listenAddress(), false, null, null)) {
final Single<CompatResponse> response =
client.unimplementedServerCall(CompatRequest.newBuilder().setId(1).build());
validateGrpcErrorInResponse(response.toFuture(), false, UNIMPLEMENTED,
"Method grpc.netty.Compat/unimplementedServerCall is unimplemented");
}
}

@ParameterizedTest
@MethodSource("sslStreamingAndCompressionParams")
void grpcJavaToServiceTalkBlockingError(final boolean ssl,
Expand Down Expand Up @@ -1496,6 +1541,27 @@ public Publisher<CompatResponse> serverStreamingCall(final GrpcClientMetadata me
return serverStreamingCall(request);
}

@Override
public Single<CompatResponse> unimplementedServerCall(final CompatRequest request) {
final Processor<CompatResponse, CompatResponse> processor =
newSingleProcessor();
finalStub.unimplementedServerCall(request, adaptResponse(processor));
return fromSource(processor);
}

@Override
public Single<CompatResponse> unimplementedServerCall(final GrpcClientMetadata metadata,
final CompatRequest request) {
return unimplementedServerCall(request);
}

@Deprecated
@Override
public Single<CompatResponse> unimplementedServerCall(final Compat.UnimplementedServerCallMetadata metadata,
final CompatRequest request) {
return unimplementedServerCall(request);
}

@Override
public void close() throws Exception {
channel.shutdown().awaitTermination(DEFAULT_TIMEOUT_SECONDS, SECONDS);
Expand Down
1 change: 1 addition & 0 deletions servicetalk-grpc-netty/src/test/proto/test_compat.proto
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,5 @@ service Compat {
rpc clientStreamingCall (stream RequestContainer.CompatRequest) returns (ResponseContainer.CompatResponse) {}
rpc serverStreamingCall (RequestContainer.CompatRequest) returns (stream ResponseContainer.CompatResponse) {}
rpc bidirectionalStreamingCall (stream RequestContainer.CompatRequest) returns (stream ResponseContainer.CompatResponse) {}
rpc unimplementedServerCall (RequestContainer.CompatRequest) returns (ResponseContainer.CompatResponse) {}
}
31 changes: 31 additions & 0 deletions servicetalk-grpc-protoc/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,34 @@ And with Maven:
</args>
</protocPlugin>
----

==== `defaultServiceMethods=<true|false> (default: false)`
Generates default service interface methods to ensure implementations will continue to compile as the API is evolved.

If you are using the
link:https://github.com/google/protobuf-gradle-plugin#configure-what-to-generate[protobuf-gradle-plugin] this is how you
can specify an option:

[source,gradle]
----
task.plugins {
servicetalk_grpc {
option 'defaultServiceMethods=true'
}
}
----

And with Maven:

And with Maven:

[source, xml]
----
<protocPlugin>
<id>servicetalk-grpc-protoc</id>
<!-- more params -->
<args>
<arg>defaultServiceMethods=true</arg>
</args>
</protocPlugin>
----
Loading
Loading