Add sample and integration test

This commit is contained in:
Dave Syer
2024-11-21 13:36:36 +00:00
parent f97487c768
commit c239cfe970
7 changed files with 261 additions and 15 deletions

View File

@@ -2,6 +2,12 @@ package org.springframework.grpc.sample;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.grpc.server.GlobalServerInterceptor;
import org.springframework.grpc.server.exception.GrpcExceptionHandlerInterceptor;
import io.grpc.ServerInterceptor;
import io.grpc.Status;
@SpringBootApplication
public class GrpcServerApplication {
@@ -10,4 +16,15 @@ public class GrpcServerApplication {
SpringApplication.run(GrpcServerApplication.class, args);
}
@GlobalServerInterceptor
@Bean
public ServerInterceptor globalInterceptor() {
return new GrpcExceptionHandlerInterceptor(exception -> {
if (exception instanceof IllegalArgumentException) {
return Status.INVALID_ARGUMENT.withDescription(exception.getMessage());
}
return null;
});
}
}

View File

@@ -18,6 +18,12 @@ public class GrpcServerService extends SimpleGrpc.SimpleImplBase {
@Override
public void sayHello(HelloRequest req, StreamObserver<HelloReply> responseObserver) {
log.info("Hello " + req.getName());
if (req.getName().startsWith("error")) {
throw new IllegalArgumentException("Bad name: " + req.getName());
}
if (req.getName().startsWith("internal")) {
throw new RuntimeException();
}
HelloReply reply = HelloReply.newBuilder().setMessage("Hello ==> " + req.getName()).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();

View File

@@ -17,6 +17,7 @@
package org.springframework.grpc.sample;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertThrows;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
@@ -35,6 +36,8 @@ import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ActiveProfiles;
import io.grpc.ManagedChannel;
import io.grpc.StatusRuntimeException;
import io.grpc.Status.Code;
/**
* More detailed integration tests for {@link GrpcServerFactory gRPC server factories} and
@@ -53,6 +56,32 @@ class GrpcServerIntegrationTests {
}
@Nested
@SpringBootTest
class ServerWithException {
@Test
void specificErrorResponse(@Autowired GrpcChannelFactory channels) {
SimpleGrpc.SimpleBlockingStub client = SimpleGrpc
.newBlockingStub(channels.createChannel("0.0.0.0:0").build());
assertThat(assertThrows(StatusRuntimeException.class,
() -> client.sayHello(HelloRequest.newBuilder().setName("error").build()))
.getStatus()
.getCode()).isEqualTo(Code.INVALID_ARGUMENT);
}
@Test
void defaultErrorResponseIsUnknown(@Autowired GrpcChannelFactory channels) {
SimpleGrpc.SimpleBlockingStub client = SimpleGrpc
.newBlockingStub(channels.createChannel("0.0.0.0:0").build());
assertThat(assertThrows(StatusRuntimeException.class,
() -> client.sayHello(HelloRequest.newBuilder().setName("internal").build()))
.getStatus()
.getCode()).isEqualTo(Code.UNKNOWN);
}
}
@Nested
@SpringBootTest(properties = { "spring.grpc.server.host=0.0.0.0", "spring.grpc.server.port=0",
"spring.grpc.inprocess.enabled=false" })

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2024-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.grpc.server.exception;
import io.grpc.Status;
public class CompositeGrpcExceptionHandler implements GrpcExceptionHandler {
private final GrpcExceptionHandler[] exceptionHandlers;
public CompositeGrpcExceptionHandler(GrpcExceptionHandler... exceptionHandlers) {
this.exceptionHandlers = exceptionHandlers;
}
@Override
public Status handleException(Throwable exception) {
for (GrpcExceptionHandler exceptionHandler : this.exceptionHandlers) {
Status status = exceptionHandler.handleException(exception);
if (status != null) {
return status;
}
}
return null;
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2024-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.grpc.server.exception;
import io.grpc.Status;
/**
* Defines an exception handler for handling exceptions that occur during gRPC server-side
* processing. Implementations of this interface can be used to customize the error
* handling behavior of a gRPC server.
*
* @author Dave Syer
*/
public interface GrpcExceptionHandler {
/**
* Handle the given exception that occurred during gRPC server-side processing.
* @param exception the exception to handle
* @return the status to return to the client, or {@code null} if the exception cannot
* be classified
*/
Status handleException(Throwable exception);
}

View File

@@ -0,0 +1,120 @@
/*
* Copyright 2024-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.grpc.server.exception;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import io.grpc.ForwardingServerCallListener.SimpleForwardingServerCallListener;
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;
/**
* A gRPC {@link ServerInterceptor} that handles exceptions thrown during the processing
* of gRPC calls. It intercepts the call and wraps the {@link ServerCall.Listener} with an
* {@link ExceptionHandlerListener} that catches exceptions in {@code onMessage} and
* {@code onHalfClose} methods, and delegates the exception handling to the provided
* {@link GrpcExceptionHandler}.
*
* <p>
* A fallback mechanism is used to return UNONOWN in case the {@link GrpcExceptionHandler}
* returns a null.
*
* @author Dave Syer
* @see ServerInterceptor
* @see GrpcExceptionHandler
*/
@Order(Ordered.LOWEST_PRECEDENCE)
public class GrpcExceptionHandlerInterceptor implements ServerInterceptor {
private final GrpcExceptionHandler exceptionHandler;
public GrpcExceptionHandlerInterceptor(GrpcExceptionHandler exceptionHandler) {
this.exceptionHandler = exceptionHandler;
}
/**
* Intercepts a gRPC server call to handle exceptions.
* @param <ReqT> the type of the request message
* @param <RespT> the type of the response message
* @param call the server call object
* @param headers the metadata headers for the call
* @param next the next server call handler in the interceptor chain
* @return a listener for the request messages
*/
@Override
public <ReqT, RespT> Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> call, Metadata headers,
ServerCallHandler<ReqT, RespT> next) {
return new ExceptionHandlerListener<>(next.startCall(call, headers), call,
new FallbackHandler(this.exceptionHandler));
}
static class ExceptionHandlerListener<ReqT, RespT> extends SimpleForwardingServerCallListener<ReqT> {
private ServerCall<ReqT, RespT> call;
private GrpcExceptionHandler exceptionHandler;
ExceptionHandlerListener(ServerCall.Listener<ReqT> delegate, ServerCall<ReqT, RespT> call,
GrpcExceptionHandler exceptionHandler) {
super(delegate);
this.call = call;
this.exceptionHandler = exceptionHandler;
}
@Override
public void onMessage(ReqT message) {
try {
super.onMessage(message);
}
catch (Throwable t) {
this.call.close(this.exceptionHandler.handleException(t), new Metadata());
}
}
@Override
public void onHalfClose() {
try {
super.onHalfClose();
}
catch (Throwable t) {
this.call.close(this.exceptionHandler.handleException(t), new Metadata());
}
}
}
static class FallbackHandler implements GrpcExceptionHandler {
private final GrpcExceptionHandler exceptionHandler;
FallbackHandler(GrpcExceptionHandler exceptionHandler) {
this.exceptionHandler = exceptionHandler;
}
@Override
public Status handleException(Throwable exception) {
Status status = this.exceptionHandler.handleException(exception);
return status != null ? status : Status.fromThrowable(exception);
}
}
}

View File

@@ -12,31 +12,29 @@
|spring.grpc.client.default-channel.max-inbound-message-size | |
|spring.grpc.client.default-channel.max-inbound-metadata-size | |
|spring.grpc.client.default-channel.negotiation-type | | The negotiation type for the channel. Default is {@link NegotiationType#PLAINTEXT}.
|spring.grpc.client.default-channel.secure | `+++true+++` | Flag to say that strict SSL checks are not enabled (so the remote certificate could be anonymous).
|spring.grpc.client.default-channel.secure | | Flag to say that strict SSL checks are not enabled (so the remote certificate could be anonymous).
|spring.grpc.client.default-channel.ssl.bundle | | SSL bundle name.
|spring.grpc.client.default-channel.ssl.enabled | | Whether to enable SSL support. Enabled automatically if "bundle" is provided unless specified otherwise.
|spring.grpc.client.default-channel.user-agent | |
|spring.grpc.server.address | | The address to bind to. could be a host:port combination or a pseudo URL like static://host:port. Can not be set if host or port are set independently.
|spring.grpc.server.health.actuator.enabled | `+++true+++` | Whether to adapt Actuator health checks into gRPC health checks.
|spring.grpc.server.health.actuator.enabled | | Whether to adapt Actuator health checks into gRPC health checks.
|spring.grpc.server.health.actuator.endpoints | | List of Actuator health checks to adapt into gRPC health checks.
|spring.grpc.server.health.enabled | `+++true+++` | Whether to auto-configure Health feature on the gRPC server.
|spring.grpc.server.host | `+++*+++` | Server address to bind to. The default is any IP address ('*').
|spring.grpc.server.health.enabled | | Whether to auto-configure Health feature on the gRPC server.
|spring.grpc.server.host | | Server address to bind to. The default is any IP address ('*').
|spring.grpc.server.keep-alive.max-age | | Maximum time a connection may exist before being gracefully terminated (default infinite).
|spring.grpc.server.keep-alive.max-age-grace | | Maximum time for graceful connection termination (default infinite).
|spring.grpc.server.keep-alive.max-idle | | Maximum time a connection can remain idle before being gracefully terminated (default infinite).
|spring.grpc.server.keep-alive.permit-time | `+++5m+++` | Maximum keep-alive time clients are permitted to configure (default 5m).
|spring.grpc.server.keep-alive.permit-without-calls | `+++false+++` | Whether clients are permitted to send keep alive pings when there are no outstanding RPCs on the connection (default false).
|spring.grpc.server.keep-alive.time | `+++2h+++` | Duration without read activity before sending a keep alive ping (default 2h).
|spring.grpc.server.keep-alive.timeout | `+++20s+++` | Maximum time to wait for read activity after sending a keep alive ping. If sender does not receive an acknowledgment within this time, it will close the connection (default 20s).
|spring.grpc.server.max-inbound-message-size | `+++4194304B+++` | Maximum message size allowed to be received by the server (default 4MiB).
|spring.grpc.server.max-inbound-metadata-size | `+++8192B+++` | Maximum metadata size allowed to be received by the server (default 8KiB).
|spring.grpc.server.observations.enabled | `+++true+++` | Whether to enable Observations on the server.
|spring.grpc.server.port | `+++9090+++` | Server port to listen on. When the value is 0, a random available port is selected. The default is 9090.
|spring.grpc.server.reflection.enabled | `+++true+++` | Whether to enable Reflection on the gRPC server.
|spring.grpc.server.shutdown-grace-period | `+++30s+++` | Maximum time to wait for the server to gracefully shutdown. When the value is negative, the server waits forever. When the value is 0, the server will force shutdown immediately. The default is 30 seconds.
|spring.grpc.server.keep-alive.permit-time | | Maximum keep-alive time clients are permitted to configure (default 5m).
|spring.grpc.server.keep-alive.permit-without-calls | | Whether clients are permitted to send keep alive pings when there are no outstanding RPCs on the connection (default false).
|spring.grpc.server.keep-alive.time | | Duration without read activity before sending a keep alive ping (default 2h).
|spring.grpc.server.keep-alive.timeout | | Maximum time to wait for read activity after sending a keep alive ping. If sender does not receive an acknowledgment within this time, it will close the connection (default 20s).
|spring.grpc.server.max-inbound-message-size | | Maximum message size allowed to be received by the server (default 4MiB).
|spring.grpc.server.max-inbound-metadata-size | | Maximum metadata size allowed to be received by the server (default 8KiB).
|spring.grpc.server.port | | Server port to listen on. When the value is 0, a random available port is selected. The default is 9090.
|spring.grpc.server.shutdown-grace-period | | Maximum time to wait for the server to gracefully shutdown. When the value is negative, the server waits forever. When the value is 0, the server will force shutdown immediately. The default is 30 seconds.
|spring.grpc.server.ssl.bundle | | SSL bundle name.
|spring.grpc.server.ssl.client-auth | | Client authentication mode.
|spring.grpc.server.ssl.enabled | | Whether to enable SSL support. Enabled automatically if "bundle" is provided unless specified otherwise.
|spring.grpc.server.ssl.secure | `+++true+++` | Flag to indicate that client authentication is secure (i.e. certificates are checked). Do not set this to false in production.
|spring.grpc.server.ssl.secure | | Flag to indicate that client authentication is secure (i.e. certificates are checked). Do not set this to false in production.
|===