/**
* Copyright 2016 LinkedIn Corp. All rights reserved.
*
* 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
*
* 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.
*/
package com.github.ambry.rest;
import com.codahale.metrics.MetricRegistry;
import com.github.ambry.commons.ByteBufferReadableStreamChannel;
import com.github.ambry.config.VerifiableProperties;
import com.github.ambry.router.AsyncWritableChannel;
import com.github.ambry.router.ByteBufferRSC;
import com.github.ambry.router.Callback;
import com.github.ambry.router.FutureResult;
import com.github.ambry.router.InMemoryRouter;
import com.github.ambry.router.ReadableStreamChannel;
import com.github.ambry.router.Router;
import com.github.ambry.utils.TestUtils;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.net.ssl.SSLSession;
import org.json.JSONException;
import org.json.JSONObject;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Tests functionality of {@link AsyncRequestResponseHandler}.
*/
public class AsyncRequestResponseHandlerTest {
private static VerifiableProperties verifiableProperties;
private static Router router;
private static MockBlobStorageService blobStorageService;
private static AsyncRequestResponseHandler asyncRequestResponseHandler;
/**
* Sets up all the tests by providing a started {@link AsyncRequestResponseHandler} for their use.
* @throws InstantiationException
* @throws IOException
*/
@BeforeClass
public static void startRequestResponseHandler() throws InstantiationException, IOException {
verifiableProperties = new VerifiableProperties(new Properties());
router = new InMemoryRouter(verifiableProperties);
asyncRequestResponseHandler = getAsyncRequestResponseHandler(5);
blobStorageService.start();
asyncRequestResponseHandler.start();
}
/**
* Shuts down the created {@link AsyncRequestResponseHandler}.
* @throws IOException
*/
@AfterClass
public static void shutdownRequestResponseHandler() throws IOException {
asyncRequestResponseHandler.shutdown();
blobStorageService.shutdown();
router.close();
}
/**
* Tests {@link AsyncRequestResponseHandler#start()} and {@link AsyncRequestResponseHandler#shutdown()}.
* @throws IOException
*/
@Test
public void startShutdownTest() throws IOException {
final int EXPECTED_WORKER_COUNT = new Random().nextInt(10);
AsyncRequestResponseHandler handler = getAsyncRequestResponseHandler(EXPECTED_WORKER_COUNT);
assertEquals("Number of workers alive is incorrect", 0, handler.getWorkersAlive());
assertFalse("IsRunning should be false", handler.isRunning());
handler.start();
try {
assertTrue("IsRunning should be true", handler.isRunning());
assertEquals("Number of workers alive is incorrect", EXPECTED_WORKER_COUNT, handler.getWorkersAlive());
} finally {
handler.shutdown();
}
assertFalse("IsRunning should be false", handler.isRunning());
}
/**
* Tests for {@link AsyncRequestResponseHandler#shutdown()} when {@link AsyncRequestResponseHandler#start()} has not
* been called previously. This test is for cases where {@link AsyncRequestResponseHandler#start()} has failed and
* {@link AsyncRequestResponseHandler#shutdown()} needs to be run.
* @throws IOException
*/
@Test
public void shutdownWithoutStart() throws IOException {
AsyncRequestResponseHandler handler = getAsyncRequestResponseHandler(1);
handler.shutdown();
}
/**
* This tests for exceptions thrown when a {@link AsyncRequestResponseHandler} is used without calling
* {@link AsyncRequestResponseHandler#start()}first.
* @throws IOException
* @throws JSONException
* @throws URISyntaxException
*/
@Test
public void useServiceWithoutStartTest() throws IOException, JSONException, URISyntaxException {
AsyncRequestResponseHandler handler = getAsyncRequestResponseHandler(1);
RestRequest restRequest = createRestRequest(RestMethod.GET, "/", null, null);
try {
handler.handleRequest(restRequest, new MockRestResponseChannel());
fail("Should have thrown RestServiceException because the AsyncRequestResponseHandler has not been started");
} catch (RestServiceException e) {
assertEquals("The RestServiceErrorCode does not match", RestServiceErrorCode.ServiceUnavailable,
e.getErrorCode());
}
restRequest = createRestRequest(RestMethod.GET, "/", null, null);
restRequest.getMetricsTracker().scalingMetricsTracker.markRequestReceived();
try {
handler.handleResponse(restRequest, new MockRestResponseChannel(),
new ByteBufferReadableStreamChannel(ByteBuffer.allocate(0)), null);
fail("Should have thrown RestServiceException because the AsyncRequestResponseHandler has not been started");
} catch (RestServiceException e) {
assertEquals("The RestServiceErrorCode does not match", RestServiceErrorCode.ServiceUnavailable,
e.getErrorCode());
}
}
/**
* Tests the behavior of {@link AsyncRequestResponseHandler} when request worker count is not set or is zero.
* @throws Exception
*/
@Test
public void edgeCaseWorkerCountsTest() throws Exception {
RequestResponseHandlerMetrics metrics = new RequestResponseHandlerMetrics(new MetricRegistry());
AsyncRequestResponseHandler requestResponseHandler = new AsyncRequestResponseHandler(metrics);
noRequestHandlersTest(requestResponseHandler);
requestResponseHandler = getAsyncRequestResponseHandler(0);
noRequestHandlersTest(requestResponseHandler);
}
@Test
public void setFunctionsBadArgumentsTest() {
RequestResponseHandlerMetrics metrics = new RequestResponseHandlerMetrics(new MetricRegistry());
AsyncRequestResponseHandler requestResponseHandler = new AsyncRequestResponseHandler(metrics);
// set request workers < 0
try {
requestResponseHandler.setupRequestHandling(-1, blobStorageService);
fail("Setting request workers < 0 should have thrown exception");
} catch (IllegalArgumentException e) {
// expected. nothing to do.
}
// set null BlobStorageService
try {
requestResponseHandler.setupRequestHandling(1, null);
fail("Setting BlobStorageService to null should have thrown exception");
} catch (IllegalArgumentException e) {
// expected. nothing to do.
}
}
/**
* Tests behavior of {@link AsyncRequestResponseHandler#setupRequestHandling(int, BlobStorageService)} after the
* {@link AsyncRequestResponseHandler} has been started.
*/
@Test
public void setupRequestHandlingStartTest() {
// set request workers.
try {
asyncRequestResponseHandler.setupRequestHandling(5, blobStorageService);
fail("Setting request workers after start should have thrown exception");
} catch (IllegalStateException e) {
// expected. nothing to do.
}
}
/**
* Tests handling of all {@link RestMethod}s. The {@link MockBlobStorageService} instance being used is
* asked to only echo the method.
* @throws Exception
*/
@Test
public void allRestMethodsSuccessTest() throws Exception {
for (int i = 0; i < 25; i++) {
for (RestMethod restMethod : RestMethod.values()) {
// PUT is not supported, so it always fails.
if (restMethod != RestMethod.UNKNOWN && restMethod != RestMethod.PUT) {
doHandleRequestSuccessTest(restMethod, asyncRequestResponseHandler);
}
}
}
}
/**
* Tests that right exceptions are thrown on bad input to
* {@link AsyncRequestResponseHandler#handleRequest(RestRequest, RestResponseChannel)}. These are exceptions that get
* thrown before queuing.
* @throws Exception
*/
@Test
public void handleRequestFailureBeforeQueueTest() throws Exception {
// RestRequest null.
try {
asyncRequestResponseHandler.handleRequest(null, new MockRestResponseChannel());
fail("Test should have thrown exception, but did not");
} catch (IllegalArgumentException e) {
// expected. Nothing to do.
}
// RestResponseChannel null.
RestRequest restRequest = createRestRequest(RestMethod.GET, "/", new JSONObject(), null);
try {
asyncRequestResponseHandler.handleRequest(restRequest, null);
fail("Test should have thrown exception, but did not");
} catch (IllegalArgumentException e) {
// expected. Nothing to do.
}
// AsyncRequestResponseHandler should still be alive and serving requests
assertTrue("AsyncRequestResponseHandler is dead", asyncRequestResponseHandler.isRunning());
}
/**
* Tests that right exceptions are thrown on bad input while a de-queued {@link RestRequest} is being handled.
* @throws Exception
*/
@Test
public void handleRequestFailureOnDequeueTest() throws Exception {
unknownRestMethodTest(asyncRequestResponseHandler);
putRestMethodTest(asyncRequestResponseHandler);
delayedHandleRequestThatThrowsRestException(asyncRequestResponseHandler);
delayedHandleRequestThatThrowsRuntimeException(asyncRequestResponseHandler);
}
/**
* Tests {@link AsyncRequestResponseHandler#handleResponse(RestRequest, RestResponseChannel, ReadableStreamChannel, * Exception)} with good input.
* @throws Exception
*/
@Test
public void handleResponseSuccessTest() throws Exception {
for (int i = 0; i < 100; i++) {
doHandleResponseSuccessTest(asyncRequestResponseHandler);
}
}
/**
* Tests the reaction of {@link AsyncRequestResponseHandler#handleResponse(RestRequest, RestResponseChannel, * ReadableStreamChannel, Exception)} to some misbehaving components.
* @throws Exception
*/
@Test
public void handleResponseExceptionTest() throws Exception {
ByteBufferRSC response = new ByteBufferRSC(ByteBuffer.allocate(0));
// RestRequest null.
try {
asyncRequestResponseHandler.handleResponse(null, new MockRestResponseChannel(), response, null);
fail("Test should have thrown exception, but did not");
} catch (IllegalArgumentException e) {
// expected. Nothing to do.
}
// RestResponseChannel null.
MockRestRequest restRequest = createRestRequest(RestMethod.GET, "/", new JSONObject(), null);
restRequest.getMetricsTracker().scalingMetricsTracker.markRequestReceived();
try {
asyncRequestResponseHandler.handleResponse(restRequest, null, response, null);
fail("Test should have thrown exception, but did not");
} catch (IllegalArgumentException e) {
// expected. Nothing to do.
}
// Writing response throws exception.
MockRestRequest goodRestRequest = createRestRequest(RestMethod.GET, "/", null, null);
goodRestRequest.getMetricsTracker().scalingMetricsTracker.markRequestReceived();
MockRestResponseChannel restResponseChannel = new MockRestResponseChannel();
String exceptionMsg = "@@randomMsg@@";
ReadableStreamChannel badResponse = new IncompleteReadReadableStreamChannel(null, new Exception(exceptionMsg));
awaitResponse(asyncRequestResponseHandler, goodRestRequest, restResponseChannel, badResponse, null);
assertNotNull("MockRestResponseChannel would have been passed an exception", restResponseChannel.getException());
assertEquals("Exception message does not match", exceptionMsg, restResponseChannel.getException().getMessage());
// Writing response is incomplete
goodRestRequest = createRestRequest(RestMethod.GET, "/", null, null);
goodRestRequest.getMetricsTracker().scalingMetricsTracker.markRequestReceived();
restResponseChannel = new MockRestResponseChannel();
badResponse = new IncompleteReadReadableStreamChannel(0L, null);
awaitResponse(asyncRequestResponseHandler, goodRestRequest, restResponseChannel, badResponse, null);
assertNotNull("MockRestResponseChannel would have been passed an exception", restResponseChannel.getException());
assertEquals("Unexpected exception", IllegalStateException.class, restResponseChannel.getException().getClass());
// No bytes read and no exception
goodRestRequest = createRestRequest(RestMethod.GET, "/", null, null);
goodRestRequest.getMetricsTracker().scalingMetricsTracker.markRequestReceived();
restResponseChannel = new MockRestResponseChannel();
badResponse = new IncompleteReadReadableStreamChannel(null, null);
awaitResponse(asyncRequestResponseHandler, goodRestRequest, restResponseChannel, badResponse, null);
assertNotNull("MockRestResponseChannel would have been passed an exception", restResponseChannel.getException());
assertEquals("Unexpected exception", IllegalStateException.class, restResponseChannel.getException().getClass());
// RestRequest is bad.
BadRestRequest badRestRequest = new BadRestRequest();
badRestRequest.getMetricsTracker().scalingMetricsTracker.markRequestReceived();
restResponseChannel = new MockRestResponseChannel();
ByteBufferRSC goodResponse = new ByteBufferRSC(ByteBuffer.allocate(0));
EventMonitor<ByteBufferRSC.Event> responseCloseMonitor =
new EventMonitor<ByteBufferRSC.Event>(ByteBufferRSC.Event.Close);
goodResponse.addListener(responseCloseMonitor);
awaitResponse(asyncRequestResponseHandler, badRestRequest, restResponseChannel, goodResponse, null);
assertTrue("Response is not closed", responseCloseMonitor.awaitEvent(1, TimeUnit.SECONDS));
// AsyncRequestResponseHandler should still be alive and serving requests
assertTrue("AsyncRequestResponseHandler is dead", asyncRequestResponseHandler.isRunning());
}
/**
* Tests various functions when multiple responses are in flight.
* @throws Exception
*/
@Test
public void multipleResponsesInFlightTest() throws Exception {
ExecutorService executorService = Executors.newSingleThreadExecutor();
AsyncRequestResponseHandler responseHandler = getAsyncRequestResponseHandler(0);
responseHandler.start();
try {
final int EXPECTED_QUEUE_SIZE = 5;
// test for duplicate request submission
// submit a few responses that halt on read
CountDownLatch releaseRead = new CountDownLatch(1);
byte[] data = null;
RestRequest restRequest = null;
MockRestResponseChannel restResponseChannel = null;
ReadableStreamChannel response = null;
for (int i = 0; i < EXPECTED_QUEUE_SIZE; i++) {
data = TestUtils.getRandomBytes(32);
response = new HaltingRSC(ByteBuffer.wrap(data), releaseRead, executorService);
restRequest = createRestRequest(RestMethod.GET, "/", null, null);
restRequest.getMetricsTracker().scalingMetricsTracker.markRequestReceived();
restResponseChannel = new MockRestResponseChannel(restRequest);
responseHandler.handleResponse(restRequest, restResponseChannel, response, null);
}
assertEquals("Response set size unexpected", EXPECTED_QUEUE_SIZE, responseHandler.getResponseSetSize());
// attach a listener to the last MockRestResponseChannel
EventMonitor<MockRestResponseChannel.Event> eventMonitor =
new EventMonitor<MockRestResponseChannel.Event>(MockRestResponseChannel.Event.OnRequestComplete);
restResponseChannel.addListener(eventMonitor);
// we try to re submit the last response. This should fail.
try {
responseHandler.handleResponse(restRequest, restResponseChannel, response, null);
fail("Trying to resubmit a response that is already in flight should have failed");
} catch (RestServiceException e) {
assertEquals("Unexpected error code", RestServiceErrorCode.RequestResponseQueuingFailure, e.getErrorCode());
}
assertEquals("Response set size unexpected", EXPECTED_QUEUE_SIZE, responseHandler.getResponseSetSize());
releaseRead.countDown();
if (!eventMonitor.awaitEvent(1, TimeUnit.SECONDS)) {
fail("awaitResponse took too long. There might be a problem or the timeout may need to be increased");
}
// compare the output. We care only about the last one because we want to make sure the duplicate submission
// did not mess with the original
assertArrayEquals("Unexpected data in the response", data, restResponseChannel.getResponseBody());
// test for shutdown when responses are still in progress
releaseRead = new CountDownLatch(1);
List<MockRestResponseChannel> responseChannels = new ArrayList<MockRestResponseChannel>(EXPECTED_QUEUE_SIZE);
for (int i = 0; i < EXPECTED_QUEUE_SIZE; i++) {
response = new HaltingRSC(ByteBuffer.allocate(0), releaseRead, executorService);
restRequest = createRestRequest(RestMethod.GET, "/", null, null);
restRequest.getMetricsTracker().scalingMetricsTracker.markRequestReceived();
restResponseChannel = new MockRestResponseChannel(restRequest);
responseHandler.handleResponse(restRequest, restResponseChannel, response, null);
responseChannels.add(restResponseChannel);
}
assertEquals("Response set size unexpected", EXPECTED_QUEUE_SIZE, responseHandler.getResponseSetSize());
responseHandler.shutdown();
assertEquals("Response set size unexpected", 0, responseHandler.getResponseSetSize());
releaseRead.countDown();
// all of the responses should have exceptions
for (MockRestResponseChannel channel : responseChannels) {
assertNotNull("There should be an exception", channel.getException());
if (channel.getException() instanceof RestServiceException) {
RestServiceException rse = (RestServiceException) channel.getException();
assertEquals("Unexpected RestServiceErrorCode", RestServiceErrorCode.ServiceUnavailable, rse.getErrorCode());
} else {
throw channel.getException();
}
}
} finally {
responseHandler.shutdown();
executorService.shutdown();
}
}
/**
* Tests various functions when multiple requests are in queue.
* @throws Exception
*/
@Test
public void multipleRequestsInQueueTest() throws Exception {
final int EXPECTED_MIN_QUEUE_SIZE = 5;
blobStorageService.blockAllOperations();
try {
// the first request that each worker processes will block.
for (int i = 0; i < EXPECTED_MIN_QUEUE_SIZE + asyncRequestResponseHandler.getWorkersAlive(); i++) {
MockRestRequest restRequest = createRestRequest(RestMethod.GET, "/", null, null);
restRequest.getMetricsTracker().scalingMetricsTracker.markRequestReceived();
MockRestResponseChannel restResponseChannel = new MockRestResponseChannel(restRequest);
asyncRequestResponseHandler.handleRequest(restRequest, restResponseChannel);
}
assertTrue("Request queue size should be at least " + EXPECTED_MIN_QUEUE_SIZE + ". Is "
+ asyncRequestResponseHandler.getRequestQueueSize(),
asyncRequestResponseHandler.getRequestQueueSize() >= EXPECTED_MIN_QUEUE_SIZE);
} finally {
blobStorageService.releaseAllOperations();
}
}
// helpers
// general
/**
* Creates a {@link MockRestRequest} with the given parameters.
* @param method the {@link RestMethod} desired.
* @param uri the URI to hit.
* @param headers the headers that will accompany the request.
* @param contents the content associated with the request.
* @return a {@link MockRestRequest} based on the given parameters.
* @throws JSONException
* @throws UnsupportedEncodingException
* @throws URISyntaxException
*/
private MockRestRequest createRestRequest(RestMethod method, String uri, JSONObject headers,
List<ByteBuffer> contents) throws JSONException, UnsupportedEncodingException, URISyntaxException {
JSONObject data = new JSONObject();
data.put(MockRestRequest.REST_METHOD_KEY, method);
data.put(MockRestRequest.URI_KEY, uri);
if (headers != null) {
data.put(MockRestRequest.HEADERS_KEY, headers);
}
return new MockRestRequest(data, contents);
}
/**
* Sends a the {@code restRequest} to the {@code requestHandler} for handling and waits for
* {@link RestResponseChannel#onResponseComplete(Exception)} to be called in {@code restResponseChannel}.
* If there was an exception input to the function, throws that exception.
* @param requestHandler the {@link AsyncRequestResponseHandler} instance to use.
* @param restRequest the {@link RestRequest} to send to the {@code requestHandler}.
* @param restResponseChannel the {@link RestResponseChannel} for responses.
* @throws InterruptedException
* @throws RestServiceException
*/
private void sendRequestAwaitResponse(AsyncRequestResponseHandler requestHandler, RestRequest restRequest,
MockRestResponseChannel restResponseChannel) throws InterruptedException, RestServiceException {
EventMonitor<MockRestResponseChannel.Event> eventMonitor =
new EventMonitor<MockRestResponseChannel.Event>(MockRestResponseChannel.Event.OnRequestComplete);
restResponseChannel.addListener(eventMonitor);
requestHandler.handleRequest(restRequest, restResponseChannel);
if (!eventMonitor.awaitEvent(1, TimeUnit.SECONDS)) {
fail("sendRequestAwaitResponse took too long. There might be a problem or the timeout may need to be increased");
}
}
/**
* Queues a response and waits while the response is completely sent out.
* @param responseHandler the {@link AsyncRequestResponseHandler} instance to use.
* @param restRequest the {@link RestRequest} to send to the {@code requestHandler}.
* @param restResponseChannel the {@link RestResponseChannel} for responses.
* @param response the response to send as a {@link ReadableStreamChannel}.
* @param exception the exception that occurred while building the response, if any.
* @throws InterruptedException
* @throws RestServiceException
*/
private void awaitResponse(AsyncRequestResponseHandler responseHandler, RestRequest restRequest,
MockRestResponseChannel restResponseChannel, ReadableStreamChannel response, Exception exception)
throws InterruptedException, RestServiceException {
EventMonitor<MockRestResponseChannel.Event> eventMonitor =
new EventMonitor<MockRestResponseChannel.Event>(MockRestResponseChannel.Event.OnRequestComplete);
restResponseChannel.addListener(eventMonitor);
responseHandler.handleResponse(restRequest, restResponseChannel, response, exception);
if (!eventMonitor.awaitEvent(1, TimeUnit.SECONDS)) {
fail("awaitResponse took too long. There might be a problem or the timeout may need to be increased");
}
}
/**
* Sends a {@link RestRequest} to the {@code requestHandler} with the specified {@code restMethod} and checks the
* response to see that the {@code restMethod} has been echoed.
* @param restMethod the {@link RestMethod} required.
* @param requestHandler the {@link AsyncRequestResponseHandler} instance to use.
* @throws Exception
*/
private void doHandleRequestSuccessTest(RestMethod restMethod, AsyncRequestResponseHandler requestHandler)
throws Exception {
RestRequest restRequest = createRestRequest(restMethod, MockBlobStorageService.ECHO_REST_METHOD, null, null);
MockRestResponseChannel restResponseChannel = new MockRestResponseChannel();
sendRequestAwaitResponse(requestHandler, restRequest, restResponseChannel);
if (restResponseChannel.getException() == null) {
byte[] response = restResponseChannel.getResponseBody();
assertArrayEquals("Unexpected response", restMethod.toString().getBytes(), response);
} else {
throw restResponseChannel.getException();
}
}
/**
* Tests {@link AsyncRequestResponseHandler#handleResponse(RestRequest, RestResponseChannel, ReadableStreamChannel, * Exception)} with good input.
* @throws Exception
*/
private void doHandleResponseSuccessTest(AsyncRequestResponseHandler asyncRequestResponseHandler) throws Exception {
// both response and exception null
MockRestRequest restRequest = createRestRequest(RestMethod.GET, "/", null, null);
restRequest.getMetricsTracker().scalingMetricsTracker.markRequestReceived();
MockRestResponseChannel restResponseChannel = new MockRestResponseChannel();
awaitResponse(asyncRequestResponseHandler, restRequest, restResponseChannel, null, null);
if (restResponseChannel.getException() != null) {
throw restResponseChannel.getException();
}
// both response and exception not null
restRequest = createRestRequest(RestMethod.GET, "/", null, null);
restRequest.getMetricsTracker().scalingMetricsTracker.markRequestReceived();
restResponseChannel = new MockRestResponseChannel();
ByteBuffer responseBuffer = ByteBuffer.wrap(TestUtils.getRandomBytes(1024));
ByteBufferRSC response = new ByteBufferRSC(responseBuffer);
EventMonitor<ByteBufferRSC.Event> responseCloseMonitor =
new EventMonitor<ByteBufferRSC.Event>(ByteBufferRSC.Event.Close);
response.addListener(responseCloseMonitor);
Exception e = new Exception();
awaitResponse(asyncRequestResponseHandler, restRequest, restResponseChannel, response, e);
// make sure exception was correctly sent to the RestResponseChannel.
assertEquals("Exception was not piped correctly", e, restResponseChannel.getException());
assertTrue("Response is not closed", responseCloseMonitor.awaitEvent(1, TimeUnit.SECONDS));
// response null but exception not null.
restRequest = createRestRequest(RestMethod.GET, "/", null, null);
restRequest.getMetricsTracker().scalingMetricsTracker.markRequestReceived();
restResponseChannel = new MockRestResponseChannel();
e = new Exception();
awaitResponse(asyncRequestResponseHandler, restRequest, restResponseChannel, null, e);
// make sure exception was correctly sent to the RestResponseChannel.
assertEquals("Exception was not piped correctly", e, restResponseChannel.getException());
// response not null.
// steady response - full response available.
restRequest = createRestRequest(RestMethod.GET, "/", null, null);
restRequest.getMetricsTracker().scalingMetricsTracker.markRequestReceived();
restResponseChannel = new MockRestResponseChannel();
responseBuffer = ByteBuffer.wrap(TestUtils.getRandomBytes(1024));
response = new ByteBufferRSC(responseBuffer);
responseCloseMonitor = new EventMonitor<ByteBufferRSC.Event>(ByteBufferRSC.Event.Close);
response.addListener(responseCloseMonitor);
awaitResponse(asyncRequestResponseHandler, restRequest, restResponseChannel, response, null);
if (restResponseChannel.getException() == null) {
assertArrayEquals("Response does not match", responseBuffer.array(), restResponseChannel.getResponseBody());
assertTrue("Response is not closed", responseCloseMonitor.awaitEvent(1, TimeUnit.SECONDS));
} else {
throw restResponseChannel.getException();
}
}
// BeforeClass helpers
/**
* Gets a new instance of {@link AsyncRequestResponseHandler}.
* @param requestWorkers the number of request workers.
* @return a new instance of {@link AsyncRequestResponseHandler}.
* @throws IOException
*/
private static AsyncRequestResponseHandler getAsyncRequestResponseHandler(int requestWorkers) throws IOException {
RequestResponseHandlerMetrics metrics = new RequestResponseHandlerMetrics(new MetricRegistry());
AsyncRequestResponseHandler handler = new AsyncRequestResponseHandler(metrics);
if (requestWorkers > 0) {
if (blobStorageService == null) {
blobStorageService = new MockBlobStorageService(verifiableProperties, handler, router);
}
handler.setupRequestHandling(requestWorkers, blobStorageService);
}
return handler;
}
// useWithoutSettingWorkerCountTest() and zeroScalingUnitsTest() helpers
/**
* Uses the {@code requestResponseHandler} with zero request workers and one response worker and verifies that
* responses are sent, but requests are not served.
* @param requestResponseHandler the {@link AsyncRequestResponseHandler} instance to use. Must have zero request
* workers and more then zero response workers.
* @throws Exception
*/
private void noRequestHandlersTest(AsyncRequestResponseHandler requestResponseHandler) throws Exception {
// ok for start
requestResponseHandler.start();
try {
// using for responses OK.
MockRestRequest restRequest = createRestRequest(RestMethod.GET, "/", null, null);
restRequest.getMetricsTracker().scalingMetricsTracker.markRequestReceived();
MockRestResponseChannel restResponseChannel = new MockRestResponseChannel();
awaitResponse(requestResponseHandler, restRequest, restResponseChannel, null, null);
if (restResponseChannel.getException() != null) {
throw restResponseChannel.getException();
}
// using for request is not OK.
try {
doHandleRequestSuccessTest(RestMethod.GET, requestResponseHandler);
fail("Handling request should have failed because no BlobStorageService was set.");
} catch (RestServiceException e) {
assertEquals("Unexpected RestServiceErrorCode", RestServiceErrorCode.ServiceUnavailable, e.getErrorCode());
}
} finally {
requestResponseHandler.shutdown();
}
}
// handleRequestFailureOnDequeueTest() helpers
/**
* Sends a {@link RestRequest} with an unknown {@link RestMethod}. The failure will happen once the request is
* dequeued.
* @param requestResponseHandler The {@link AsyncRequestResponseHandler} instance to use.
* @throws Exception
*/
private void unknownRestMethodTest(AsyncRequestResponseHandler requestResponseHandler) throws Exception {
RestRequest restRequest = createRestRequest(RestMethod.UNKNOWN, "/", null, null);
MockRestResponseChannel restResponseChannel = new MockRestResponseChannel();
sendRequestAwaitResponse(requestResponseHandler, restRequest, restResponseChannel);
if (restResponseChannel.getException() == null) {
fail("Request handling would have failed and an exception should have been generated");
} else {
// it's ok if this conversion fails - the test should fail anyway.
RestServiceException e = (RestServiceException) restResponseChannel.getException();
assertEquals("Did not get expected RestServiceErrorCode", RestServiceErrorCode.UnsupportedRestMethod,
e.getErrorCode());
assertTrue("AsyncRequestResponseHandler is dead", requestResponseHandler.isRunning());
}
}
/**
* Sends a {@link RestRequest} with an PUT {@link RestMethod}. The failure will happen once the request is
* dequeued.
* @param requestResponseHandler The {@link AsyncRequestResponseHandler} instance to use.
* @throws Exception
*/
private void putRestMethodTest(AsyncRequestResponseHandler requestResponseHandler) throws Exception {
RestRequest restRequest = createRestRequest(RestMethod.PUT, "/", null, null);
MockRestResponseChannel restResponseChannel = new MockRestResponseChannel();
sendRequestAwaitResponse(requestResponseHandler, restRequest, restResponseChannel);
if (restResponseChannel.getException() == null) {
fail("Request handling would have failed and an exception should have been generated");
} else {
// it's ok if this conversion fails - the test should fail anyway.
RestServiceException e = (RestServiceException) restResponseChannel.getException();
assertEquals("Did not get expected RestServiceErrorCode", RestServiceErrorCode.UnsupportedHttpMethod,
e.getErrorCode());
assertTrue("AsyncRequestResponseHandler is dead", requestResponseHandler.isRunning());
}
}
/**
* Sends a {@link RestRequest} that requests a {@link RestServiceException}. The failure will happen once the request
* is dequeued.
* @param requestResponseHandler The {@link AsyncRequestResponseHandler} instance to use.
* @throws Exception
*/
private void delayedHandleRequestThatThrowsRestException(AsyncRequestResponseHandler requestResponseHandler)
throws Exception {
RestRequest restRequest =
createRestRequest(RestMethod.GET, MockBlobStorageService.SEND_RESPONSE_REST_SERVICE_EXCEPTION, null, null);
MockRestResponseChannel restResponseChannel = new MockRestResponseChannel();
sendRequestAwaitResponse(requestResponseHandler, restRequest, restResponseChannel);
if (restResponseChannel.getException() == null) {
fail("Request handling would have failed and an exception should have been generated");
} else {
// it's ok if this conversion fails - the test should fail anyway.
RestServiceException e = (RestServiceException) restResponseChannel.getException();
assertEquals("Did not get expected RestServiceErrorCode", RestServiceErrorCode.InternalServerError,
e.getErrorCode());
assertTrue("AsyncRequestResponseHandler is dead", requestResponseHandler.isRunning());
}
}
/**
* Sends a {@link RestRequest} that requests a {@link RuntimeException}. The failure will happen once the request is
* dequeued.
* @param requestResponseHandler The {@link AsyncRequestResponseHandler} instance to use.
* @throws Exception
*/
private void delayedHandleRequestThatThrowsRuntimeException(AsyncRequestResponseHandler requestResponseHandler)
throws Exception {
RestRequest restRequest =
createRestRequest(RestMethod.GET, MockBlobStorageService.THROW_RUNTIME_EXCEPTION, null, null);
MockRestResponseChannel restResponseChannel = new MockRestResponseChannel();
sendRequestAwaitResponse(requestResponseHandler, restRequest, restResponseChannel);
if (restResponseChannel.getException() == null) {
fail("Request handling would have failed and an exception should have been generated");
} else {
// it's ok if this conversion fails - the test should fail anyway.
RuntimeException e = (RuntimeException) restResponseChannel.getException();
assertEquals("Failure message does not match expectation", MockBlobStorageService.THROW_RUNTIME_EXCEPTION,
e.getMessage());
assertTrue("AsyncRequestResponseHandler is dead", requestResponseHandler.isRunning());
}
}
}
/**
* Implementation of many event listeners that checks for the expected event and records the parameters received. Can be
* used with one event only.
* @param <T> the type of event that this is a monitor for.
*/
class EventMonitor<T> implements MockRestResponseChannel.EventListener, MockRestRequest.EventListener, ByteBufferRSC.EventListener {
private final T eventOfInterest;
private final CountDownLatch eventFired = new CountDownLatch(1);
private final AtomicBoolean eventOccurred = new AtomicBoolean(false);
/**
* Creates a listener that listens to the {@code eventOfInterest}.
* @param eventOfInterest the event that this EventMonitor listens to.
*/
public EventMonitor(T eventOfInterest) {
this.eventOfInterest = eventOfInterest;
}
@Override
public void onEventComplete(MockRestResponseChannel mockRestResponseChannel, MockRestResponseChannel.Event event) {
if (eventOfInterest.equals(event) && eventOccurred.compareAndSet(false, true)) {
eventFired.countDown();
}
}
@Override
public void onEventComplete(MockRestRequest mockRestRequest, MockRestRequest.Event event) {
if (eventOfInterest.equals(event) && eventOccurred.compareAndSet(false, true)) {
eventFired.countDown();
}
}
@Override
public void onEventComplete(ByteBufferRSC byteBufferRSC, ByteBufferRSC.Event event) {
if (eventOfInterest.equals(event) && eventOccurred.compareAndSet(false, true)) {
eventFired.countDown();
}
}
/**
* Wait for a finite amount of time for event to occur.
* @param timeout the length of time to wait for.
* @param timeUnit the time unit of timeout.
* @return {@code true} if the the event occurred within the {@code timeout}. Otherwise {@code false}.
* @throws InterruptedException
*/
public boolean awaitEvent(long timeout, TimeUnit timeUnit) throws InterruptedException {
return eventFired.await(timeout, timeUnit);
}
}
/**
* Object that wraps another {@link ReadableStreamChannel} and simply blocks on read until released.
*/
class HaltingRSC extends ByteBufferRSC implements ReadableStreamChannel {
private final CountDownLatch release;
private final ExecutorService executorService;
public HaltingRSC(ByteBuffer buffer, CountDownLatch releaseRead, ExecutorService executorService) {
super(buffer);
this.release = releaseRead;
this.executorService = executorService;
}
@Override
public Future<Long> readInto(final AsyncWritableChannel asyncWritableChannel, final Callback<Long> callback) {
if (!isOpen()) {
throw new IllegalStateException("Channel is not open");
}
final FutureResult<Long> future = new FutureResult<Long>();
executorService.submit(new Runnable() {
@Override
public void run() {
try {
release.await();
} catch (InterruptedException e) {
// move on.
}
long result = 0;
Exception exception = null;
try {
result = asyncWritableChannel.write(buffer, callback).get();
} catch (Exception e) {
exception = e;
}
future.done(result, exception);
}
});
return future;
}
}
/**
* {@link ReadableStreamChannel} implementation that either has an {@link Exception} on
* {@link #readInto(AsyncWritableChannel, Callback)} or executes an incomplete read.
*/
class IncompleteReadReadableStreamChannel implements ReadableStreamChannel {
private final AtomicBoolean channelOpen = new AtomicBoolean(true);
private final Long bytesRead;
private final Exception exceptionToThrow;
/**
* Create an instance of {@link IncompleteReadReadableStreamChannel} with {@code bytesRead} and
* {@code exceptionToThrow} that will be returned via the future and callback.
* @param bytesRead The number of bytes read that needs to be returned. Can be null.
* @param exceptionToThrow if desired, provide an exception that will thrown on read. Can be null.
*/
public IncompleteReadReadableStreamChannel(Long bytesRead, Exception exceptionToThrow) {
this.bytesRead = bytesRead;
this.exceptionToThrow = exceptionToThrow;
}
@Override
public long getSize() {
return 1;
}
/**
* Either throws the exception provided or returns immediately saying no bytes were read.
* @param asyncWritableChannel the {@link AsyncWritableChannel} to read the data into.
* @param callback the {@link Callback} that will be invoked either when all the data in the channel has been emptied
* into the {@code asyncWritableChannel} or if there is an exception in doing so. This can be null.
* @return a {@link Future} that will eventually contain the result of the operation.
*/
@Override
public Future<Long> readInto(AsyncWritableChannel asyncWritableChannel, Callback<Long> callback) {
Exception exception;
if (!channelOpen.get()) {
exception = new ClosedChannelException();
} else {
exception = exceptionToThrow;
}
FutureResult<Long> futureResult = new FutureResult<Long>();
futureResult.done(bytesRead, exception);
if (callback != null) {
callback.onCompletion(bytesRead, exception);
}
return futureResult;
}
@Override
public boolean isOpen() {
return channelOpen.get();
}
@Override
public void close() throws IOException {
channelOpen.set(false);
}
}
/**
* A bad implementation of {@link RestRequest}. Just throws exceptions.
*/
class BadRestRequest implements RestRequest {
private final RestRequestMetricsTracker restRequestMetricsTracker = new RestRequestMetricsTracker();
@Override
public RestMethod getRestMethod() {
return null;
}
@Override
public String getPath() {
return null;
}
@Override
public String getUri() {
return null;
}
@Override
public Map<String, Object> getArgs() {
return null;
}
@Override
public SSLSession getSSLSession() {
return null;
}
@Override
public void prepare() {
throw new IllegalStateException("Not implemented");
}
@Override
public boolean isOpen() {
throw new IllegalStateException("Not implemented");
}
@Override
public void close() throws IOException {
throw new IOException("Not implemented");
}
@Override
public RestRequestMetricsTracker getMetricsTracker() {
return restRequestMetricsTracker;
}
@Override
public long getSize() {
throw new IllegalStateException("Not implemented");
}
@Override
public Future<Long> readInto(AsyncWritableChannel asyncWritableChannel, Callback<Long> callback) {
throw new IllegalStateException("Not implemented");
}
@Override
public void setDigestAlgorithm(String digestAlgorithm) {
throw new IllegalStateException("Not implemented");
}
@Override
public byte[] getDigest() {
throw new IllegalStateException("Not implemented");
}
}