//
// ========================================================================
// Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.http2.client;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import javax.servlet.AsyncContext;
import javax.servlet.AsyncEvent;
import javax.servlet.AsyncListener;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.MetaData;
import org.eclipse.jetty.http2.api.Session;
import org.eclipse.jetty.http2.api.Stream;
import org.eclipse.jetty.http2.api.server.ServerSessionListener;
import org.eclipse.jetty.http2.frames.DataFrame;
import org.eclipse.jetty.http2.frames.HeadersFrame;
import org.eclipse.jetty.http2.server.HTTP2ServerConnectionFactory;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.FuturePromise;
import org.eclipse.jetty.util.Promise;
import org.junit.Assert;
import org.junit.Test;
public class AsyncServletTest extends AbstractTest
{
@Test
public void testStartAsyncThenDispatch() throws Exception
{
byte[] content = new byte[1024];
new Random().nextBytes(content);
start(new HttpServlet()
{
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
AsyncContext asyncContext = (AsyncContext)request.getAttribute(AsyncContext.class.getName());
if (asyncContext == null)
{
AsyncContext context = request.startAsync();
context.setTimeout(0);
request.setAttribute(AsyncContext.class.getName(), context);
context.start(() ->
{
sleep(1000);
context.dispatch();
});
}
else
{
response.getOutputStream().write(content);
}
}
});
Session session = newClient(new Session.Listener.Adapter());
HttpFields fields = new HttpFields();
MetaData.Request metaData = newRequest("GET", fields);
HeadersFrame frame = new HeadersFrame(metaData, null, true);
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
CountDownLatch latch = new CountDownLatch(1);
session.newStream(frame, new Promise.Adapter<>(), new Stream.Listener.Adapter()
{
@Override
public void onData(Stream stream, DataFrame frame, Callback callback)
{
try
{
buffer.write(BufferUtil.toArray(frame.getData()));
callback.succeeded();
if (frame.isEndStream())
latch.countDown();
}
catch (IOException x)
{
callback.failed(x);
}
}
});
Assert.assertTrue(latch.await(5, TimeUnit.SECONDS));
Assert.assertArrayEquals(content, buffer.toByteArray());
}
@Test
public void testStartAsyncThenClientSessionIdleTimeout() throws Exception
{
CountDownLatch serverLatch = new CountDownLatch(1);
start(new AsyncOnErrorServlet(serverLatch));
long idleTimeout = 1000;
client.setIdleTimeout(idleTimeout);
Session session = newClient(new Session.Listener.Adapter());
HttpFields fields = new HttpFields();
MetaData.Request metaData = newRequest("GET", fields);
HeadersFrame frame = new HeadersFrame(metaData, null, true);
FuturePromise<Stream> promise = new FuturePromise<>();
CountDownLatch clientLatch = new CountDownLatch(1);
session.newStream(frame, promise, new Stream.Listener.Adapter()
{
@Override
public void onHeaders(Stream stream, HeadersFrame frame)
{
MetaData.Response response = (MetaData.Response)frame.getMetaData();
if (response.getStatus() == HttpStatus.INTERNAL_SERVER_ERROR_500 && frame.isEndStream())
clientLatch.countDown();
}
});
Stream stream = promise.get(5, TimeUnit.SECONDS);
stream.setIdleTimeout(10 * idleTimeout);
// When the client closes, the server receives the
// corresponding frame and acts by notifying the failure,
// which sends back to the client the error response.
Assert.assertTrue(serverLatch.await(2 * idleTimeout, TimeUnit.MILLISECONDS));
Assert.assertTrue(clientLatch.await(2 * idleTimeout, TimeUnit.MILLISECONDS));
}
@Test
public void testStartAsyncThenClientStreamIdleTimeout() throws Exception
{
CountDownLatch serverLatch = new CountDownLatch(1);
start(new AsyncOnErrorServlet(serverLatch));
long idleTimeout = 1000;
client.setIdleTimeout(10 * idleTimeout);
Session session = newClient(new Session.Listener.Adapter());
HttpFields fields = new HttpFields();
MetaData.Request metaData = newRequest("GET", fields);
HeadersFrame frame = new HeadersFrame(metaData, null, true);
FuturePromise<Stream> promise = new FuturePromise<>();
CountDownLatch clientLatch = new CountDownLatch(1);
session.newStream(frame, promise, new Stream.Listener.Adapter()
{
@Override
public boolean onIdleTimeout(Stream stream, Throwable x)
{
clientLatch.countDown();
return true;
}
});
Stream stream = promise.get(5, TimeUnit.SECONDS);
stream.setIdleTimeout(idleTimeout);
// When the client resets, the server receives the
// corresponding frame and acts by notifying the failure,
// but the response is not sent back to the client.
Assert.assertTrue(serverLatch.await(2 * idleTimeout, TimeUnit.MILLISECONDS));
Assert.assertTrue(clientLatch.await(2 * idleTimeout, TimeUnit.MILLISECONDS));
}
@Test
public void testStartAsyncThenServerSessionIdleTimeout() throws Exception
{
testStartAsyncThenServerIdleTimeout(1000, 10 * 1000);
}
@Test
public void testStartAsyncThenServerStreamIdleTimeout() throws Exception
{
testStartAsyncThenServerIdleTimeout(10 * 1000, 1000);
}
private void testStartAsyncThenServerIdleTimeout(long sessionTimeout, long streamTimeout) throws Exception
{
prepareServer(new HTTP2ServerConnectionFactory(new HttpConfiguration())
{
@Override
protected ServerSessionListener newSessionListener(Connector connector, EndPoint endPoint)
{
return new HTTPServerSessionListener(connector, endPoint)
{
@Override
public Stream.Listener onNewStream(Stream stream, HeadersFrame frame)
{
stream.setIdleTimeout(streamTimeout);
return super.onNewStream(stream, frame);
}
};
}
});
connector.setIdleTimeout(sessionTimeout);
ServletContextHandler context = new ServletContextHandler(server, "/");
long timeout = Math.min(sessionTimeout, streamTimeout);
CountDownLatch errorLatch = new CountDownLatch(1);
context.addServlet(new ServletHolder(new HttpServlet()
{
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
AsyncContext asyncContext = (AsyncContext)request.getAttribute(AsyncContext.class.getName());
if (asyncContext == null)
{
AsyncContext context = request.startAsync();
context.setTimeout(2 * timeout);
request.setAttribute(AsyncContext.class.getName(), context);
context.addListener(new AsyncListener()
{
@Override
public void onComplete(AsyncEvent event) throws IOException
{
}
@Override
public void onTimeout(AsyncEvent event) throws IOException
{
event.getAsyncContext().complete();
}
@Override
public void onError(AsyncEvent event) throws IOException
{
errorLatch.countDown();
}
@Override
public void onStartAsync(AsyncEvent event) throws IOException
{
}
});
}
else
{
throw new ServletException();
}
}
}), servletPath + "/*");
server.start();
prepareClient();
client.start();
Session session = newClient(new Session.Listener.Adapter());
HttpFields fields = new HttpFields();
MetaData.Request metaData = newRequest("GET", fields);
HeadersFrame frame = new HeadersFrame(metaData, null, true);
CountDownLatch clientLatch = new CountDownLatch(1);
session.newStream(frame, new Promise.Adapter<>(), new Stream.Listener.Adapter()
{
@Override
public void onHeaders(Stream stream, HeadersFrame frame)
{
MetaData.Response response = (MetaData.Response)frame.getMetaData();
if (response.getStatus() == HttpStatus.OK_200 && frame.isEndStream())
clientLatch.countDown();
}
});
// When the server idle times out, but the request has been dispatched
// then the server must ignore the idle timeout as per Servlet semantic.
Assert.assertFalse(errorLatch.await(2 * timeout, TimeUnit.MILLISECONDS));
Assert.assertTrue(clientLatch.await(2 * timeout, TimeUnit.MILLISECONDS));
}
private void sleep(long ms)
{
try
{
Thread.sleep(ms);
}
catch (InterruptedException x)
{
x.printStackTrace();
}
}
private static class AsyncOnErrorServlet extends HttpServlet implements AsyncListener
{
private final CountDownLatch latch;
public AsyncOnErrorServlet(CountDownLatch latch)
{
this.latch = latch;
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
AsyncContext asyncContext = (AsyncContext)request.getAttribute(AsyncContext.class.getName());
if (asyncContext == null)
{
AsyncContext context = request.startAsync();
context.setTimeout(0);
request.setAttribute(AsyncContext.class.getName(), context);
context.addListener(this);
}
else
{
throw new ServletException();
}
}
@Override
public void onComplete(AsyncEvent event) throws IOException
{
}
@Override
public void onTimeout(AsyncEvent event) throws IOException
{
}
@Override
public void onError(AsyncEvent event) throws IOException
{
HttpServletResponse response = (HttpServletResponse)event.getSuppliedResponse();
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR_500);
event.getAsyncContext().complete();
latch.countDown();
}
@Override
public void onStartAsync(AsyncEvent event) throws IOException
{
}
}
}