1 /* 2 * Copyright 2016 The gRPC Authors 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package io.grpc.testing.integration; 18 19 import static java.util.concurrent.Executors.newFixedThreadPool; 20 21 import com.google.common.util.concurrent.Futures; 22 import com.google.common.util.concurrent.ListenableFuture; 23 import com.google.common.util.concurrent.ListeningExecutorService; 24 import com.google.common.util.concurrent.MoreExecutors; 25 import com.google.protobuf.ByteString; 26 import io.grpc.ManagedChannel; 27 import io.grpc.Status; 28 import io.grpc.StatusRuntimeException; 29 import io.grpc.netty.NegotiationType; 30 import io.grpc.netty.NettyChannelBuilder; 31 import io.grpc.stub.StreamObserver; 32 import io.grpc.testing.integration.Messages.Payload; 33 import io.grpc.testing.integration.Messages.SimpleRequest; 34 import io.grpc.testing.integration.Messages.SimpleResponse; 35 import java.net.InetAddress; 36 import java.net.InetSocketAddress; 37 import java.net.UnknownHostException; 38 import java.util.ArrayList; 39 import java.util.List; 40 import java.util.Locale; 41 import java.util.concurrent.CountDownLatch; 42 import java.util.concurrent.TimeUnit; 43 import java.util.logging.Level; 44 import java.util.logging.Logger; 45 46 /** 47 * Client application for the {@link TestServiceGrpc.TestServiceImplBase} that runs through a series 48 * of HTTP/2 interop tests. The tests are designed to simulate incorrect behavior on the part of the 49 * server. Some of the test cases require server-side checks and do not have assertions within the 50 * client code. 51 */ 52 public final class Http2Client { 53 private static final Logger logger = Logger.getLogger(Http2Client.class.getName()); 54 55 /** 56 * The main application allowing this client to be launched from the command line. 57 */ main(String[] args)58 public static void main(String[] args) throws Exception { 59 final Http2Client client = new Http2Client(); 60 client.parseArgs(args); 61 client.setUp(); 62 63 Runtime.getRuntime().addShutdownHook(new Thread() { 64 @Override 65 public void run() { 66 try { 67 client.shutdown(); 68 } catch (Exception e) { 69 logger.log(Level.SEVERE, e.getMessage(), e); 70 } 71 } 72 }); 73 74 try { 75 client.run(); 76 } finally { 77 client.shutdown(); 78 } 79 } 80 81 private String serverHost = "localhost"; 82 private int serverPort = 8080; 83 private String testCase = Http2TestCases.RST_AFTER_DATA.name(); 84 85 private Tester tester = new Tester(); 86 private ListeningExecutorService threadpool; 87 88 ManagedChannel channel; 89 TestServiceGrpc.TestServiceBlockingStub blockingStub; 90 TestServiceGrpc.TestServiceStub asyncStub; 91 parseArgs(String[] args)92 private void parseArgs(String[] args) { 93 boolean usage = false; 94 for (String arg : args) { 95 if (!arg.startsWith("--")) { 96 System.err.println("All arguments must start with '--': " + arg); 97 usage = true; 98 break; 99 } 100 String[] parts = arg.substring(2).split("=", 2); 101 String key = parts[0]; 102 if ("help".equals(key)) { 103 usage = true; 104 break; 105 } 106 if (parts.length != 2) { 107 System.err.println("All arguments must be of the form --arg=value"); 108 usage = true; 109 break; 110 } 111 String value = parts[1]; 112 if ("server_host".equals(key)) { 113 serverHost = value; 114 } else if ("server_port".equals(key)) { 115 serverPort = Integer.parseInt(value); 116 } else if ("test_case".equals(key)) { 117 testCase = value; 118 } else { 119 System.err.println("Unknown argument: " + key); 120 usage = true; 121 break; 122 } 123 } 124 if (usage) { 125 Http2Client c = new Http2Client(); 126 System.out.println( 127 "Usage: [ARGS...]" 128 + "\n" 129 + "\n --server_host=HOST Server to connect to. Default " + c.serverHost 130 + "\n --server_port=PORT Port to connect to. Default " + c.serverPort 131 + "\n --test_case=TESTCASE Test case to run. Default " + c.testCase 132 + "\n Valid options:" 133 + validTestCasesHelpText() 134 ); 135 System.exit(1); 136 } 137 } 138 setUp()139 private void setUp() { 140 channel = createChannel(); 141 blockingStub = TestServiceGrpc.newBlockingStub(channel); 142 asyncStub = TestServiceGrpc.newStub(channel); 143 } 144 shutdown()145 private void shutdown() { 146 try { 147 if (channel != null) { 148 channel.shutdownNow(); 149 channel.awaitTermination(1, TimeUnit.SECONDS); 150 } 151 } catch (Exception ex) { 152 throw new RuntimeException(ex); 153 } 154 155 try { 156 if (threadpool != null) { 157 threadpool.shutdownNow(); 158 } 159 } catch (Exception ex) { 160 throw new RuntimeException(ex); 161 } 162 } 163 run()164 private void run() { 165 logger.info("Running test " + testCase); 166 try { 167 runTest(Http2TestCases.fromString(testCase)); 168 } catch (RuntimeException ex) { 169 throw ex; 170 } catch (Exception ex) { 171 throw new RuntimeException(ex); 172 } 173 logger.info("Test completed."); 174 } 175 runTest(Http2TestCases testCase)176 private void runTest(Http2TestCases testCase) throws Exception { 177 switch (testCase) { 178 case RST_AFTER_HEADER: 179 tester.rstAfterHeader(); 180 break; 181 case RST_AFTER_DATA: 182 tester.rstAfterData(); 183 break; 184 case RST_DURING_DATA: 185 tester.rstDuringData(); 186 break; 187 case GOAWAY: 188 tester.goAway(); 189 break; 190 case PING: 191 tester.ping(); 192 break; 193 case MAX_STREAMS: 194 tester.maxStreams(); 195 break; 196 default: 197 throw new IllegalArgumentException("Unknown test case: " + testCase); 198 } 199 } 200 201 private class Tester { 202 private final int timeoutSeconds = 180; 203 204 private final int responseSize = 314159; 205 private final int payloadSize = 271828; 206 private final SimpleRequest simpleRequest = SimpleRequest.newBuilder() 207 .setResponseSize(responseSize) 208 .setPayload(Payload.newBuilder().setBody(ByteString.copyFrom(new byte[payloadSize]))) 209 .build(); 210 final SimpleResponse goldenResponse = SimpleResponse.newBuilder() 211 .setPayload(Payload.newBuilder() 212 .setBody(ByteString.copyFrom(new byte[responseSize]))) 213 .build(); 214 rstAfterHeader()215 private void rstAfterHeader() throws Exception { 216 try { 217 blockingStub.unaryCall(simpleRequest); 218 throw new AssertionError("Expected call to fail"); 219 } catch (StatusRuntimeException ex) { 220 assertRstStreamReceived(ex.getStatus()); 221 } 222 } 223 rstAfterData()224 private void rstAfterData() throws Exception { 225 // Use async stub to verify data is received. 226 RstStreamObserver responseObserver = new RstStreamObserver(); 227 asyncStub.unaryCall(simpleRequest, responseObserver); 228 if (!responseObserver.awaitCompletion(timeoutSeconds, TimeUnit.SECONDS)) { 229 throw new AssertionError("Operation timed out"); 230 } 231 if (responseObserver.getError() == null) { 232 throw new AssertionError("Expected call to fail"); 233 } 234 assertRstStreamReceived(Status.fromThrowable(responseObserver.getError())); 235 if (responseObserver.getResponses().size() != 1) { 236 throw new AssertionError("Expected one response"); 237 } 238 } 239 rstDuringData()240 private void rstDuringData() throws Exception { 241 // Use async stub to verify no data is received. 242 RstStreamObserver responseObserver = new RstStreamObserver(); 243 asyncStub.unaryCall(simpleRequest, responseObserver); 244 if (!responseObserver.awaitCompletion(timeoutSeconds, TimeUnit.SECONDS)) { 245 throw new AssertionError("Operation timed out"); 246 } 247 if (responseObserver.getError() == null) { 248 throw new AssertionError("Expected call to fail"); 249 } 250 assertRstStreamReceived(Status.fromThrowable(responseObserver.getError())); 251 if (responseObserver.getResponses().size() != 0) { 252 throw new AssertionError("Expected zero responses"); 253 } 254 } 255 goAway()256 private void goAway() throws Exception { 257 assertResponseEquals(blockingStub.unaryCall(simpleRequest), goldenResponse); 258 TimeUnit.SECONDS.sleep(1); 259 assertResponseEquals(blockingStub.unaryCall(simpleRequest), goldenResponse); 260 } 261 ping()262 private void ping() throws Exception { 263 assertResponseEquals(blockingStub.unaryCall(simpleRequest), goldenResponse); 264 } 265 maxStreams()266 private void maxStreams() throws Exception { 267 final int numThreads = 10; 268 269 // Preliminary call to ensure MAX_STREAMS setting is received by the client. 270 assertResponseEquals(blockingStub.unaryCall(simpleRequest), goldenResponse); 271 272 threadpool = MoreExecutors.listeningDecorator(newFixedThreadPool(numThreads)); 273 List<ListenableFuture<?>> workerFutures = new ArrayList<>(); 274 for (int i = 0; i < numThreads; i++) { 275 workerFutures.add(threadpool.submit(new MaxStreamsWorker(i))); 276 } 277 ListenableFuture<?> f = Futures.allAsList(workerFutures); 278 f.get(timeoutSeconds, TimeUnit.SECONDS); 279 } 280 281 private class RstStreamObserver implements StreamObserver<SimpleResponse> { 282 private final CountDownLatch latch = new CountDownLatch(1); 283 private final List<SimpleResponse> responses = new ArrayList<>(); 284 private Throwable error; 285 286 @Override onNext(SimpleResponse value)287 public void onNext(SimpleResponse value) { 288 responses.add(value); 289 } 290 291 @Override onError(Throwable t)292 public void onError(Throwable t) { 293 error = t; 294 latch.countDown(); 295 } 296 297 @Override onCompleted()298 public void onCompleted() { 299 latch.countDown(); 300 } 301 getResponses()302 public List<SimpleResponse> getResponses() { 303 return responses; 304 } 305 getError()306 public Throwable getError() { 307 return error; 308 } 309 awaitCompletion(long timeout, TimeUnit unit)310 public boolean awaitCompletion(long timeout, TimeUnit unit) throws Exception { 311 return latch.await(timeout, unit); 312 } 313 } 314 315 private class MaxStreamsWorker implements Runnable { 316 int threadNum; 317 MaxStreamsWorker(int threadNum)318 MaxStreamsWorker(int threadNum) { 319 this.threadNum = threadNum; 320 } 321 322 @Override run()323 public void run() { 324 Thread.currentThread().setName("thread:" + threadNum); 325 try { 326 TestServiceGrpc.TestServiceBlockingStub blockingStub = 327 TestServiceGrpc.newBlockingStub(channel); 328 assertResponseEquals(blockingStub.unaryCall(simpleRequest), goldenResponse); 329 } catch (Exception e) { 330 throw new RuntimeException(e); 331 } 332 } 333 } 334 assertRstStreamReceived(Status status)335 private void assertRstStreamReceived(Status status) { 336 if (!status.getCode().equals(Status.Code.UNAVAILABLE)) { 337 throw new AssertionError("Wrong status code. Expected: " + Status.Code.UNAVAILABLE 338 + " Received: " + status.getCode()); 339 } 340 String http2ErrorPrefix = "HTTP/2 error code: NO_ERROR"; 341 if (status.getDescription() == null 342 || !status.getDescription().startsWith(http2ErrorPrefix)) { 343 throw new AssertionError("Wrong HTTP/2 error code. Expected: " + http2ErrorPrefix 344 + " Received: " + status.getDescription()); 345 } 346 } 347 assertResponseEquals(SimpleResponse response, SimpleResponse goldenResponse)348 private void assertResponseEquals(SimpleResponse response, SimpleResponse goldenResponse) { 349 if (!response.equals(goldenResponse)) { 350 throw new AssertionError("Incorrect response received"); 351 } 352 } 353 } 354 createChannel()355 private ManagedChannel createChannel() { 356 InetAddress address; 357 try { 358 address = InetAddress.getByName(serverHost); 359 } catch (UnknownHostException ex) { 360 throw new RuntimeException(ex); 361 } 362 return NettyChannelBuilder.forAddress(new InetSocketAddress(address, serverPort)) 363 .negotiationType(NegotiationType.PLAINTEXT) 364 .build(); 365 } 366 validTestCasesHelpText()367 private static String validTestCasesHelpText() { 368 StringBuilder builder = new StringBuilder(); 369 for (Http2TestCases testCase : Http2TestCases.values()) { 370 String strTestcase = testCase.name().toLowerCase(Locale.ROOT); 371 builder.append("\n ") 372 .append(strTestcase) 373 .append(": ") 374 .append(testCase.description()); 375 } 376 return builder.toString(); 377 } 378 } 379 380