Skip to content

Commit 4efb349

Browse files
author
Alexander Furer
committed
closes #223
1 parent 926fb1b commit 4efb349

File tree

13 files changed

+728
-32
lines changed

13 files changed

+728
-32
lines changed
Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
package org.lognet.springboot.grpc.recovery;
2+
3+
import io.grpc.Metadata;
4+
import io.grpc.Status;
5+
import io.grpc.StatusRuntimeException;
6+
import io.grpc.examples.custom.Custom;
7+
import io.grpc.examples.custom.CustomServiceGrpc;
8+
import io.grpc.stub.StreamObserver;
9+
import org.junit.Test;
10+
import org.junit.runner.RunWith;
11+
import org.lognet.springboot.grpc.GRpcService;
12+
import org.lognet.springboot.grpc.GrpcServerTestBase;
13+
import org.lognet.springboot.grpc.demo.DemoApp;
14+
import org.mockito.Mockito;
15+
import org.springframework.boot.test.context.SpringBootTest;
16+
import org.springframework.boot.test.context.TestConfiguration;
17+
import org.springframework.boot.test.mock.mockito.SpyBean;
18+
import org.springframework.context.annotation.Import;
19+
import org.springframework.test.context.ActiveProfiles;
20+
import org.springframework.test.context.junit4.SpringRunner;
21+
22+
import java.util.concurrent.CompletableFuture;
23+
import java.util.concurrent.ExecutionException;
24+
25+
import static org.hamcrest.MatcherAssert.assertThat;
26+
import static org.hamcrest.Matchers.is;
27+
import static org.hamcrest.Matchers.isA;
28+
import static org.hamcrest.Matchers.notNullValue;
29+
import static org.junit.jupiter.api.Assertions.assertThrows;
30+
import static org.mockito.ArgumentMatchers.any;
31+
import static org.mockito.Mockito.never;
32+
import static org.mockito.Mockito.times;
33+
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.NONE;
34+
35+
@RunWith(SpringRunner.class)
36+
@SpringBootTest(classes = {DemoApp.class}, webEnvironment = NONE)
37+
@ActiveProfiles({"disable-security"})
38+
@Import(GRpcRecoveryTest.Cfg.class)
39+
public class GRpcRecoveryTest extends GrpcServerTestBase {
40+
41+
static class CheckedException extends Exception {
42+
43+
}
44+
static class CheckedException1 extends Exception {
45+
46+
}
47+
48+
static class ExceptionA extends RuntimeException {
49+
50+
}
51+
52+
static class ExceptionB extends ExceptionA {
53+
54+
}
55+
56+
static class Exception1 extends RuntimeException {
57+
58+
}
59+
60+
@TestConfiguration
61+
static class Cfg {
62+
63+
64+
@GRpcServiceAdvice
65+
static class CustomErrorHandler {
66+
@GRpcExceptionHandler
67+
public Status handleA(ExceptionA e, GRpcExceptionScope scope) {
68+
return Status.NOT_FOUND;
69+
}
70+
71+
@GRpcExceptionHandler
72+
public Status handleB(ExceptionB e, GRpcExceptionScope scope) {
73+
return Status.ALREADY_EXISTS;
74+
}
75+
76+
77+
@GRpcExceptionHandler
78+
public Status handleCheckedException(CheckedException e, GRpcExceptionScope scope) {
79+
return Status.OUT_OF_RANGE;
80+
}
81+
82+
@GRpcExceptionHandler
83+
public Status handle(Exception e, GRpcExceptionScope scope) {
84+
return Status.DATA_LOSS;
85+
}
86+
}
87+
88+
89+
@GRpcService
90+
static class CustomService extends CustomServiceGrpc.CustomServiceImplBase {
91+
92+
@Override
93+
public void custom(Custom.CustomRequest request, StreamObserver<Custom.CustomReply> responseObserver) {
94+
super.custom(request, responseObserver);
95+
}
96+
97+
@Override
98+
public StreamObserver<Custom.CustomRequest> customStream(StreamObserver<Custom.CustomReply> responseObserver) {
99+
100+
return new StreamObserver<Custom.CustomRequest>() {
101+
@Override
102+
public void onNext(Custom.CustomRequest value) {
103+
responseObserver.onNext(Custom.CustomReply.newBuilder().build());
104+
}
105+
106+
@Override
107+
public void onError(Throwable t) {
108+
109+
}
110+
111+
@Override
112+
public void onCompleted() {
113+
throw new GRpcRuntimeExceptionWrapper(new CheckedException1());
114+
}
115+
};
116+
}
117+
118+
@GRpcExceptionHandler
119+
public Status handle(CheckedException1 e, GRpcExceptionScope scope) {
120+
return Status.RESOURCE_EXHAUSTED;
121+
}
122+
123+
@GRpcExceptionHandler
124+
public Status handleB(ExceptionB e, GRpcExceptionScope scope) {
125+
126+
assertThat(scope.getResponseHeaders(), notNullValue());
127+
scope.getResponseHeaders().put(Metadata.Key.of("test", Metadata.ASCII_STRING_MARSHALLER), "5");
128+
129+
assertThat(scope.getRequest(), notNullValue());
130+
assertThat(scope.getRequest(), isA(Custom.CustomRequest.class));
131+
132+
assertThat(scope.getCallHeaders(), notNullValue());
133+
assertThat(scope.getMethodCallAttributes(), notNullValue());
134+
assertThat(scope.getMethodDescriptor(), notNullValue());
135+
136+
137+
return Status.FAILED_PRECONDITION;
138+
}
139+
140+
141+
}
142+
143+
}
144+
145+
@SpyBean
146+
private Cfg.CustomService srv;
147+
148+
@SpyBean
149+
private Cfg.CustomErrorHandler handler;
150+
151+
152+
@Test
153+
public void streamingServiceErrorHandlerTest() throws ExecutionException, InterruptedException {
154+
155+
156+
157+
final CompletableFuture<Throwable> errorFuture = new CompletableFuture<>();
158+
final StreamObserver<Custom.CustomReply> reply = new StreamObserver<Custom.CustomReply>() {
159+
160+
@Override
161+
public void onNext(Custom.CustomReply value) {
162+
163+
}
164+
165+
@Override
166+
public void onError(Throwable t) {
167+
errorFuture.complete(t);
168+
}
169+
170+
@Override
171+
public void onCompleted() {
172+
errorFuture.complete(null);
173+
}
174+
};
175+
176+
final StreamObserver<Custom.CustomRequest> requests = CustomServiceGrpc.newStub(getChannel()).customStream(reply);
177+
requests.onNext(Custom.CustomRequest.newBuilder().build());
178+
requests.onCompleted();
179+
180+
181+
182+
183+
184+
final Throwable actual = errorFuture.get();
185+
assertThat(actual, notNullValue());
186+
assertThat(actual, isA(StatusRuntimeException.class));
187+
assertThat(((StatusRuntimeException)actual).getStatus(), is(Status.RESOURCE_EXHAUSTED));
188+
189+
Mockito.verify(srv,times(1)).handle(any(CheckedException1.class),any());
190+
191+
}
192+
193+
@Test
194+
public void checkedExceptionHandlerTest() {
195+
Mockito.doThrow(new GRpcRuntimeExceptionWrapper(new CheckedException()))
196+
.when(srv)
197+
.custom(any(Custom.CustomRequest.class), any(StreamObserver.class));
198+
199+
final StatusRuntimeException statusRuntimeException = assertThrows(StatusRuntimeException.class, () ->
200+
CustomServiceGrpc.newBlockingStub(getChannel()).custom(Custom.CustomRequest.newBuilder().build())
201+
);
202+
assertThat(statusRuntimeException.getStatus(), is(Status.OUT_OF_RANGE));
203+
204+
Mockito.verify(srv, never()).handleB(any(), any());
205+
206+
207+
Mockito.verify(handler, never()).handle(any(), any());
208+
Mockito.verify(handler, never()).handleA(any(), any());
209+
Mockito.verify(handler, times(1)).handleCheckedException(any(CheckedException.class), any());
210+
Mockito.verify(handler, never()).handleB(any(), any());
211+
212+
213+
}
214+
215+
@Test
216+
public void globalHandlerTest() {
217+
Mockito.doThrow(ExceptionA.class)
218+
.when(srv)
219+
.custom(any(Custom.CustomRequest.class), any(StreamObserver.class));
220+
221+
final StatusRuntimeException statusRuntimeException = assertThrows(StatusRuntimeException.class, () ->
222+
CustomServiceGrpc.newBlockingStub(getChannel()).custom(Custom.CustomRequest.newBuilder().build())
223+
);
224+
assertThat(statusRuntimeException.getStatus(), is(Status.NOT_FOUND));
225+
226+
Mockito.verify(srv, never()).handleB(any(), any());
227+
228+
229+
Mockito.verify(handler, never()).handle(any(), any());
230+
Mockito.verify(handler, times(1)).handleA(any(), any());
231+
Mockito.verify(handler, never()).handleB(any(), any());
232+
233+
234+
}
235+
236+
@Test
237+
public void globalHandlerWithExceptionHierarchyTest() {
238+
Mockito.doThrow(Exception1.class)
239+
.when(srv)
240+
.custom(any(Custom.CustomRequest.class), any(StreamObserver.class));
241+
242+
final StatusRuntimeException statusRuntimeException = assertThrows(StatusRuntimeException.class, () ->
243+
CustomServiceGrpc.newBlockingStub(getChannel()).custom(Custom.CustomRequest.newBuilder().build())
244+
);
245+
assertThat(statusRuntimeException.getStatus(), is(Status.DATA_LOSS));
246+
247+
Mockito.verify(srv, never()).handleB(any(), any());
248+
249+
250+
Mockito.verify(handler, never()).handleA(any(), any());
251+
Mockito.verify(handler, never()).handleB(any(), any());
252+
Mockito.verify(handler, times(1)).handle(any(), any());
253+
254+
255+
}
256+
257+
@Test
258+
public void privateHandlerHasHigherPrecedence() {
259+
Mockito.doThrow(ExceptionB.class)
260+
.when(srv)
261+
.custom(any(Custom.CustomRequest.class), any(StreamObserver.class));
262+
263+
final StatusRuntimeException statusRuntimeException = assertThrows(StatusRuntimeException.class, () ->
264+
CustomServiceGrpc.newBlockingStub(getChannel()).custom(Custom.CustomRequest.newBuilder().build())
265+
);
266+
assertThat(statusRuntimeException.getStatus(), is(Status.FAILED_PRECONDITION));
267+
268+
Mockito.verify(srv, times(1)).handleB(any(), any());
269+
270+
final String testTrailer = statusRuntimeException.getTrailers().get(Metadata.Key.of("test", Metadata.ASCII_STRING_MARSHALLER));
271+
assertThat(testTrailer, is("5"));
272+
273+
274+
Mockito.verify(handler, never()).handle(any(), any());
275+
Mockito.verify(handler, never()).handleA(any(), any());
276+
Mockito.verify(handler, never()).handleB(any(), any());
277+
278+
}
279+
}

