From b2ff4b719b20d51ae0d491f20de2c413b10d469a Mon Sep 17 00:00:00 2001 From: "Mr.Z" <15869103363@163.com> Date: Wed, 14 Aug 2019 17:04:36 +0800 Subject: [PATCH] Fix the bug of wrong RT and exception tracing in sentinel-grpc-adapter (#291) --- .../grpc/SentinelGrpcClientInterceptor.java | 61 +++++----- .../grpc/SentinelGrpcServerInterceptor.java | 85 ++++++++------ .../adapter/grpc/FooServiceClient.java | 26 ++++- .../sentinel/adapter/grpc/FooServiceImpl.java | 26 ++++- .../sentinel/adapter/grpc/GrpcTestServer.java | 1 + ...tinelGrpcClientInterceptorDegradeTest.java | 98 ++++++++++++++++ ...tinelGrpcServerInterceptorDegradeTest.java | 109 ++++++++++++++++++ .../src/test/proto/example.proto | 7 ++ 8 files changed, 342 insertions(+), 71 deletions(-) create mode 100644 sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/SentinelGrpcClientInterceptorDegradeTest.java create mode 100644 sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/SentinelGrpcServerInterceptorDegradeTest.java diff --git a/sentinel-adapter/sentinel-grpc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/grpc/SentinelGrpcClientInterceptor.java b/sentinel-adapter/sentinel-grpc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/grpc/SentinelGrpcClientInterceptor.java index 4e168627..a265c95d 100755 --- a/sentinel-adapter/sentinel-grpc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/grpc/SentinelGrpcClientInterceptor.java +++ b/sentinel-adapter/sentinel-grpc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/grpc/SentinelGrpcClientInterceptor.java @@ -15,28 +15,20 @@ */ package com.alibaba.csp.sentinel.adapter.grpc; -import javax.annotation.Nullable; - -import com.alibaba.csp.sentinel.Entry; +import com.alibaba.csp.sentinel.AsyncEntry; import com.alibaba.csp.sentinel.EntryType; import com.alibaba.csp.sentinel.SphU; import com.alibaba.csp.sentinel.Tracer; import com.alibaba.csp.sentinel.context.ContextUtil; import com.alibaba.csp.sentinel.slots.block.BlockException; - -import io.grpc.CallOptions; -import io.grpc.Channel; -import io.grpc.ClientCall; -import io.grpc.ClientInterceptor; -import io.grpc.ForwardingClientCall; +import io.grpc.*; import io.grpc.ForwardingClientCallListener.SimpleForwardingClientCallListener; -import io.grpc.Metadata; -import io.grpc.MethodDescriptor; -import io.grpc.Status; + +import javax.annotation.Nullable; /** *

gRPC client interceptor for Sentinel. Currently it only works with unary methods.

- * + *

* Example code: *

  * public class ServiceClient {
@@ -52,7 +44,7 @@ import io.grpc.Status;
  *
  * }
  * 
- * + *

* For server interceptor, see {@link SentinelGrpcServerInterceptor}. * * @author Eric Zhao @@ -60,18 +52,20 @@ import io.grpc.Status; public class SentinelGrpcClientInterceptor implements ClientInterceptor { private static final Status FLOW_CONTROL_BLOCK = Status.UNAVAILABLE.withDescription( - "Flow control limit exceeded (client side)"); + "Flow control limit exceeded (client side)"); @Override public ClientCall interceptCall(MethodDescriptor methodDescriptor, CallOptions callOptions, Channel channel) { String resourceName = methodDescriptor.getFullMethodName(); - Entry entry = null; + AsyncEntry asyncEntry = null; try { - entry = SphU.entry(resourceName, EntryType.OUT); + asyncEntry = SphU.asyncEntry(resourceName, EntryType.OUT); + + final AsyncEntry tempEntry = asyncEntry; // Allow access, forward the call. return new ForwardingClientCall.SimpleForwardingClientCall( - channel.newCall(methodDescriptor, callOptions)) { + channel.newCall(methodDescriptor, callOptions)) { @Override public void start(Listener responseListener, Metadata headers) { super.start(new SimpleForwardingClientCallListener(responseListener) { @@ -85,18 +79,14 @@ public class SentinelGrpcClientInterceptor implements ClientInterceptor { super.onClose(status, trailers); // Record the exception metrics. if (!status.isOk()) { - recordException(status.asRuntimeException()); + recordException(status.asRuntimeException(), tempEntry); } + tempEntry.exit(); } }, headers); } - @Override - public void cancel(@Nullable String message, @Nullable Throwable cause) { - super.cancel(message, cause); - // Record the exception metrics. - recordException(cause); - } + }; } catch (BlockException e) { // Flow control threshold exceeded, block the call. @@ -126,14 +116,25 @@ public class SentinelGrpcClientInterceptor implements ClientInterceptor { } }; - } finally { - if (entry != null) { - entry.exit(); + + } catch (RuntimeException e) { + //catch the RuntimeException newCall throws, + // entry is guaranteed to exit + if (asyncEntry != null) { + asyncEntry.exit(); } + throw e; + + } } - private void recordException(Throwable t) { - Tracer.trace(t); + private void recordException(final Throwable t, AsyncEntry asyncEntry) { + ContextUtil.runOnContext(asyncEntry.getAsyncContext(), new Runnable() { + @Override + public void run() { + Tracer.trace(t); + } + }); } } diff --git a/sentinel-adapter/sentinel-grpc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/grpc/SentinelGrpcServerInterceptor.java b/sentinel-adapter/sentinel-grpc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/grpc/SentinelGrpcServerInterceptor.java index 1e0d9090..a9b118ba 100755 --- a/sentinel-adapter/sentinel-grpc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/grpc/SentinelGrpcServerInterceptor.java +++ b/sentinel-adapter/sentinel-grpc-adapter/src/main/java/com/alibaba/csp/sentinel/adapter/grpc/SentinelGrpcServerInterceptor.java @@ -15,25 +15,17 @@ */ package com.alibaba.csp.sentinel.adapter.grpc; -import com.alibaba.csp.sentinel.Entry; +import com.alibaba.csp.sentinel.AsyncEntry; import com.alibaba.csp.sentinel.EntryType; import com.alibaba.csp.sentinel.SphU; import com.alibaba.csp.sentinel.Tracer; import com.alibaba.csp.sentinel.context.ContextUtil; import com.alibaba.csp.sentinel.slots.block.BlockException; - -import io.grpc.ForwardingServerCall; -import io.grpc.ForwardingServerCallListener; -import io.grpc.Metadata; -import io.grpc.ServerCall; -import io.grpc.ServerCall.Listener; -import io.grpc.ServerCallHandler; -import io.grpc.ServerInterceptor; -import io.grpc.Status; +import io.grpc.*; /** *

gRPC server interceptor for Sentinel. Currently it only works with unary methods.

- * + *

* Example code: *

  * Server server = ServerBuilder.forPort(port)
@@ -41,7 +33,7 @@ import io.grpc.Status;
  *      .intercept(new SentinelGrpcServerInterceptor()) // Add the server interceptor.
  *      .build();
  * 
- * + *

* For client interceptor, see {@link SentinelGrpcClientInterceptor}. * * @author Eric Zhao @@ -49,42 +41,65 @@ import io.grpc.Status; public class SentinelGrpcServerInterceptor implements ServerInterceptor { private static final Status FLOW_CONTROL_BLOCK = Status.UNAVAILABLE.withDescription( - "Flow control limit exceeded (server side)"); + "Flow control limit exceeded (server side)"); @Override - public Listener interceptCall(ServerCall serverCall, Metadata metadata, - ServerCallHandler serverCallHandler) { - String resourceName = serverCall.getMethodDescriptor().getFullMethodName(); + public ServerCall.Listener interceptCall(ServerCall call, Metadata headers, ServerCallHandler next) { + String resourceName = call.getMethodDescriptor().getFullMethodName(); // Remote address: serverCall.getAttributes().get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR); - Entry entry = null; + AsyncEntry entry = null; try { ContextUtil.enter(resourceName); - entry = SphU.entry(resourceName, EntryType.IN); + entry = SphU.asyncEntry(resourceName, EntryType.IN); // Allow access, forward the call. + final AsyncEntry tempEntry = entry; return new ForwardingServerCallListener.SimpleForwardingServerCallListener( - serverCallHandler.startCall( - new ForwardingServerCall.SimpleForwardingServerCall(serverCall) { - @Override - public void close(Status status, Metadata trailers) { - super.close(status, trailers); - // Record the exception metrics. - if (!status.isOk()) { - recordException(status.asRuntimeException()); - } - } - }, metadata)) {}; + next.startCall( + new ForwardingServerCall.SimpleForwardingServerCall(call) { + @Override + public void close(Status status, Metadata trailers) { + super.close(status, trailers); + // Record the exception metrics. + if (!status.isOk()) { + recordException(status.asException(), tempEntry); + } + //entry exit when the call be closed + tempEntry.exit(); + } + }, headers)) { + + /** + * if call was canceled, onCancel will be called. and the close will not be called + * so the server is encouraged to abort processing to save resources by onCancel + * @see ServerCall.Listener#onCancel() + */ + @Override + public void onCancel() { + super.onCancel(); + // request has be canceled, entry should exit + tempEntry.exit(); + } + }; } catch (BlockException e) { - serverCall.close(FLOW_CONTROL_BLOCK, new Metadata()); - return new ServerCall.Listener() {}; - } finally { + call.close(FLOW_CONTROL_BLOCK, new Metadata()); + return new ServerCall.Listener() { + }; + } catch (RuntimeException e) { + //catch the RuntimeException startCall throws, + // entry is guaranteed to exit if (entry != null) { entry.exit(); } - ContextUtil.exit(); + throw e; } } - private void recordException(Throwable t) { - Tracer.trace(t); + private void recordException(final Throwable t, AsyncEntry tempEntry) { + ContextUtil.runOnContext(tempEntry.getAsyncContext(), new Runnable() { + @Override + public void run() { + Tracer.trace(t); + } + }); } } diff --git a/sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/FooServiceClient.java b/sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/FooServiceClient.java index dab36678..c72e732a 100755 --- a/sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/FooServiceClient.java +++ b/sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/FooServiceClient.java @@ -37,16 +37,16 @@ final class FooServiceClient { FooServiceClient(String host, int port) { this.channel = ManagedChannelBuilder.forAddress(host, port) - .usePlaintext() - .build(); + .usePlaintext() + .build(); this.blockingStub = FooServiceGrpc.newBlockingStub(this.channel); } FooServiceClient(String host, int port, ClientInterceptor interceptor) { this.channel = ManagedChannelBuilder.forAddress(host, port) - .usePlaintext() - .intercept(interceptor) - .build(); + .usePlaintext() + .intercept(interceptor) + .build(); this.blockingStub = FooServiceGrpc.newBlockingStub(this.channel); } @@ -57,6 +57,7 @@ final class FooServiceClient { return blockingStub.sayHello(request); } + FooResponse anotherHello(FooRequest request) { if (request == null) { throw new IllegalArgumentException("Request cannot be null"); @@ -64,6 +65,21 @@ final class FooServiceClient { return blockingStub.anotherHello(request); } + FooResponse helloWithEx(FooRequest request) { + if (request == null) { + throw new IllegalArgumentException("Request cannot be null"); + } + return blockingStub.helloWithEx(request); + } + + + FooResponse anotherHelloWithEx(FooRequest request) { + if (request == null) { + throw new IllegalArgumentException("Request cannot be null"); + } + return blockingStub.anotherHelloWithEx(request); + } + void shutdown() throws InterruptedException { channel.shutdown().awaitTermination(1, TimeUnit.SECONDS); } diff --git a/sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/FooServiceImpl.java b/sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/FooServiceImpl.java index adda5102..832c6247 100755 --- a/sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/FooServiceImpl.java +++ b/sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/FooServiceImpl.java @@ -18,7 +18,6 @@ package com.alibaba.csp.sentinel.adapter.grpc; import com.alibaba.csp.sentinel.adapter.grpc.gen.FooRequest; import com.alibaba.csp.sentinel.adapter.grpc.gen.FooResponse; import com.alibaba.csp.sentinel.adapter.grpc.gen.FooServiceGrpc; - import io.grpc.stub.StreamObserver; /** @@ -28,7 +27,9 @@ class FooServiceImpl extends FooServiceGrpc.FooServiceImplBase { @Override public void sayHello(FooRequest request, StreamObserver responseObserver) { + String message = String.format("Hello %s! Your ID is %d.", request.getName(), request.getId()); + FooResponse response = FooResponse.newBuilder().setMessage(message).build(); responseObserver.onNext(response); responseObserver.onCompleted(); @@ -36,6 +37,29 @@ class FooServiceImpl extends FooServiceGrpc.FooServiceImplBase { @Override public void anotherHello(FooRequest request, StreamObserver responseObserver) { + + String message = String.format("Good day, %s (%d)", request.getName(), request.getId()); + FooResponse response = FooResponse.newBuilder().setMessage(message).build(); + responseObserver.onNext(response); + responseObserver.onCompleted(); + } + @Override + public void helloWithEx(FooRequest request, StreamObserver responseObserver) { + if (request.getId() == -1) { + responseObserver.onError(new IllegalAccessException("The id is error")); + return; + } + String message = String.format("Good day, %s (%d)", request.getName(), request.getId()); + FooResponse response = FooResponse.newBuilder().setMessage(message).build(); + responseObserver.onNext(response); + responseObserver.onCompleted(); + } + @Override + public void anotherHelloWithEx(FooRequest request, StreamObserver responseObserver) { + if (request.getId() == -1) { + responseObserver.onError(new IllegalAccessException("The id is error")); + return; + } String message = String.format("Good day, %s (%d)", request.getName(), request.getId()); FooResponse response = FooResponse.newBuilder().setMessage(message).build(); responseObserver.onNext(response); diff --git a/sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/GrpcTestServer.java b/sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/GrpcTestServer.java index f46c5718..86fe0115 100644 --- a/sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/GrpcTestServer.java +++ b/sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/GrpcTestServer.java @@ -19,6 +19,7 @@ import io.grpc.Server; import io.grpc.ServerBuilder; import java.io.IOException; +import java.util.concurrent.Executors; class GrpcTestServer { diff --git a/sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/SentinelGrpcClientInterceptorDegradeTest.java b/sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/SentinelGrpcClientInterceptorDegradeTest.java new file mode 100644 index 00000000..742ba676 --- /dev/null +++ b/sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/SentinelGrpcClientInterceptorDegradeTest.java @@ -0,0 +1,98 @@ +package com.alibaba.csp.sentinel.adapter.grpc; + +import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.node.ClusterNode; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager; +import com.alibaba.csp.sentinel.adapter.grpc.gen.FooRequest; +import com.alibaba.csp.sentinel.adapter.grpc.gen.FooResponse; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; +import com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot; +import io.grpc.StatusRuntimeException; +import org.junit.After; +import org.junit.Test; + +import java.io.IOException; +import java.util.Collections; + +import static org.junit.Assert.*; + +/** + * @author zhengzechao + * @date 2018/12/7 + * Email ooczzoo@gmail.com + */ +public class SentinelGrpcClientInterceptorDegradeTest { + + private final String resourceName = "com.alibaba.sentinel.examples.FooService/helloWithEx"; + private final int threshold = 2; + private final GrpcTestServer server = new GrpcTestServer(); + private final int timeWindow = 10; + + + + + private void configureDegradeRule(int count) { + DegradeRule rule = new DegradeRule() + .setCount(count) + .setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT) + .setResource(resourceName) + .setLimitApp("default") + .as(DegradeRule.class) + .setTimeWindow(timeWindow); + DegradeRuleManager.loadRules(Collections.singletonList(rule)); + + + } + + + private boolean sendRequest(FooServiceClient client) { + try { + FooResponse response = client.helloWithEx(FooRequest.newBuilder().setName("Sentinel").setId(666).build()); + System.out.println("Response: " + response); + return true; + } catch (StatusRuntimeException ex) { + System.out.println("Blocked, cause: " + ex.getMessage()); + return false; + } + } + + + @Test + public void testGrpcClientInterceptor_degrade() throws IOException { + final int port = 19316; + + configureDegradeRule(1); + server.start(port, false); + + FooServiceClient client = new FooServiceClient("localhost", port, new SentinelGrpcClientInterceptor()); + + assertFalse(sendErrorRequest(client)); + ClusterNode clusterNode = ClusterBuilderSlot.getClusterNode(resourceName, EntryType.OUT); + assertNotNull(clusterNode); + assertEquals(1, clusterNode.exceptionQps()); + // The second request will be blocked. + assertFalse(sendRequest(client)); + assertEquals(1, clusterNode.blockRequest()); + + server.stop(); + } + + private boolean sendErrorRequest(FooServiceClient client) { + try { + FooResponse response = client.helloWithEx(FooRequest.newBuilder().setName("Sentinel").setId(-1).build()); + System.out.println("Response: " + response); + return true; + } catch (StatusRuntimeException ex) { + System.out.println("Blocked, cause: " + ex.getMessage()); + return false; + } + } + + @After + public void cleanUp() { + FlowRuleManager.loadRules(null); + ClusterBuilderSlot.getClusterNodeMap().clear(); + } +} diff --git a/sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/SentinelGrpcServerInterceptorDegradeTest.java b/sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/SentinelGrpcServerInterceptorDegradeTest.java new file mode 100644 index 00000000..19b22ae0 --- /dev/null +++ b/sentinel-adapter/sentinel-grpc-adapter/src/test/java/com/alibaba/csp/sentinel/adapter/grpc/SentinelGrpcServerInterceptorDegradeTest.java @@ -0,0 +1,109 @@ +package com.alibaba.csp.sentinel.adapter.grpc; + +import com.alibaba.csp.sentinel.EntryType; +import com.alibaba.csp.sentinel.node.ClusterNode; +import com.alibaba.csp.sentinel.adapter.grpc.gen.FooRequest; +import com.alibaba.csp.sentinel.adapter.grpc.gen.FooResponse; +import com.alibaba.csp.sentinel.slots.block.RuleConstant; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule; +import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager; +import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager; +import com.alibaba.csp.sentinel.slots.clusterbuilder.ClusterBuilderSlot; +import io.grpc.StatusRuntimeException; +import org.junit.After; +import org.junit.Test; + +import java.io.IOException; +import java.util.Collections; +import java.util.concurrent.CountDownLatch; + +import static org.junit.Assert.*; + +/** + * @author zhengzechao + * @date 2018/12/7 + * Email ooczzoo@gmail.com + */ +public class SentinelGrpcServerInterceptorDegradeTest { + + private final String resourceName = "com.alibaba.sentinel.examples.FooService/anotherHelloWithEx"; + private final int threshold = 4; + private final GrpcTestServer server = new GrpcTestServer(); + private final int timeWindow = 10; + private FooServiceClient client; + + + + private void configureDegradeRule(int count) { + DegradeRule rule = new DegradeRule() + .setCount(count) + .setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT) + .setResource(resourceName) + .setLimitApp("default") + .as(DegradeRule.class) + .setTimeWindow(timeWindow); + DegradeRuleManager.loadRules(Collections.singletonList(rule)); + + + } + + + private boolean sendRequest() { + try { + FooResponse response = client.anotherHelloWithEx(FooRequest.newBuilder().setName("Sentinel").setId(666).build()); + System.out.println("Response: " + response); + return true; + } catch (StatusRuntimeException ex) { + System.out.println("Blocked, cause: " + ex.getMessage()); + return false; + } + } + + + + + + @Test + public void testGrpcServerInterceptor_degrade_fail_threads() throws IOException, InterruptedException { + final int port = 19349; + client = new FooServiceClient("localhost", port); + server.start(port, true); + // exception count = 1 + configureDegradeRule(20); + final CountDownLatch latch = new CountDownLatch(20); + + for (int i = 0; i < 20; i++) { + new Thread(new Runnable() { + @Override + public void run() { + assertFalse(sendErrorRequest()); + latch.countDown(); + } + }).start(); + } + latch.await(); + assertFalse(sendRequest()); + ClusterNode clusterNode = ClusterBuilderSlot.getClusterNode(resourceName, EntryType.IN); + assertEquals(20, clusterNode.totalException()); + assertEquals(1, clusterNode.blockRequest()); + + } + + private boolean sendErrorRequest() { + try { + FooResponse response = client.anotherHelloWithEx(FooRequest.newBuilder().setName("Sentinel").setId(-1).build()); + System.out.println("Response: " + response); + return true; + } catch (StatusRuntimeException ex) { + System.out.println("Blocked, cause: " + ex.getMessage()); + return false; + } + } + + + @After + public void cleanUp() { + FlowRuleManager.loadRules(null); + ClusterBuilderSlot.getClusterNodeMap().clear(); + } +} diff --git a/sentinel-adapter/sentinel-grpc-adapter/src/test/proto/example.proto b/sentinel-adapter/sentinel-grpc-adapter/src/test/proto/example.proto index 61858510..2b25b1e1 100755 --- a/sentinel-adapter/sentinel-grpc-adapter/src/test/proto/example.proto +++ b/sentinel-adapter/sentinel-grpc-adapter/src/test/proto/example.proto @@ -17,7 +17,14 @@ message FooResponse { // Example service definition. service FooService { + + rpc sayHello(FooRequest) returns (FooResponse) {} rpc anotherHello(FooRequest) returns (FooResponse) {} + + rpc helloWithEx(FooRequest) returns (FooResponse) {} + rpc anotherHelloWithEx(FooRequest) returns (FooResponse) {} + + } \ No newline at end of file