/* * Copyright 2017 LINE Corporation * * LINE Corporation licenses this file to you 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: * * http://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 com.linecorp.armeria.client.grpc; import static com.linecorp.armeria.common.http.HttpSessionProtocols.HTTP; import static com.linecorp.armeria.grpc.testing.Messages.PayloadType.COMPRESSABLE; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.catchThrowable; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import org.junit.After; import org.junit.Before; import org.junit.ClassRule; import org.junit.Ignore; import org.junit.Test; import org.mockito.ArgumentCaptor; import com.google.common.base.Throwables; import com.google.protobuf.ByteString; import com.linecorp.armeria.client.ClientFactory; import com.linecorp.armeria.client.ClientOption; import com.linecorp.armeria.client.Clients; import com.linecorp.armeria.common.http.FilteredHttpResponse; import com.linecorp.armeria.common.http.HttpHeaders; import com.linecorp.armeria.common.http.HttpObject; import com.linecorp.armeria.common.http.HttpResponse; import com.linecorp.armeria.grpc.testing.Messages.EchoStatus; import com.linecorp.armeria.grpc.testing.Messages.Payload; import com.linecorp.armeria.grpc.testing.Messages.PayloadType; import com.linecorp.armeria.grpc.testing.Messages.ResponseParameters; import com.linecorp.armeria.grpc.testing.Messages.SimpleRequest; import com.linecorp.armeria.grpc.testing.Messages.SimpleResponse; import com.linecorp.armeria.grpc.testing.Messages.StreamingInputCallRequest; import com.linecorp.armeria.grpc.testing.Messages.StreamingInputCallResponse; import com.linecorp.armeria.grpc.testing.Messages.StreamingOutputCallRequest; import com.linecorp.armeria.grpc.testing.Messages.StreamingOutputCallResponse; import com.linecorp.armeria.grpc.testing.TestServiceGrpc; import com.linecorp.armeria.grpc.testing.TestServiceGrpc.TestServiceBlockingStub; import com.linecorp.armeria.grpc.testing.TestServiceGrpc.TestServiceStub; import com.linecorp.armeria.grpc.testing.UnimplementedServiceGrpc; import com.linecorp.armeria.internal.grpc.GrpcHeaderNames; import com.linecorp.armeria.internal.grpc.StreamRecorder; import com.linecorp.armeria.internal.grpc.TestServiceImpl; import com.linecorp.armeria.internal.grpc.TimeoutHeaderUtil; import com.linecorp.armeria.protobuf.EmptyProtos.Empty; import com.linecorp.armeria.server.ServerBuilder; import com.linecorp.armeria.server.grpc.GrpcServiceBuilder; import com.linecorp.armeria.testing.server.ServerRule; import io.grpc.CallOptions; import io.grpc.ClientCall; import io.grpc.Metadata; import io.grpc.Status; import io.grpc.Status.Code; import io.grpc.StatusRuntimeException; import io.grpc.stub.StreamObserver; public class GrpcClientTest { /** * Must be at least {@link #unaryPayloadLength()}, plus some to account for encoding overhead. */ private static final int MAX_MESSAGE_SIZE = 16 * 1024 * 1024; private static final Empty EMPTY = Empty.getDefaultInstance(); private static AtomicReference<HttpHeaders> CLIENT_HEADERS_CAPTURE = new AtomicReference<>(); private static AtomicReference<HttpHeaders> SERVER_TRAILERS_CAPTURE = new AtomicReference<>(); @ClassRule public static ServerRule server = new ServerRule() { @Override protected void configure(ServerBuilder sb) throws Exception { sb.numWorkers(1); sb.port(0, HTTP); sb.defaultMaxRequestLength(MAX_MESSAGE_SIZE); sb.serviceUnder("/", new GrpcServiceBuilder() .addService(new TestServiceImpl(Executors.newSingleThreadScheduledExecutor())) .setMaxInboundMessageSizeBytes(MAX_MESSAGE_SIZE) .setMaxOutboundMessageSizeBytes(MAX_MESSAGE_SIZE) .build() .decorate(TestServiceImpl.EchoRequestHeadersInTrailers::new) .decorate((client, ctx, req) -> { CLIENT_HEADERS_CAPTURE.set(req.headers()); HttpResponse res = client.serve(ctx, req); return new FilteredHttpResponse(res) { private boolean headersReceived; @Override protected HttpObject filter(HttpObject obj) { if (obj instanceof HttpHeaders) { if (!headersReceived) { headersReceived = true; } else { SERVER_TRAILERS_CAPTURE.set((HttpHeaders) obj); } } return obj; } }; })); } }; private TestServiceBlockingStub blockingStub; private TestServiceStub asyncStub; @Before public void setUp() { blockingStub = ClientFactory.DEFAULT .newClient("gproto+" + server.httpUri("/"), TestServiceBlockingStub.class, ClientOption.DEFAULT_MAX_RESPONSE_LENGTH.newValue((long) MAX_MESSAGE_SIZE)); asyncStub = ClientFactory.DEFAULT .newClient("gproto+" + server.httpUri("/"), TestServiceStub.class); } @After public void tearDown() { CLIENT_HEADERS_CAPTURE.set(null); SERVER_TRAILERS_CAPTURE.set(null); } @Test(timeout = 10000) public void emptyUnary() throws Exception { assertThat(blockingStub.emptyCall(EMPTY)).isEqualTo(EMPTY); } @Test(timeout = 10000) public void largeUnary() throws Exception { final SimpleRequest request = SimpleRequest.newBuilder() .setResponseSize(314159) .setResponseType(COMPRESSABLE) .setPayload(Payload.newBuilder() .setBody(ByteString.copyFrom(new byte[271828]))) .build(); final SimpleResponse goldenResponse = SimpleResponse.newBuilder() .setPayload(Payload.newBuilder() .setType(COMPRESSABLE) .setBody(ByteString.copyFrom(new byte[314159]))) .build(); assertThat(blockingStub.unaryCall(request)).isEqualTo(goldenResponse); } @Test(timeout = 10000) public void serverStreaming() throws Exception { final StreamingOutputCallRequest request = StreamingOutputCallRequest.newBuilder() .setResponseType(COMPRESSABLE) .addResponseParameters( ResponseParameters.newBuilder() .setSize(31415)) .addResponseParameters(ResponseParameters.newBuilder() .setSize(9)) .addResponseParameters(ResponseParameters.newBuilder() .setSize(2653)) .addResponseParameters(ResponseParameters.newBuilder() .setSize(58979)) .build(); final List<StreamingOutputCallResponse> goldenResponses = Arrays.asList( StreamingOutputCallResponse.newBuilder() .setPayload(Payload.newBuilder() .setType(COMPRESSABLE) .setBody(ByteString.copyFrom(new byte[31415]))) .build(), StreamingOutputCallResponse.newBuilder() .setPayload(Payload.newBuilder() .setType(COMPRESSABLE) .setBody(ByteString.copyFrom(new byte[9]))) .build(), StreamingOutputCallResponse.newBuilder() .setPayload(Payload.newBuilder() .setType(COMPRESSABLE) .setBody(ByteString.copyFrom(new byte[2653]))) .build(), StreamingOutputCallResponse.newBuilder() .setPayload(Payload.newBuilder() .setType(COMPRESSABLE) .setBody(ByteString.copyFrom(new byte[58979]))) .build()); StreamRecorder<StreamingOutputCallResponse> recorder = StreamRecorder.create(); asyncStub.streamingOutputCall(request, recorder); recorder.awaitCompletion(); assertSuccess(recorder); assertThat(recorder.getValues()).containsExactlyElementsOf(goldenResponses); } @Test(timeout = 10000) public void clientStreaming() throws Exception { final List<StreamingInputCallRequest> requests = Arrays.asList( StreamingInputCallRequest.newBuilder() .setPayload(Payload.newBuilder() .setBody(ByteString.copyFrom(new byte[27182]))) .build(), StreamingInputCallRequest.newBuilder() .setPayload(Payload.newBuilder() .setBody(ByteString.copyFrom(new byte[8]))) .build(), StreamingInputCallRequest.newBuilder() .setPayload(Payload.newBuilder() .setBody(ByteString.copyFrom(new byte[1828]))) .build(), StreamingInputCallRequest.newBuilder() .setPayload(Payload.newBuilder() .setBody(ByteString.copyFrom(new byte[45904]))) .build()); final StreamingInputCallResponse goldenResponse = StreamingInputCallResponse.newBuilder() .setAggregatedPayloadSize(74922) .build(); StreamRecorder<StreamingInputCallResponse> responseObserver = StreamRecorder.create(); StreamObserver<StreamingInputCallRequest> requestObserver = asyncStub.streamingInputCall(responseObserver); for (StreamingInputCallRequest request : requests) { requestObserver.onNext(request); } requestObserver.onCompleted(); assertThat(responseObserver.firstValue().get()).isEqualTo(goldenResponse); responseObserver.awaitCompletion(); } @Test(timeout = 10000) public void pingPong() throws Exception { final List<StreamingOutputCallRequest> requests = Arrays.asList( StreamingOutputCallRequest.newBuilder() .addResponseParameters(ResponseParameters.newBuilder() .setSize(31415)) .setPayload(Payload.newBuilder() .setBody(ByteString.copyFrom(new byte[27182]))) .build(), StreamingOutputCallRequest.newBuilder() .addResponseParameters(ResponseParameters.newBuilder() .setSize(9)) .setPayload(Payload.newBuilder() .setBody(ByteString.copyFrom(new byte[8]))) .build(), StreamingOutputCallRequest.newBuilder() .addResponseParameters(ResponseParameters.newBuilder() .setSize(2653)) .setPayload(Payload.newBuilder() .setBody(ByteString.copyFrom(new byte[1828]))) .build(), StreamingOutputCallRequest.newBuilder() .addResponseParameters(ResponseParameters.newBuilder() .setSize(58979)) .setPayload(Payload.newBuilder() .setBody(ByteString.copyFrom(new byte[45904]))) .build()); final List<StreamingOutputCallResponse> goldenResponses = Arrays.asList( StreamingOutputCallResponse.newBuilder() .setPayload(Payload.newBuilder() .setType(COMPRESSABLE) .setBody(ByteString.copyFrom(new byte[31415]))) .build(), StreamingOutputCallResponse.newBuilder() .setPayload(Payload.newBuilder() .setType(COMPRESSABLE) .setBody(ByteString.copyFrom(new byte[9]))) .build(), StreamingOutputCallResponse.newBuilder() .setPayload(Payload.newBuilder() .setType(COMPRESSABLE) .setBody(ByteString.copyFrom(new byte[2653]))) .build(), StreamingOutputCallResponse.newBuilder() .setPayload(Payload.newBuilder() .setType(COMPRESSABLE) .setBody(ByteString.copyFrom(new byte[58979]))) .build()); final ArrayBlockingQueue<Object> queue = new ArrayBlockingQueue<Object>(5); StreamObserver<StreamingOutputCallRequest> requestObserver = asyncStub.fullDuplexCall(new StreamObserver<StreamingOutputCallResponse>() { @Override public void onNext(StreamingOutputCallResponse response) { queue.add(response); } @Override public void onError(Throwable t) { queue.add(t); } @Override public void onCompleted() { queue.add("Completed"); } }); for (int i = 0; i < requests.size(); i++) { assertThat(queue.peek()).isNull(); requestObserver.onNext(requests.get(i)); assertThat(queue.poll(operationTimeoutMillis(), TimeUnit.MILLISECONDS)).isEqualTo( goldenResponses.get(i)); } requestObserver.onCompleted(); assertThat(queue.poll(operationTimeoutMillis(), TimeUnit.MILLISECONDS)).isEqualTo("Completed"); } @Test(timeout = 10000) public void emptyStream() throws Exception { StreamRecorder<StreamingOutputCallResponse> responseObserver = StreamRecorder.create(); StreamObserver<StreamingOutputCallRequest> requestObserver = asyncStub.fullDuplexCall(responseObserver); requestObserver.onCompleted(); responseObserver.awaitCompletion(operationTimeoutMillis(), TimeUnit.MILLISECONDS); } @Test(timeout = 10000) public void cancelAfterBegin() throws Exception { StreamRecorder<StreamingInputCallResponse> responseObserver = StreamRecorder.create(); StreamObserver<StreamingInputCallRequest> requestObserver = asyncStub.streamingInputCall(responseObserver); requestObserver.onError(new RuntimeException()); responseObserver.awaitCompletion(); assertThat(responseObserver.getValues()).isEmpty(); assertThat(Status.fromThrowable(responseObserver.getError()).getCode()).isEqualTo(Code.CANCELLED); } @Test(timeout = 10000) public void cancelAfterFirstResponse() throws Exception { final StreamingOutputCallRequest request = StreamingOutputCallRequest.newBuilder() .addResponseParameters(ResponseParameters.newBuilder() .setSize(31415)) .setPayload(Payload.newBuilder() .setBody(ByteString.copyFrom(new byte[27182]))) .build(); final StreamingOutputCallResponse goldenResponse = StreamingOutputCallResponse.newBuilder() .setPayload(Payload.newBuilder() .setType( COMPRESSABLE) .setBody(ByteString.copyFrom(new byte[31415]))) .build(); StreamRecorder<StreamingOutputCallResponse> responseObserver = StreamRecorder.create(); StreamObserver<StreamingOutputCallRequest> requestObserver = asyncStub.fullDuplexCall(responseObserver); requestObserver.onNext(request); assertThat(responseObserver.firstValue().get()).isEqualTo(goldenResponse); requestObserver.onError(new RuntimeException()); responseObserver.awaitCompletion(operationTimeoutMillis(), TimeUnit.MILLISECONDS); assertThat(responseObserver.getValues()).hasSize(1); assertThat(Status.fromThrowable(responseObserver.getError()).getCode()).isEqualTo(Code.CANCELLED); } @Test(timeout = 10000) public void fullDuplexCallShouldSucceed() throws Exception { // Build the request. List<Integer> responseSizes = Arrays.asList(50, 100, 150, 200); StreamingOutputCallRequest.Builder streamingOutputBuilder = StreamingOutputCallRequest.newBuilder(); streamingOutputBuilder.setResponseType(COMPRESSABLE); for (Integer size : responseSizes) { streamingOutputBuilder.addResponseParametersBuilder().setSize(size).setIntervalUs(0); } final StreamingOutputCallRequest request = streamingOutputBuilder.build(); StreamRecorder<StreamingOutputCallResponse> recorder = StreamRecorder.create(); StreamObserver<StreamingOutputCallRequest> requestStream = asyncStub.fullDuplexCall(recorder); final int numRequests = 10; List<StreamingOutputCallRequest> requests = new ArrayList<>(numRequests); for (int ix = numRequests; ix > 0; --ix) { requests.add(request); requestStream.onNext(request); } requestStream.onCompleted(); recorder.awaitCompletion(); assertSuccess(recorder); assertThat(recorder.getValues()).hasSize(responseSizes.size() * numRequests); for (int ix = 0; ix < recorder.getValues().size(); ++ix) { StreamingOutputCallResponse response = recorder.getValues().get(ix); assertThat(response.getPayload().getType()).isEqualTo(COMPRESSABLE); int length = response.getPayload().getBody().size(); int expectedSize = responseSizes.get(ix % responseSizes.size()); assertThat(length).isEqualTo(expectedSize).withFailMessage("comparison failed at index " + ix); } } @Test(timeout = 10000) public void halfDuplexCallShouldSucceed() throws Exception { // Build the request. List<Integer> responseSizes = Arrays.asList(50, 100, 150, 200); StreamingOutputCallRequest.Builder streamingOutputBuilder = StreamingOutputCallRequest.newBuilder(); streamingOutputBuilder.setResponseType(COMPRESSABLE); for (Integer size : responseSizes) { streamingOutputBuilder.addResponseParametersBuilder().setSize(size).setIntervalUs(0); } final StreamingOutputCallRequest request = streamingOutputBuilder.build(); StreamRecorder<StreamingOutputCallResponse> recorder = StreamRecorder.create(); StreamObserver<StreamingOutputCallRequest> requestStream = asyncStub.halfDuplexCall(recorder); final int numRequests = 10; List<StreamingOutputCallRequest> requests = new ArrayList<StreamingOutputCallRequest>(numRequests); for (int ix = numRequests; ix > 0; --ix) { requests.add(request); requestStream.onNext(request); } requestStream.onCompleted(); recorder.awaitCompletion(); assertSuccess(recorder); assertThat(recorder.getValues()).hasSize(responseSizes.size() * numRequests); for (int ix = 0; ix < recorder.getValues().size(); ++ix) { StreamingOutputCallResponse response = recorder.getValues().get(ix); assertThat(response.getPayload().getType()).isEqualTo(COMPRESSABLE); int length = response.getPayload().getBody().size(); int expectedSize = responseSizes.get(ix % responseSizes.size()); assertThat(length).isEqualTo(expectedSize).withFailMessage("comparison failed at index " + ix); } } @Test(timeout = 10000) public void serverStreamingShouldBeFlowControlled() throws Exception { final StreamingOutputCallRequest request = StreamingOutputCallRequest.newBuilder() .setResponseType(COMPRESSABLE) .addResponseParameters( ResponseParameters.newBuilder().setSize(100000)) .addResponseParameters( ResponseParameters.newBuilder().setSize(100001)) .build(); final List<StreamingOutputCallResponse> goldenResponses = Arrays.asList( StreamingOutputCallResponse.newBuilder() .setPayload(Payload.newBuilder() .setType(PayloadType.COMPRESSABLE) .setBody(ByteString.copyFrom(new byte[100000]))) .build(), StreamingOutputCallResponse.newBuilder() .setPayload(Payload.newBuilder() .setType(PayloadType.COMPRESSABLE) .setBody(ByteString.copyFrom(new byte[100001]))) .build()); long start = System.nanoTime(); final ArrayBlockingQueue<Object> queue = new ArrayBlockingQueue<Object>(10); ClientCall<StreamingOutputCallRequest, StreamingOutputCallResponse> call = asyncStub.getChannel().newCall(TestServiceGrpc.METHOD_STREAMING_OUTPUT_CALL, CallOptions.DEFAULT); call.start(new ClientCall.Listener<StreamingOutputCallResponse>() { @Override public void onHeaders(Metadata headers) {} @Override public void onMessage(final StreamingOutputCallResponse message) { queue.add(message); } @Override public void onClose(Status status, Metadata trailers) { queue.add(status); } }, new Metadata()); call.sendMessage(request); call.halfClose(); // Time how long it takes to get the first response. call.request(1); assertThat(queue.poll(operationTimeoutMillis(), TimeUnit.MILLISECONDS)).isEqualTo( goldenResponses.get(0)); long firstCallDuration = System.nanoTime() - start; // Without giving additional flow control, make sure that we don't get another response. We wait // until we are comfortable the next message isn't coming. We may have very low nanoTime // resolution (like on Windows) or be using a testing, in-process transport where message // handling is instantaneous. In both cases, firstCallDuration may be 0, so round up sleep time // to at least 1ms. assertThat(queue.poll(Math.max(firstCallDuration * 4, 1 * 1000 * 1000), TimeUnit.NANOSECONDS)).isNull(); // Make sure that everything still completes. call.request(1); assertThat(queue.poll(operationTimeoutMillis(), TimeUnit.MILLISECONDS)).isEqualTo( goldenResponses.get(1)); assertThat(queue.poll(operationTimeoutMillis(), TimeUnit.MILLISECONDS)).isEqualTo(Status.OK); } @Test(timeout = 30000) public void veryLargeRequest() throws Exception { final SimpleRequest request = SimpleRequest.newBuilder() .setPayload(Payload.newBuilder() .setType(PayloadType.COMPRESSABLE) .setBody(ByteString.copyFrom(new byte[unaryPayloadLength()]))) .setResponseSize(10) .setResponseType(PayloadType.COMPRESSABLE) .build(); final SimpleResponse goldenResponse = SimpleResponse.newBuilder() .setPayload(Payload.newBuilder() .setType(PayloadType.COMPRESSABLE) .setBody(ByteString.copyFrom(new byte[10]))) .build(); assertThat(blockingStub.unaryCall(request)).isEqualTo(goldenResponse); } @Test(timeout = 30000) public void veryLargeResponse() throws Exception { final SimpleRequest request = SimpleRequest.newBuilder() .setResponseSize(unaryPayloadLength()) .setResponseType(PayloadType.COMPRESSABLE) .build(); final SimpleResponse goldenResponse = SimpleResponse.newBuilder() .setPayload(Payload.newBuilder() .setType(PayloadType.COMPRESSABLE) .setBody(ByteString.copyFrom(new byte[unaryPayloadLength()]))) .build(); assertThat(blockingStub.unaryCall(request)).isEqualTo(goldenResponse); } @Test(timeout = 10000) public void exchangeHeadersUnaryCall() throws Exception { TestServiceBlockingStub stub = Clients.newDerivedClient( blockingStub, ClientOption.HTTP_HEADERS.newValue( HttpHeaders.of() .set(TestServiceImpl.EXTRA_HEADER_NAME, "dog"))); assertThat(stub.emptyCall(EMPTY)).isNotNull(); // Assert that our side channel object is echoed back in both headers and trailers assertThat(CLIENT_HEADERS_CAPTURE.get().get(TestServiceImpl.EXTRA_HEADER_NAME)).isEqualTo("dog"); assertThat(SERVER_TRAILERS_CAPTURE.get().get(TestServiceImpl.EXTRA_HEADER_NAME)).isEqualTo("dog"); } @Test(timeout = 10000) public void exchangeHeadersStreamingCall() throws Exception { TestServiceStub stub = Clients.newDerivedClient( asyncStub, ClientOption.HTTP_HEADERS.newValue( HttpHeaders.of() .set(TestServiceImpl.EXTRA_HEADER_NAME, "dog"))); List<Integer> responseSizes = Arrays.asList(50, 100, 150, 200); StreamingOutputCallRequest.Builder streamingOutputBuilder = StreamingOutputCallRequest.newBuilder(); streamingOutputBuilder.setResponseType(COMPRESSABLE); for (Integer size : responseSizes) { streamingOutputBuilder.addResponseParametersBuilder().setSize(size).setIntervalUs(0); } final StreamingOutputCallRequest request = streamingOutputBuilder.build(); StreamRecorder<StreamingOutputCallResponse> recorder = StreamRecorder.create(); StreamObserver<StreamingOutputCallRequest> requestStream = stub.fullDuplexCall(recorder); final int numRequests = 10; List<StreamingOutputCallRequest> requests = new ArrayList<>(numRequests); for (int ix = numRequests; ix > 0; --ix) { requests.add(request); requestStream.onNext(request); } requestStream.onCompleted(); recorder.awaitCompletion(); assertSuccess(recorder); assertThat(recorder.getValues()).hasSize(responseSizes.size() * numRequests); // Assert that our side channel object is echoed back in both headers and trailers assertThat(CLIENT_HEADERS_CAPTURE.get().get(TestServiceImpl.EXTRA_HEADER_NAME)).isEqualTo("dog"); assertThat(SERVER_TRAILERS_CAPTURE.get().get(TestServiceImpl.EXTRA_HEADER_NAME)).isEqualTo("dog"); } @Test(timeout = 10000) public void sendsTimeoutHeader() { long configuredTimeoutMinutes = 100; TestServiceBlockingStub stub = Clients.newDerivedClient( blockingStub, ClientOption.DEFAULT_RESPONSE_TIMEOUT_MILLIS.newValue( TimeUnit.MINUTES.toMillis(configuredTimeoutMinutes))); stub.emptyCall(EMPTY); long transferredTimeoutMinutes = TimeUnit.NANOSECONDS.toMinutes( TimeoutHeaderUtil.fromHeaderValue( CLIENT_HEADERS_CAPTURE.get().get(GrpcHeaderNames.GRPC_TIMEOUT))); assertThat(transferredTimeoutMinutes).isEqualTo(configuredTimeoutMinutes); } @Test public void deadlineNotExceeded() { // warm up the channel and JVM blockingStub.emptyCall(Empty.getDefaultInstance()); TestServiceBlockingStub stub = Clients.newDerivedClient( blockingStub, ClientOption.DEFAULT_RESPONSE_TIMEOUT_MILLIS.newValue( TimeUnit.SECONDS.toMillis(10))); stub .streamingOutputCall( StreamingOutputCallRequest.newBuilder() .addResponseParameters( ResponseParameters.newBuilder() .setIntervalUs(0)) .build()) .next(); } @Test(timeout = 10000) public void deadlineExceeded() { // warm up the channel and JVM blockingStub.emptyCall(Empty.getDefaultInstance()); TestServiceBlockingStub stub = Clients.newDerivedClient( blockingStub, ClientOption.DEFAULT_RESPONSE_TIMEOUT_MILLIS.newValue(10L)); StreamingOutputCallRequest request = StreamingOutputCallRequest.newBuilder() .addResponseParameters( ResponseParameters.newBuilder() .setIntervalUs(20000)) .build(); Throwable t = catchThrowable(() -> stub.streamingOutputCall(request).next()); assertThat(t).isInstanceOf(StatusRuntimeException.class); // (anuraag): As GRPC supports handling timeouts in the server or client due to the grpc-timeout // header, it's not guaranteed which is the source of this error. // Until https://github.com/line/armeria/issues/521 a servTODOer side timeout will not have the correct // status so we don't verify it for now. //assertThat(((StatusRuntimeException) t).getStatus().getCode()) //.isEqualTo(Status.DEADLINE_EXCEEDED.getCode()); } @Test(timeout = 10000) public void deadlineExceededServerStreaming() throws Exception { // warm up the channel and JVM blockingStub.emptyCall(Empty.getDefaultInstance()); ResponseParameters.Builder responseParameters = ResponseParameters.newBuilder() .setSize(1) .setIntervalUs(20000); StreamingOutputCallRequest request = StreamingOutputCallRequest.newBuilder() .setResponseType(PayloadType.COMPRESSABLE) .addResponseParameters(responseParameters) .addResponseParameters(responseParameters) .addResponseParameters(responseParameters) .addResponseParameters(responseParameters) .build(); StreamRecorder<StreamingOutputCallResponse> recorder = StreamRecorder.create(); TestServiceStub stub = Clients.newDerivedClient( asyncStub, ClientOption.DEFAULT_RESPONSE_TIMEOUT_MILLIS.newValue(30L)); stub.streamingOutputCall(request, recorder); recorder.awaitCompletion(); // TODO(anuraag): As GRPC supports handling timeouts in the server or client due to the grpc-timeout // header, it's not guaranteed which is the source of this error. // Until https://github.com/line/armeria/issues/521 a server side timeout will not have the correct // status so we don't verify it for now. assertThat(recorder.getError()).isNotNull(); //assertThat(Status.fromThrowable(recorder.getError()).getCode()) //.isEqualTo(Status.DEADLINE_EXCEEDED.getCode()); } // NB: It's unclear when anyone would set a negative timeout, and trying to set the negative timeout // into a header correctly raises an exception. The test has been copied over from upstream to make it // easier to understand the compatibility test coverage - not sure why the GRPC test doesn't fail but it // doesn't seem worth investigating too hard on this one. @Ignore @Test(timeout = 10000) public void deadlineInPast() throws Exception { // Test once with idle channel and once with active channel TestServiceGrpc.TestServiceBlockingStub stub = Clients.newDerivedClient( blockingStub, ClientOption.DEFAULT_RESPONSE_TIMEOUT_MILLIS.newValue(TimeUnit.SECONDS.toMillis(-10))); stub.emptyCall(EMPTY); Throwable t = catchThrowable(() -> stub.emptyCall(EMPTY)); assertThat(t).isInstanceOf(StatusRuntimeException.class); assertThat(((StatusRuntimeException) t).getStatus().getCode()); // warm up the channel blockingStub.emptyCall(Empty.getDefaultInstance()); t = catchThrowable(() -> stub.emptyCall(EMPTY)); assertThat(t).isInstanceOf(StatusRuntimeException.class); assertThat(((StatusRuntimeException) t).getStatus().getCode()); } @Test(timeout = 10000) public void maxInboundSize_exact() { StreamingOutputCallRequest request = StreamingOutputCallRequest.newBuilder() .addResponseParameters(ResponseParameters.newBuilder().setSize(1)) .build(); int size = blockingStub.streamingOutputCall(request).next().getSerializedSize(); TestServiceBlockingStub stub = Clients.newDerivedClient( blockingStub, GrpcClientOptions.MAX_INBOUND_MESSAGE_SIZE_BYTES.newValue(size)); stub.streamingOutputCall(request).next(); } @Test(timeout = 10000) public void maxInboundSize_tooBig() { StreamingOutputCallRequest request = StreamingOutputCallRequest.newBuilder() .addResponseParameters(ResponseParameters.newBuilder().setSize(1)) .build(); int size = blockingStub.streamingOutputCall(request).next().getSerializedSize(); TestServiceBlockingStub stub = Clients.newDerivedClient( blockingStub, GrpcClientOptions.MAX_INBOUND_MESSAGE_SIZE_BYTES.newValue(size - 1)); Throwable t = catchThrowable(() -> stub.streamingOutputCall(request).next()); assertThat(t).isInstanceOf(StatusRuntimeException.class); assertThat(((StatusRuntimeException) t).getStatus().getCode()).isEqualTo(Code.RESOURCE_EXHAUSTED); assertThat(Throwables.getStackTraceAsString(t)).contains("exceeds maximum"); } @Test(timeout = 10000) public void maxOutboundSize_exact() { // set at least one field to ensure the size is non-zero. StreamingOutputCallRequest request = StreamingOutputCallRequest.newBuilder() .addResponseParameters(ResponseParameters.newBuilder().setSize(1)) .build(); TestServiceBlockingStub stub = Clients.newDerivedClient( blockingStub, GrpcClientOptions.MAX_OUTBOUND_MESSAGE_SIZE_BYTES.newValue( request.getSerializedSize())); stub.streamingOutputCall(request).next(); } @Test(timeout = 10000) public void maxOutboundSize_tooBig() { // set at least one field to ensure the size is non-zero. StreamingOutputCallRequest request = StreamingOutputCallRequest.newBuilder() .addResponseParameters(ResponseParameters.newBuilder().setSize(1)) .build(); TestServiceBlockingStub stub = Clients.newDerivedClient( blockingStub, GrpcClientOptions.MAX_OUTBOUND_MESSAGE_SIZE_BYTES.newValue( request.getSerializedSize() - 1)); Throwable t = catchThrowable(() -> stub.streamingOutputCall(request).next()); assertThat(t).isInstanceOf(StatusRuntimeException.class); assertThat(((StatusRuntimeException) t).getStatus().getCode()).isEqualTo(Code.CANCELLED); assertThat(Throwables.getStackTraceAsString(t)).contains("message too large"); } @Test(timeout = 10000) public void statusCodeAndMessage() throws Exception { int errorCode = 2; String errorMessage = "test status message"; EchoStatus responseStatus = EchoStatus.newBuilder() .setCode(errorCode) .setMessage(errorMessage) .build(); SimpleRequest simpleRequest = SimpleRequest.newBuilder() .setResponseStatus(responseStatus) .build(); StreamingOutputCallRequest streamingRequest = StreamingOutputCallRequest.newBuilder() .setResponseStatus( responseStatus) .build(); // Test UnaryCall Throwable t = catchThrowable(() -> blockingStub.unaryCall(simpleRequest)); assertThat(t).isInstanceOf(StatusRuntimeException.class); StatusRuntimeException e = (StatusRuntimeException) t; assertThat(e.getStatus().getCode()).isEqualTo(Status.UNKNOWN.getCode()); assertThat(e.getStatus().getDescription()).isEqualTo(errorMessage); // Test FullDuplexCall @SuppressWarnings("unchecked") StreamObserver<StreamingOutputCallResponse> responseObserver = mock(StreamObserver.class); StreamObserver<StreamingOutputCallRequest> requestObserver = asyncStub.fullDuplexCall(responseObserver); requestObserver.onNext(streamingRequest); requestObserver.onCompleted(); ArgumentCaptor<Throwable> captor = ArgumentCaptor.forClass(Throwable.class); verify(responseObserver, timeout(operationTimeoutMillis())).onError(captor.capture()); assertThat(Status.fromThrowable(captor.getValue()).getCode()).isEqualTo(Status.UNKNOWN.getCode()); assertThat(Status.fromThrowable(captor.getValue()).getDescription()).isEqualTo(errorMessage); verifyNoMoreInteractions(responseObserver); } /** Sends an rpc to an unimplemented method within TestService. */ @Test(timeout = 10000) public void unimplementedMethod() { Throwable t = catchThrowable(() -> blockingStub.unimplementedCall(Empty.getDefaultInstance())); assertThat(t).isInstanceOf(StatusRuntimeException.class); assertThat(((StatusRuntimeException) t).getStatus().getCode()) .isEqualTo(Status.UNIMPLEMENTED.getCode()); } @Test(timeout = 10000) public void unimplementedService() { UnimplementedServiceGrpc.UnimplementedServiceBlockingStub stub = UnimplementedServiceGrpc.newBlockingStub(asyncStub.getChannel()); Throwable t = catchThrowable(() -> stub.unimplementedCall(Empty.getDefaultInstance())); assertThat(t).isInstanceOf(StatusRuntimeException.class); assertThat(((StatusRuntimeException) t).getStatus().getCode()) .isEqualTo(Status.UNIMPLEMENTED.getCode()); } /** Start a fullDuplexCall which the server will not respond, and verify the deadline expires. */ @Test(timeout = 10000) public void timeoutOnSleepingServer() throws Exception { TestServiceStub stub = Clients.newDerivedClient( asyncStub, ClientOption.DEFAULT_RESPONSE_TIMEOUT_MILLIS.newValue(1L)); StreamRecorder<StreamingOutputCallResponse> responseObserver = StreamRecorder.create(); StreamObserver<StreamingOutputCallRequest> requestObserver = stub.fullDuplexCall(responseObserver); StreamingOutputCallRequest request = StreamingOutputCallRequest .newBuilder() .setPayload(Payload.newBuilder() .setBody(ByteString.copyFrom(new byte[27182]))) .addResponseParameters( ResponseParameters.newBuilder() .setIntervalUs((int) TimeUnit.SECONDS.toMicros(10))) .build(); try { requestObserver.onNext(request); // TODO(anuraag): Upstream test does not need to call onCompleted - response timeout is set at the // start of the stream, which seems to make sense. Figure out how to fix this timeout handling in // armeria. requestObserver.onCompleted(); } catch (IllegalStateException expected) { // This can happen if the stream has already been terminated due to deadline exceeded. } responseObserver.awaitCompletion(operationTimeoutMillis(), TimeUnit.MILLISECONDS); assertThat(responseObserver.getValues()).isEmpty(); assertThat(responseObserver.getError()).isNotNull(); // TODO(anuraag): As GRPC supports handling timeouts in the server or client due to the grpc-timeout // header, it's not guaranteed which is the source of this error. // Until https://github.com/line/armeria/issues/521 a server side timeout will not have the correct // status so we don't verify it for now. //assertThat(Status.fromThrowable(responseObserver.getError()).getCode()) //.isEqualTo(Status.DEADLINE_EXCEEDED.getCode()); } private static void assertSuccess(StreamRecorder<?> recorder) { if (recorder.getError() != null) { throw new AssertionError(recorder.getError()); } } private int unaryPayloadLength() { // 10MiB. return 10485760; } private int operationTimeoutMillis() { return 5000; } }