grpc-spring-boot-starter-demo/src/test/proto/custom.proto

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ option java_package = "io.grpc.examples.custom";
77
// Custom service definition.
88
service CustomService {
99
rpc Custom ( CustomRequest) returns ( CustomReply) {}
10+
rpc CustomStream (stream CustomRequest) returns (stream CustomReply) {}
1011
}
1112

1213

grpc-spring-boot-starter/src/main/java/org/lognet/springboot/grpc/FailureHandlingServerInterceptor.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import io.grpc.ServerCall;
66
import io.grpc.ServerInterceptor;
77
import io.grpc.Status;
8+
import org.lognet.springboot.grpc.recovery.GRpcExceptionScope;
9+
import org.lognet.springboot.grpc.recovery.HandlerMethod;
810

911
public interface FailureHandlingServerInterceptor extends ServerInterceptor {
1012
default void closeCall(Object o, GRpcErrorHandler errorHandler, ServerCall<?, ?> call, Metadata headers, final Status status, Exception exception){
@@ -13,6 +15,23 @@ default void closeCall(Object o, GRpcErrorHandler errorHandler, ServerCall<?, ?
1315
Status statusToSend = errorHandler.handle(o,status, exception, headers, responseHeaders);
1416
call.close(statusToSend, responseHeaders);
1517

18+
}
19+
default void closeCall(ServerCall<?, ?> call,Throwable e, GRpcExceptionScope excScope, HandlerMethod handlerMethod){
20+
21+
Status statusToSend = Status.INTERNAL;
22+
try {
23+
statusToSend = handlerMethod.invoke(e, excScope);
24+
}catch (Exception handlerException){
25+
26+
org.slf4j.LoggerFactory.getLogger(this.getClass())
27+
.error("Caught exception while executing handler method {}, returning {} status.",
28+
handlerMethod.getMethod(),
29+
statusToSend,
30+
handlerException);
31+
32+
}
33+
call.close(statusToSend, excScope.getResponseHeaders());
34+
1635
}
1736

1837
class MessageBlockingServerCallListener<R> extends ForwardingServerCallListener.SimpleForwardingServerCallListener<R> {

0 commit comments

Comments
 (0)