// // ======================================================================== // 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.continuation; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; import java.io.IOException; import java.io.InputStream; import java.net.Socket; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.Timer; import java.util.TimerTask; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.server.Handler; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.RequestLog; import org.eclipse.jetty.server.Response; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.server.handler.RequestLogHandler; import org.eclipse.jetty.server.handler.StatisticsHandler; import org.eclipse.jetty.servlet.FilterHolder; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.servlet.ServletHandler; import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.component.AbstractLifeCycle; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameter; import org.junit.runners.Parameterized.Parameters; @RunWith(Parameterized.class) public class ContinuationsTest { @SuppressWarnings("serial") public static class SneakyList extends ArrayList<String> { @Override public boolean add(String e) { // System.err.printf("add(%s)%n",e); return super.add(e); } }; @Parameters(name="{0}") public static List<Object[]> data() { List<Object[]> setup = new ArrayList<>(); // Servlet3 / AsyncContext Setup { String description = "Servlet 3 Setup"; Class<? extends Continuation> expectedImplClass = Servlet3Continuation.class; List<String> log = new ArrayList<>(); RequestLogHandler servlet3Setup = new RequestLogHandler(); servlet3Setup.setRequestLog(new Log(log)); ServletContextHandler servletContext = new ServletContextHandler(); servlet3Setup.setHandler(servletContext); ServletHandler servletHandler=servletContext.getServletHandler(); List<String> history = new SneakyList(); Listener listener = new Listener(history); ServletHolder holder=new ServletHolder(new SuspendServlet(history, listener)); holder.setAsyncSupported(true); servletHandler.addServletWithMapping(holder, "/"); setup.add(new Object[]{description,servlet3Setup,history,listener,expectedImplClass,log}); } // Faux Continuations Setup { String description = "Faux Setup"; Class<? extends Continuation> expectedImplClass = FauxContinuation.class; // no log for this setup List<String> log = null; ServletContextHandler fauxSetup = new ServletContextHandler(); ServletHandler servletHandler=fauxSetup.getServletHandler(); List<String> history = new SneakyList(); Listener listener = new Listener(history); ServletHolder holder=new ServletHolder(new SuspendServlet(history, listener)); servletHandler.addServletWithMapping(holder,"/"); FilterHolder filter= servletHandler.addFilterWithMapping(ContinuationFilter.class,"/*",null); filter.setInitParameter("debug","true"); filter.setInitParameter("faux","true"); setup.add(new Object[]{description,fauxSetup,history,listener,expectedImplClass,log}); } return setup; } @Parameter(0) public String setupDescription; @Parameter(1) public Handler setupHandler; @Parameter(2) public List<String> history; @Parameter(3) public Listener listener; @Parameter(4) public Class<? extends Continuation> expectedImplClass; @Parameter(5) public List<String> log; @Test public void testNormal() throws Exception { String response = process(null, null); assertThat(response, startsWith("HTTP/1.1 200 OK")); assertThat(response, containsString("NORMAL")); assertThat(history, hasItem(expectedImplClass.getName())); assertThat(history, not(hasItem("onTimeout"))); assertThat(history, not(hasItem("onComplete"))); } @Test public void testSleep() throws Exception { String response = process("sleep=200", null); assertThat(response, startsWith("HTTP/1.1 200 OK")); assertThat(response, containsString("SLEPT")); assertThat(history, not(hasItem("onTimeout"))); assertThat(history, not(hasItem("onComplete"))); } @Test public void testSuspend() throws Exception { String response = process("suspend=200", null); assertThat(response, startsWith("HTTP/1.1 200 OK")); assertThat(response, containsString("TIMEOUT")); assertThat(history, hasItem("onTimeout")); assertThat(history, hasItem("onComplete")); } @Test public void testSuspendWaitResume() throws Exception { String response = process("suspend=200&resume=10", null); assertThat(response, startsWith("HTTP/1.1 200 OK")); assertThat(response, containsString("RESUMED")); assertThat(history, not(hasItem("onTimeout"))); assertThat(history, hasItem("onComplete")); } @Test public void testSuspendResume() throws Exception { String response = process("suspend=200&resume=0", null); assertThat(response, startsWith("HTTP/1.1 200 OK")); assertThat(response, containsString("RESUMED")); assertThat(history, not(hasItem("onTimeout"))); assertThat(history, hasItem("onComplete")); } @Test public void testSuspendWaitComplete() throws Exception { String response = process("suspend=200&complete=50", null); assertThat(response, startsWith("HTTP/1.1 200 OK")); assertThat(response, containsString("COMPLETED")); assertThat(history, hasItem("initial")); assertThat(history, not(hasItem("!initial"))); assertThat(history, not(hasItem("onTimeout"))); assertThat(history, hasItem("onComplete")); } @Test public void testSuspendComplete() throws Exception { String response = process("suspend=200&complete=0", null); assertThat(response, startsWith("HTTP/1.1 200 OK")); assertThat(response, containsString("COMPLETED")); assertThat(history, hasItem("initial")); assertThat(history, not(hasItem("!initial"))); assertThat(history, not(hasItem("onTimeout"))); assertThat(history, hasItem("onComplete")); } @Test public void testSuspendWaitResumeSuspendWaitResume() throws Exception { String response = process("suspend=1000&resume=10&suspend2=1000&resume2=10", null); assertThat(response, startsWith("HTTP/1.1 200 OK")); assertThat(response, containsString("RESUMED")); assertEquals(2, count(history, "suspend")); assertEquals(2, count(history, "resume")); assertEquals(0, count(history, "onTimeout")); assertEquals(1, count(history, "onComplete")); } @Test public void testSuspendWaitResumeSuspendComplete() throws Exception { String response = process("suspend=1000&resume=10&suspend2=1000&complete2=10", null); assertThat(response, startsWith("HTTP/1.1 200 OK")); assertThat(response, containsString("COMPLETED")); assertEquals(2, count(history, "suspend")); assertEquals(1, count(history, "resume")); assertEquals(0, count(history, "onTimeout")); assertEquals(1, count(history, "onComplete")); } @Test public void testSuspendWaitResumeSuspend() throws Exception { String response = process("suspend=1000&resume=10&suspend2=10", null); assertThat(response, startsWith("HTTP/1.1 200 OK")); assertThat(response, containsString("TIMEOUT")); assertEquals(2, count(history, "suspend")); assertEquals(1, count(history, "resume")); assertEquals(1, count(history, "onTimeout")); assertEquals(1, count(history, "onComplete")); } @Test public void testSuspendTimeoutSuspendResume() throws Exception { String response = process("suspend=10&suspend2=1000&resume2=10", null); assertThat(response, startsWith("HTTP/1.1 200 OK")); assertThat(response, containsString("RESUMED")); assertEquals(2, count(history, "suspend")); assertEquals(1, count(history, "resume")); assertEquals(1, count(history, "onTimeout")); assertEquals(1, count(history, "onComplete")); } @Test public void testSuspendTimeoutSuspendComplete() throws Exception { String response = process("suspend=10&suspend2=1000&complete2=10", null); assertThat(response, startsWith("HTTP/1.1 200 OK")); assertThat(response, containsString("COMPLETED")); assertEquals(2, count(history, "suspend")); assertEquals(0, count(history, "resume")); assertEquals(1, count(history, "onTimeout")); assertEquals(1, count(history, "onComplete")); } @Test public void testSuspendTimeoutSuspend() throws Exception { String response = process("suspend=10&suspend2=10", null); assertThat(response, startsWith("HTTP/1.1 200 OK")); assertThat(response, containsString("TIMEOUT")); assertEquals(2, count(history, "suspend")); assertEquals(0, count(history, "resume")); assertEquals(2, count(history, "onTimeout")); assertEquals(1, count(history, "onComplete")); } @Test public void testSuspendThrowResume() throws Exception { String response = process("suspend=200&resume=10&undispatch=true", null); assertThat(response, startsWith("HTTP/1.1 200 OK")); assertThat(response, containsString("RESUMED")); assertThat(history, not(hasItem("onTimeout"))); assertThat(history, hasItem("onComplete")); } @Test public void testSuspendResumeThrow() throws Exception { String response = process("suspend=200&resume=0&undispatch=true", null); assertThat(response, startsWith("HTTP/1.1 200 OK")); assertThat(response, containsString("RESUMED")); assertThat(history, not(hasItem("onTimeout"))); assertThat(history, hasItem("onComplete")); } @Test public void testSuspendThrowComplete() throws Exception { String response = process("suspend=200&complete=10&undispatch=true", null); assertThat(response, startsWith("HTTP/1.1 200 OK")); assertThat(response, containsString("COMPLETED")); assertThat(history, not(hasItem("onTimeout"))); assertThat(history, hasItem("onComplete")); } @Test public void testSuspendCompleteThrow() throws Exception { String response = process("suspend=200&complete=0&undispatch=true", null); assertThat(response, startsWith("HTTP/1.1 200 OK")); assertThat(response, containsString("COMPLETED")); assertThat(history, not(hasItem("onTimeout"))); assertThat(history, hasItem("onComplete")); } private long count(List<String> history, String value) { return history.stream() .filter(value::equals) .count(); } private String process(String query, String content) throws Exception { Server server = new Server(); server.setStopTimeout(20000); try { ServerConnector connector = new ServerConnector(server); server.addConnector(connector); if(log != null) { log.clear(); } history.clear(); StatisticsHandler stats = new StatisticsHandler(); server.setHandler(stats); stats.setHandler(this.setupHandler); server.start(); int port=connector.getLocalPort(); StringBuilder request = new StringBuilder("GET /"); if (query != null) request.append("?").append(query); request.append(" HTTP/1.1\r\n") .append("Host: localhost\r\n") .append("Connection: close\r\n"); if (content == null) { request.append("\r\n"); } else { request.append("Content-Length: ").append(content.length()).append("\r\n"); request.append("\r\n").append(content); } try (Socket socket = new Socket("localhost", port)) { socket.setSoTimeout(10000); socket.getOutputStream().write(request.toString().getBytes(StandardCharsets.UTF_8)); socket.getOutputStream().flush(); return toString(socket.getInputStream()); } } finally { if (log != null) { for (int i=0;log.isEmpty()&&i<60;i++) { Thread.sleep(100); } assertThat("Log.size", log.size(),is(1)); String entry = log.get(0); assertThat("Log entry", entry, startsWith("200 ")); assertThat("Log entry", entry, endsWith(" /")); } server.stop(); } } protected String toString(InputStream in) throws IOException { return IO.toString(in); } @SuppressWarnings("serial") private static class SuspendServlet extends HttpServlet { private final Timer _timer = new Timer(); private final List<String> history; private final ContinuationListener listener; public SuspendServlet(List<String> history, ContinuationListener listener) { this.history = history; this.listener = listener; } @Override protected void doGet(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException { final Continuation continuation = ContinuationSupport.getContinuation(request); history.add(continuation.getClass().getName()); int read_before = 0; long sleep_for = -1; long suspend_for = -1; long suspend2_for = -1; long resume_after = -1; long resume2_after = -1; long complete_after = -1; long complete2_after = -1; boolean undispatch = false; if (request.getParameter("read") != null) read_before = Integer.parseInt(request.getParameter("read")); if (request.getParameter("sleep") != null) sleep_for = Integer.parseInt(request.getParameter("sleep")); if (request.getParameter("suspend") != null) suspend_for = Integer.parseInt(request.getParameter("suspend")); if (request.getParameter("suspend2") != null) suspend2_for = Integer.parseInt(request.getParameter("suspend2")); if (request.getParameter("resume") != null) resume_after = Integer.parseInt(request.getParameter("resume")); if (request.getParameter("resume2") != null) resume2_after = Integer.parseInt(request.getParameter("resume2")); if (request.getParameter("complete") != null) complete_after = Integer.parseInt(request.getParameter("complete")); if (request.getParameter("complete2") != null) complete2_after = Integer.parseInt(request.getParameter("complete2")); if (request.getParameter("undispatch") != null) undispatch = Boolean.parseBoolean(request.getParameter("undispatch")); if (continuation.isInitial()) { history.add("initial"); if (read_before > 0) { byte[] buf = new byte[read_before]; request.getInputStream().read(buf); } else if (read_before < 0) { InputStream in = request.getInputStream(); int b = in.read(); while (b != -1) b = in.read(); } if (suspend_for >= 0) { if (suspend_for > 0) continuation.setTimeout(suspend_for); continuation.addContinuationListener(listener); history.add("suspend"); continuation.suspend(response); if (complete_after > 0) { TimerTask complete = new TimerTask() { @Override public void run() { try { response.setStatus(200); response.getOutputStream().println("COMPLETED"); continuation.complete(); } catch (Exception e) { e.printStackTrace(); } } }; _timer.schedule(complete, complete_after); } else if (complete_after == 0) { response.setStatus(200); response.getOutputStream().println("COMPLETED"); continuation.complete(); } else if (resume_after > 0) { TimerTask resume = new TimerTask() { @Override public void run() { history.add("resume"); continuation.resume(); } }; _timer.schedule(resume, resume_after); } else if (resume_after == 0) { history.add("resume"); continuation.resume(); } if (undispatch) { continuation.undispatch(); } } else if (sleep_for >= 0) { try { Thread.sleep(sleep_for); } catch (InterruptedException e) { e.printStackTrace(); } response.setStatus(200); response.getOutputStream().println("SLEPT"); } else { response.setStatus(200); response.getOutputStream().println("NORMAL"); } } else { history.add("!initial"); if (suspend2_for >= 0 && request.getAttribute("2nd") == null) { request.setAttribute("2nd", "cycle"); if (suspend2_for > 0) continuation.setTimeout(suspend2_for); history.add("suspend"); continuation.suspend(response); if (complete2_after > 0) { TimerTask complete = new TimerTask() { @Override public void run() { try { response.setStatus(200); response.getOutputStream().println("COMPLETED"); continuation.complete(); } catch (Exception e) { e.printStackTrace(); } } }; _timer.schedule(complete, complete2_after); } else if (complete2_after == 0) { response.setStatus(200); response.getOutputStream().println("COMPLETED"); continuation.complete(); } else if (resume2_after > 0) { TimerTask resume = new TimerTask() { @Override public void run() { history.add("resume"); continuation.resume(); } }; _timer.schedule(resume, resume2_after); } else if (resume2_after == 0) { history.add("resume"); continuation.resume(); } if (undispatch) { continuation.undispatch(); } } else if (continuation.isExpired()) { response.setStatus(200); response.getOutputStream().println("TIMEOUT"); } else if (continuation.isResumed()) { response.setStatus(200); response.getOutputStream().println("RESUMED"); } else { response.setStatus(200); response.getOutputStream().println("UNKNOWN"); } } } } private static class Listener implements ContinuationListener { private final List<String> history; public Listener(List<String> history) { this.history = history; } @Override public void onComplete(Continuation continuation) { history.add("onComplete"); } @Override public void onTimeout(Continuation continuation) { history.add("onTimeout"); } } public static class Log extends AbstractLifeCycle implements RequestLog { private final List<String> log; public Log(List<String> log) { this.log = log; } @Override public void log(Request request, Response response) { int status = response.getCommittedMetaData().getStatus(); long written = response.getHttpChannel().getBytesWritten(); log.add(status+" "+written+" "+request.getRequestURI()); } } }