package test.r2.integ; import com.linkedin.common.callback.Callback; import com.linkedin.common.callback.FutureCallback; import com.linkedin.common.util.None; import com.linkedin.data.ByteString; import com.linkedin.r2.filter.FilterChain; import com.linkedin.r2.filter.FilterChains; import com.linkedin.r2.filter.NextFilter; import com.linkedin.r2.filter.message.stream.StreamFilter; import com.linkedin.r2.message.RequestContext; import com.linkedin.r2.message.Messages; import com.linkedin.r2.message.rest.RestException; import com.linkedin.r2.message.rest.RestRequest; import com.linkedin.r2.message.rest.RestRequestBuilder; import com.linkedin.r2.message.rest.RestResponse; import com.linkedin.r2.message.rest.RestStatus; import com.linkedin.r2.message.stream.StreamRequest; import com.linkedin.r2.message.stream.StreamResponse; import com.linkedin.r2.message.stream.StreamResponseBuilder; import com.linkedin.r2.message.stream.entitystream.DrainReader; import com.linkedin.r2.message.stream.entitystream.EntityStreams; import com.linkedin.r2.message.stream.entitystream.ReadHandle; import com.linkedin.r2.message.stream.entitystream.Reader; import com.linkedin.r2.message.stream.entitystream.WriteHandle; import com.linkedin.r2.message.stream.entitystream.Writer; import com.linkedin.r2.sample.Bootstrap; import com.linkedin.r2.transport.common.Client; import com.linkedin.r2.transport.common.StreamRequestHandler; import com.linkedin.r2.transport.common.bridge.client.TransportClientAdapter; import com.linkedin.r2.transport.common.bridge.common.TransportCallback; import com.linkedin.r2.transport.common.bridge.common.TransportResponseImpl; import com.linkedin.r2.transport.common.bridge.server.TransportCallbackAdapter; import com.linkedin.r2.transport.common.bridge.server.TransportDispatcher; import com.linkedin.r2.transport.http.client.HttpClientFactory; import com.linkedin.r2.transport.http.server.HttpJettyServer; import com.linkedin.r2.transport.http.server.HttpServer; import com.linkedin.r2.transport.http.server.HttpServerFactory; import junit.framework.Assert; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import java.io.IOException; import java.net.URI; import java.util.HashMap; import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; /** * @author Zhenkai Zhu */ public class TestServerTimeoutAsyncEvent { private static final int PORT = 10001; private static final URI TIMEOUT_BEFORE_SENDING_RESPONSE_SERVER_URI = URI.create("/timeout-before-sending-response"); private static final URI TIMEOUT_AFTER_SENDING_RESPONSE_SERVER_URI = URI.create("/timeout-after-sending-response"); private static final URI THROW_BUT_SHOULD_NOT_TIMEOUT_URI = URI.create("/throw-but-should-not-timeout"); private static final URI BUGGY_FILTER_URI = URI.create("/buggy-filter"); private static final URI STREAM_EXCEPTION_FILTER_URI = URI.create("/stream-exception-filter"); private static final int ASYNC_EVENT_TIMEOUT = 2000; private static final int RESPONSE_SIZE_WRITTEN_SO_FAR = 50 * 1024; private HttpClientFactory _clientFactory; private Client _client; private HttpServer _server; private ExecutorService _asyncExecutor; @BeforeClass public void setup() throws IOException { _clientFactory = new HttpClientFactory(); Map<String, Object> clientProperties = new HashMap<String, Object>(); clientProperties.put(HttpClientFactory.HTTP_REQUEST_TIMEOUT, String.valueOf(ASYNC_EVENT_TIMEOUT * 20)); clientProperties.put(HttpClientFactory.HTTP_POOL_MIN_SIZE, "1"); clientProperties.put(HttpClientFactory.HTTP_POOL_SIZE, "1"); _client = new TransportClientAdapter(_clientFactory.getClient(clientProperties), true); final Map<URI, StreamRequestHandler> handlers = new HashMap<URI, StreamRequestHandler>(); handlers.put(TIMEOUT_BEFORE_SENDING_RESPONSE_SERVER_URI, new TimeoutBeforeRespondingRequestHandler()); handlers.put(TIMEOUT_AFTER_SENDING_RESPONSE_SERVER_URI, new TimeoutAfterRespondingRequestHandler()); handlers.put(THROW_BUT_SHOULD_NOT_TIMEOUT_URI, new ThrowHandler()); handlers.put(BUGGY_FILTER_URI, new NormalHandler()); TransportDispatcher transportDispatcher = new TransportDispatcher() { @Override public void handleRestRequest(RestRequest req, Map<String, String> wireAttrs, RequestContext requestContext, TransportCallback<RestResponse> callback) { throw new UnsupportedOperationException("This dispatcher only supports stream"); } @Override public void handleStreamRequest(StreamRequest req, Map<String, String> wireAttrs, RequestContext requestContext, TransportCallback<StreamResponse> callback) { StreamRequestHandler handler = handlers.get(req.getURI()); if (handler != null) { handler.handleRequest(req, requestContext, new TransportCallbackAdapter<StreamResponse>(callback)); } else { req.getEntityStream().setReader(new DrainReader()); callback.onResponse(TransportResponseImpl.<StreamResponse>error(new IllegalStateException("Handler not found for URI " + req.getURI()))); } } }; FilterChain filterChain = FilterChains.createStreamChain(new BuggyFilter()); _server = new HttpServerFactory(filterChain, HttpJettyServer.ServletType.ASYNC_EVENT).createServer(PORT, transportDispatcher, ASYNC_EVENT_TIMEOUT, true); _server.start(); _asyncExecutor = Executors.newSingleThreadExecutor(); } @Test public void testServerTimeoutAfterResponding() throws Exception { Future<RestResponse> futureResponse = _client.restRequest(new RestRequestBuilder(Bootstrap.createHttpURI(PORT, TIMEOUT_AFTER_SENDING_RESPONSE_SERVER_URI)).build()); // server should timeout so get should succeed RestResponse response = futureResponse.get(ASYNC_EVENT_TIMEOUT * 2, TimeUnit.MILLISECONDS); Assert.assertEquals(response.getStatus(), RestStatus.OK); Assert.assertEquals(response.getEntity().length(), RESPONSE_SIZE_WRITTEN_SO_FAR); } @Test public void testServerTimeoutBeforeResponding() throws Exception { Future<RestResponse> futureResponse = _client.restRequest(new RestRequestBuilder(Bootstrap.createHttpURI(PORT, TIMEOUT_BEFORE_SENDING_RESPONSE_SERVER_URI)).build()); try { futureResponse.get(ASYNC_EVENT_TIMEOUT * 2, TimeUnit.MILLISECONDS); Assert.fail("Should have thrown exception"); } catch (ExecutionException ex) { Throwable cause = ex.getCause(); Assert.assertNotNull(cause); Assert.assertTrue(cause instanceof RestException); RestException restException = (RestException) cause; Assert.assertEquals(restException.getResponse().getStatus(), RestStatus.INTERNAL_SERVER_ERROR); Assert.assertEquals(restException.getResponse().getEntity().asString("UTF8"), "Server timeout"); } } @Test public void testServerThrowButShouldNotTimeout() throws Exception { RestRequest request = new RestRequestBuilder(Bootstrap.createHttpURI(PORT, THROW_BUT_SHOULD_NOT_TIMEOUT_URI)) .setEntity(new byte[10240]).build(); _client.restRequest(request); Future<RestResponse> futureResponse = _client.restRequest(request); // if server times out, our second request would fail with TimeoutException because it's blocked by first one try { futureResponse.get(ASYNC_EVENT_TIMEOUT / 2, TimeUnit.MILLISECONDS); Assert.fail("Should fail with ExecutionException"); } catch (ExecutionException ex) { Assert.assertTrue(ex.getCause() instanceof RestException); RestException restException = (RestException)ex.getCause(); Assert.assertTrue(restException.getResponse().getEntity().asString("UTF8").contains("Server throw for test.")); } } @Test public void testFilterThrowButShouldNotTimeout() throws Exception { RestRequest request = new RestRequestBuilder(Bootstrap.createHttpURI(PORT, BUGGY_FILTER_URI)) .setEntity(new byte[10240]).build(); _client.restRequest(request); Future<RestResponse> futureResponse = _client.restRequest(request); // if server times out, our second request would fail with TimeoutException because it's blocked by first one try { futureResponse.get(ASYNC_EVENT_TIMEOUT / 2, TimeUnit.MILLISECONDS); Assert.fail("Should fail with ExecutionException"); } catch (ExecutionException ex) { Assert.assertTrue(ex.getCause() instanceof RestException); RestException restException = (RestException)ex.getCause(); Assert.assertTrue(restException.getResponse().getEntity().asString("UTF8").contains("Buggy filter throws.")); } } @Test public void testFilterNotCancelButShouldNotTimeout() throws Exception { RestRequest request = new RestRequestBuilder(Bootstrap.createHttpURI(PORT, STREAM_EXCEPTION_FILTER_URI)) .setEntity(new byte[10240]).build(); _client.restRequest(request); Future<RestResponse> futureResponse = _client.restRequest(request); // if server times out, our second request would fail with TimeoutException because it's blocked by first one try { futureResponse.get(ASYNC_EVENT_TIMEOUT / 2, TimeUnit.MILLISECONDS); Assert.fail("Should fail with ExecutionException"); } catch (ExecutionException ex) { Assert.assertTrue(ex.getCause() instanceof RestException); RestException restException = (RestException)ex.getCause(); Assert.assertTrue(restException.getResponse().getEntity().asString("UTF8").contains("StreamException in filter.")); } } @AfterClass public void tearDown() throws Exception { final FutureCallback<None> clientShutdownCallback = new FutureCallback<None>(); _client.shutdown(clientShutdownCallback); clientShutdownCallback.get(); final FutureCallback<None> factoryShutdownCallback = new FutureCallback<None>(); _clientFactory.shutdown(factoryShutdownCallback); factoryShutdownCallback.get(); if (_server != null) { _server.stop(); _server.waitForStop(); } _asyncExecutor.shutdown(); } private class ThrowHandler implements StreamRequestHandler { @Override public void handleRequest(final StreamRequest request, RequestContext requestContext, Callback<StreamResponse> callback) { throw new RuntimeException("Server throw for test."); } } private class NormalHandler implements StreamRequestHandler { @Override public void handleRequest(final StreamRequest request, RequestContext requestContext, Callback<StreamResponse> callback) { request.getEntityStream().setReader(new DrainReader()); callback.onSuccess(new StreamResponseBuilder().build(EntityStreams.emptyStream())); } } private class TimeoutBeforeRespondingRequestHandler implements StreamRequestHandler { @Override public void handleRequest(final StreamRequest request, RequestContext requestContext, Callback<StreamResponse> callback) { _asyncExecutor.execute(new Runnable() { @Override public void run() { request.getEntityStream().setReader(new Reader() { @Override public void onInit(ReadHandle rh) { } @Override public void onDataAvailable(ByteString data) { } @Override public void onDone() { } @Override public void onError(Throwable e) { } }); } }); } } private class TimeoutAfterRespondingRequestHandler implements StreamRequestHandler { @Override public void handleRequest(final StreamRequest request, RequestContext requestContext, final Callback<StreamResponse> callback) { _asyncExecutor.execute(new Runnable() { @Override public void run() { request.getEntityStream().setReader(new DrainReader()); Writer noFinishWriter = new Writer() { private WriteHandle _wh; boolean _written = false; @Override public void onInit(WriteHandle wh) { _wh = wh; } @Override public void onWritePossible() { if (!_written) { _wh.write(ByteString.copy(new byte[RESPONSE_SIZE_WRITTEN_SO_FAR])); } } @Override public void onAbort(Throwable e) { } }; callback.onSuccess(new StreamResponseBuilder().build(EntityStreams.newEntityStream(noFinishWriter))); } }); } } private class BuggyFilter implements StreamFilter { @Override public void onStreamRequest(StreamRequest req, RequestContext requestContext, Map<String, String> wireAttrs, NextFilter<StreamRequest, StreamResponse> nextFilter) { if (req.getURI().equals(BUGGY_FILTER_URI)) { throw new RuntimeException("Buggy filter throws."); } if (req.getURI().equals(STREAM_EXCEPTION_FILTER_URI)) { nextFilter.onError(Messages.toStreamException(RestException.forError(500, "StreamException in filter.")), requestContext, wireAttrs); return; } nextFilter.onRequest(req, requestContext, wireAttrs); } } }