| // |
| // ======================================================================== |
| // Copyright (c) 1995-2015 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.IOException; |
| import java.nio.ByteBuffer; |
| import java.util.concurrent.CountDownLatch; |
| import java.util.concurrent.TimeUnit; |
| import java.util.concurrent.TimeoutException; |
| |
| 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.HttpVersion; |
| import org.eclipse.jetty.http.MetaData; |
| import org.eclipse.jetty.http2.HTTP2Session; |
| 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.GoAwayFrame; |
| import org.eclipse.jetty.http2.frames.HeadersFrame; |
| import org.eclipse.jetty.http2.frames.ResetFrame; |
| 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; |
| |
| import static org.hamcrest.core.IsInstanceOf.instanceOf; |
| import static org.junit.Assert.assertThat; |
| |
| public class IdleTimeoutTest extends AbstractTest |
| { |
| private final int idleTimeout = 1000; |
| |
| @Test |
| public void testServerEnforcingIdleTimeout() throws Exception |
| { |
| start(new ServerSessionListener.Adapter() |
| { |
| @Override |
| public Stream.Listener onNewStream(Stream stream, HeadersFrame requestFrame) |
| { |
| stream.setIdleTimeout(10 * idleTimeout); |
| MetaData.Response metaData = new MetaData.Response(HttpVersion.HTTP_2, 200, new HttpFields()); |
| HeadersFrame responseFrame = new HeadersFrame(stream.getId(), metaData, null, true); |
| stream.headers(responseFrame, Callback.NOOP); |
| return null; |
| } |
| }); |
| connector.setIdleTimeout(idleTimeout); |
| |
| final CountDownLatch latch = new CountDownLatch(1); |
| Session session = newClient(new Session.Listener.Adapter() |
| { |
| @Override |
| public void onClose(Session session, GoAwayFrame frame) |
| { |
| if (session.isClosed() && ((HTTP2Session)session).isDisconnected()) |
| latch.countDown(); |
| } |
| }); |
| |
| MetaData.Request metaData = newRequest("GET", new HttpFields()); |
| HeadersFrame requestFrame = new HeadersFrame(metaData, null, true); |
| session.newStream(requestFrame, new Promise.Adapter<Stream>() |
| { |
| @Override |
| public void succeeded(Stream stream) |
| { |
| stream.setIdleTimeout(10 * idleTimeout); |
| } |
| }, new Stream.Listener.Adapter()); |
| |
| Assert.assertTrue(latch.await(2 * idleTimeout, TimeUnit.MILLISECONDS)); |
| |
| sleep(1000); |
| } |
| |
| @Test |
| public void testServerEnforcingIdleTimeoutWithUnrespondedStream() throws Exception |
| { |
| start(new ServerSessionListener.Adapter() |
| { |
| @Override |
| public Stream.Listener onNewStream(Stream stream, HeadersFrame frame) |
| { |
| stream.setIdleTimeout(10 * idleTimeout); |
| return null; |
| } |
| }); |
| connector.setIdleTimeout(idleTimeout); |
| |
| final CountDownLatch latch = new CountDownLatch(1); |
| Session session = newClient(new Session.Listener.Adapter() |
| { |
| @Override |
| public void onClose(Session session, GoAwayFrame frame) |
| { |
| if (session.isClosed() && ((HTTP2Session)session).isDisconnected()) |
| latch.countDown(); |
| } |
| }); |
| |
| // The request is not replied, and the server should idle timeout. |
| MetaData.Request metaData = newRequest("GET", new HttpFields()); |
| HeadersFrame requestFrame = new HeadersFrame(metaData, null, true); |
| session.newStream(requestFrame, new Promise.Adapter<Stream>() |
| { |
| @Override |
| public void succeeded(Stream stream) |
| { |
| stream.setIdleTimeout(10 * idleTimeout); |
| } |
| }, new Stream.Listener.Adapter()); |
| |
| Assert.assertTrue(latch.await(2 * idleTimeout, TimeUnit.MILLISECONDS)); |
| } |
| |
| @Test |
| public void testServerNotEnforcingIdleTimeoutWithinCallback() throws Exception |
| { |
| start(new ServerSessionListener.Adapter() |
| { |
| @Override |
| public Stream.Listener onNewStream(Stream stream, HeadersFrame frame) |
| { |
| stream.setIdleTimeout(10 * idleTimeout); |
| // Stay in the callback for more than idleTimeout, |
| // but not for an integer number of idle timeouts, |
| // to avoid a race where the idle timeout fires |
| // again before we can send the headers to the client. |
| sleep(idleTimeout + idleTimeout / 2); |
| MetaData.Response metaData = new MetaData.Response(HttpVersion.HTTP_2, 200, new HttpFields()); |
| HeadersFrame responseFrame = new HeadersFrame(stream.getId(), metaData, null, true); |
| stream.headers(responseFrame, Callback.NOOP); |
| return null; |
| } |
| }); |
| connector.setIdleTimeout(idleTimeout); |
| |
| final CountDownLatch closeLatch = new CountDownLatch(1); |
| Session session = newClient(new ServerSessionListener.Adapter() |
| { |
| @Override |
| public void onClose(Session session, GoAwayFrame frame) |
| { |
| closeLatch.countDown(); |
| } |
| }); |
| |
| final CountDownLatch replyLatch = new CountDownLatch(1); |
| MetaData.Request metaData = newRequest("GET", new HttpFields()); |
| HeadersFrame requestFrame = new HeadersFrame(metaData, null, true); |
| session.newStream(requestFrame, new Promise.Adapter<Stream>() |
| { |
| @Override |
| public void succeeded(Stream stream) |
| { |
| stream.setIdleTimeout(10 * idleTimeout); |
| } |
| }, new Stream.Listener.Adapter() |
| { |
| @Override |
| public void onHeaders(Stream stream, HeadersFrame frame) |
| { |
| replyLatch.countDown(); |
| } |
| }); |
| |
| Assert.assertTrue(replyLatch.await(3 * idleTimeout, TimeUnit.MILLISECONDS)); |
| |
| // Just make sure onClose() has never been called, but don't wait too much |
| Assert.assertFalse(closeLatch.await(idleTimeout / 2, TimeUnit.MILLISECONDS)); |
| } |
| |
| @Test |
| public void testClientEnforcingIdleTimeout() throws Exception |
| { |
| final CountDownLatch closeLatch = new CountDownLatch(1); |
| start(new ServerSessionListener.Adapter() |
| { |
| @Override |
| public Stream.Listener onNewStream(Stream stream, HeadersFrame frame) |
| { |
| stream.setIdleTimeout(10 * idleTimeout); |
| MetaData.Response metaData = new MetaData.Response(HttpVersion.HTTP_2, 200, new HttpFields()); |
| HeadersFrame responseFrame = new HeadersFrame(stream.getId(), metaData, null, true); |
| stream.headers(responseFrame, Callback.NOOP); |
| return null; |
| } |
| |
| @Override |
| public void onClose(Session session, GoAwayFrame frame) |
| { |
| if (session.isClosed() && ((HTTP2Session)session).isDisconnected()) |
| closeLatch.countDown(); |
| } |
| }); |
| client.setIdleTimeout(idleTimeout); |
| |
| Session session = newClient(new Session.Listener.Adapter()); |
| MetaData.Request metaData = newRequest("GET", new HttpFields()); |
| HeadersFrame requestFrame = new HeadersFrame(metaData, null, true); |
| session.newStream(requestFrame, new Promise.Adapter<Stream>() |
| { |
| @Override |
| public void succeeded(Stream stream) |
| { |
| stream.setIdleTimeout(10 * idleTimeout); |
| } |
| }, new Stream.Listener.Adapter()); |
| |
| Assert.assertTrue(closeLatch.await(2 * idleTimeout, TimeUnit.MILLISECONDS)); |
| Assert.assertTrue(session.isClosed()); |
| } |
| |
| @Test |
| public void testClientEnforcingIdleTimeoutWithUnrespondedStream() throws Exception |
| { |
| final CountDownLatch closeLatch = new CountDownLatch(1); |
| start(new ServerSessionListener.Adapter() |
| { |
| @Override |
| public Stream.Listener onNewStream(Stream stream, HeadersFrame frame) |
| { |
| stream.setIdleTimeout(10 * idleTimeout); |
| return null; |
| } |
| |
| @Override |
| public void onClose(Session session, GoAwayFrame frame) |
| { |
| if (session.isClosed() && ((HTTP2Session)session).isDisconnected()) |
| closeLatch.countDown(); |
| } |
| }); |
| client.setIdleTimeout(idleTimeout); |
| |
| Session session = newClient(new Session.Listener.Adapter()); |
| MetaData.Request metaData = newRequest("GET", new HttpFields()); |
| HeadersFrame requestFrame = new HeadersFrame(metaData, null, true); |
| session.newStream(requestFrame, new Promise.Adapter<Stream>() |
| { |
| @Override |
| public void succeeded(Stream stream) |
| { |
| stream.setIdleTimeout(10 * idleTimeout); |
| } |
| }, new Stream.Listener.Adapter()); |
| |
| Assert.assertTrue(closeLatch.await(2 * idleTimeout, TimeUnit.MILLISECONDS)); |
| } |
| |
| @Test |
| public void testClientNotEnforcingIdleTimeoutWithinCallback() throws Exception |
| { |
| final CountDownLatch closeLatch = new CountDownLatch(1); |
| start(new ServerSessionListener.Adapter() |
| { |
| @Override |
| public Stream.Listener onNewStream(Stream stream, HeadersFrame frame) |
| { |
| stream.setIdleTimeout(10 * idleTimeout); |
| MetaData.Response metaData = new MetaData.Response(HttpVersion.HTTP_2, 200, new HttpFields()); |
| HeadersFrame responseFrame = new HeadersFrame(stream.getId(), metaData, null, true); |
| stream.headers(responseFrame, Callback.NOOP); |
| return null; |
| } |
| |
| @Override |
| public void onClose(Session session, GoAwayFrame frame) |
| { |
| closeLatch.countDown(); |
| } |
| }); |
| client.setIdleTimeout(idleTimeout); |
| |
| Session session = newClient(new Session.Listener.Adapter()); |
| |
| final CountDownLatch replyLatch = new CountDownLatch(1); |
| MetaData.Request metaData = newRequest("GET", new HttpFields()); |
| HeadersFrame requestFrame = new HeadersFrame(metaData, null, true); |
| session.newStream(requestFrame, new Promise.Adapter<Stream>() |
| { |
| @Override |
| public void succeeded(Stream stream) |
| { |
| stream.setIdleTimeout(10 * idleTimeout); |
| } |
| }, new Stream.Listener.Adapter() |
| { |
| @Override |
| public void onHeaders(Stream stream, HeadersFrame frame) |
| { |
| // Stay in the callback for more than idleTimeout, |
| // but not for an integer number of idle timeouts, |
| // to avoid that the idle timeout fires again. |
| sleep(idleTimeout + idleTimeout / 2); |
| replyLatch.countDown(); |
| } |
| }); |
| |
| Assert.assertFalse(closeLatch.await(2 * idleTimeout, TimeUnit.MILLISECONDS)); |
| Assert.assertTrue(replyLatch.await(3 * idleTimeout, TimeUnit.MILLISECONDS)); |
| } |
| |
| @Test |
| public void testClientEnforcingStreamIdleTimeout() throws Exception |
| { |
| final int idleTimeout = 1000; |
| start(new HttpServlet() |
| { |
| @Override |
| protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException |
| { |
| sleep(2 * idleTimeout); |
| } |
| }); |
| |
| Session session = newClient(new Session.Listener.Adapter()); |
| |
| final CountDownLatch dataLatch = new CountDownLatch(1); |
| final CountDownLatch timeoutLatch = new CountDownLatch(1); |
| MetaData.Request metaData = newRequest("GET", new HttpFields()); |
| HeadersFrame requestFrame = new HeadersFrame(metaData, null, true); |
| session.newStream(requestFrame, new Promise.Adapter<Stream>() |
| { |
| @Override |
| public void succeeded(Stream stream) |
| { |
| stream.setIdleTimeout(idleTimeout); |
| } |
| }, new Stream.Listener.Adapter() |
| { |
| @Override |
| public void onData(Stream stream, DataFrame frame, Callback callback) |
| { |
| dataLatch.countDown(); |
| } |
| |
| @Override |
| public void onTimeout(Stream stream, Throwable x) |
| { |
| assertThat(x, instanceOf(TimeoutException.class)); |
| timeoutLatch.countDown(); |
| } |
| }); |
| |
| Assert.assertTrue(timeoutLatch.await(5, TimeUnit.SECONDS)); |
| // We must not receive any DATA frame. |
| Assert.assertFalse(dataLatch.await(2 * idleTimeout, TimeUnit.MILLISECONDS)); |
| // Stream must be gone. |
| Assert.assertTrue(session.getStreams().isEmpty()); |
| // Session must not be closed, nor disconnected. |
| Assert.assertFalse(session.isClosed()); |
| Assert.assertFalse(((HTTP2Session)session).isDisconnected()); |
| } |
| |
| @Test |
| public void testServerEnforcingStreamIdleTimeout() throws Exception |
| { |
| final CountDownLatch timeoutLatch = new CountDownLatch(1); |
| start(new ServerSessionListener.Adapter() |
| { |
| @Override |
| public Stream.Listener onNewStream(Stream stream, HeadersFrame frame) |
| { |
| stream.setIdleTimeout(idleTimeout); |
| return new Stream.Listener.Adapter() |
| { |
| @Override |
| public void onTimeout(Stream stream, Throwable x) |
| { |
| timeoutLatch.countDown(); |
| } |
| }; |
| } |
| }); |
| |
| final CountDownLatch resetLatch = new CountDownLatch(1); |
| Session session = newClient(new Session.Listener.Adapter()); |
| MetaData.Request metaData = newRequest("GET", new HttpFields()); |
| // Stream does not end here, but we won't send any DATA frame. |
| HeadersFrame requestFrame = new HeadersFrame(metaData, null, false); |
| session.newStream(requestFrame, new Promise.Adapter<>(), new Stream.Listener.Adapter() |
| { |
| @Override |
| public void onReset(Stream stream, ResetFrame frame) |
| { |
| resetLatch.countDown(); |
| } |
| }); |
| |
| Assert.assertTrue(timeoutLatch.await(5, TimeUnit.SECONDS)); |
| Assert.assertTrue(resetLatch.await(5, TimeUnit.SECONDS)); |
| // Stream must be gone. |
| Assert.assertTrue(session.getStreams().isEmpty()); |
| // Session must not be closed, nor disconnected. |
| Assert.assertFalse(session.isClosed()); |
| Assert.assertFalse(((HTTP2Session)session).isDisconnected()); |
| } |
| |
| @Test |
| public void testStreamIdleTimeoutIsNotEnforcedWhenReceiving() throws Exception |
| { |
| final CountDownLatch timeoutLatch = new CountDownLatch(1); |
| start(new ServerSessionListener.Adapter() |
| { |
| @Override |
| public Stream.Listener onNewStream(Stream stream, HeadersFrame frame) |
| { |
| stream.setIdleTimeout(idleTimeout); |
| return new Stream.Listener.Adapter() |
| { |
| @Override |
| public void onTimeout(Stream stream, Throwable x) |
| { |
| timeoutLatch.countDown(); |
| } |
| }; |
| } |
| }); |
| |
| Session session = newClient(new Session.Listener.Adapter()); |
| MetaData.Request metaData = newRequest("GET", new HttpFields()); |
| HeadersFrame requestFrame = new HeadersFrame(metaData, null, false); |
| FuturePromise<Stream> promise = new FuturePromise<>(); |
| session.newStream(requestFrame, promise, new Stream.Listener.Adapter()); |
| final Stream stream = promise.get(5, TimeUnit.SECONDS); |
| |
| sleep(idleTimeout / 2); |
| final CountDownLatch dataLatch = new CountDownLatch(1); |
| stream.data(new DataFrame(stream.getId(), ByteBuffer.allocate(1), false), new Callback() |
| { |
| private int sends; |
| |
| @Override |
| public void succeeded() |
| { |
| sleep(idleTimeout / 2); |
| final boolean last = ++sends == 2; |
| stream.data(new DataFrame(stream.getId(), ByteBuffer.allocate(1), last), !last ? this : new Callback.NonBlocking() |
| { |
| @Override |
| public void succeeded() |
| { |
| dataLatch.countDown(); |
| } |
| }); |
| } |
| }); |
| |
| Assert.assertTrue(dataLatch.await(2 * idleTimeout, TimeUnit.MILLISECONDS)); |
| Assert.assertFalse(timeoutLatch.await(0, TimeUnit.SECONDS)); |
| } |
| |
| @Test |
| public void testStreamIdleTimeoutIsNotEnforcedWhenSending() throws Exception |
| { |
| final CountDownLatch resetLatch = new CountDownLatch(1); |
| start(new ServerSessionListener.Adapter() |
| { |
| @Override |
| public Stream.Listener onNewStream(Stream stream, HeadersFrame frame) |
| { |
| MetaData.Response response = new MetaData.Response(HttpVersion.HTTP_2, 200, new HttpFields()); |
| stream.headers(new HeadersFrame(stream.getId(), response, null, true), Callback.NOOP); |
| return null; |
| } |
| |
| @Override |
| public void onReset(Session session, ResetFrame frame) |
| { |
| resetLatch.countDown(); |
| } |
| }); |
| |
| Session session = newClient(new Session.Listener.Adapter()); |
| MetaData.Request metaData = newRequest("GET", new HttpFields()); |
| HeadersFrame requestFrame = new HeadersFrame(metaData, null, false); |
| FuturePromise<Stream> promise = new FuturePromise<Stream>() |
| { |
| @Override |
| public void succeeded(Stream stream) |
| { |
| stream.setIdleTimeout(idleTimeout); |
| super.succeeded(stream); |
| } |
| }; |
| session.newStream(requestFrame, promise, new Stream.Listener.Adapter()); |
| final Stream stream = promise.get(5, TimeUnit.SECONDS); |
| |
| Callback.Completable completable1 = new Callback.Completable(); |
| sleep(idleTimeout / 2); |
| stream.data(new DataFrame(stream.getId(), ByteBuffer.allocate(1), false), completable1); |
| completable1.thenCompose(nil -> |
| { |
| Callback.Completable completable2 = new Callback.Completable(); |
| sleep(idleTimeout / 2); |
| stream.data(new DataFrame(stream.getId(), ByteBuffer.allocate(1), false), completable2); |
| return completable2; |
| }).thenRun(() -> |
| { |
| sleep(idleTimeout / 2); |
| stream.data(new DataFrame(stream.getId(), ByteBuffer.allocate(1), true), Callback.NOOP); |
| }); |
| |
| Assert.assertFalse(resetLatch.await(0, TimeUnit.SECONDS)); |
| } |
| |
| private void sleep(long value) |
| { |
| try |
| { |
| TimeUnit.MILLISECONDS.sleep(value); |
| } |
| catch (InterruptedException x) |
| { |
| Assert.fail(); |
| } |
| } |
| } |