Initial Contribution

Signed-off-by: Jan Supol <jan.supol@oracle.com>
diff --git a/tests/integration/servlet-3-async/pom.xml b/tests/integration/servlet-3-async/pom.xml
new file mode 100644
index 0000000..a959490
--- /dev/null
+++ b/tests/integration/servlet-3-async/pom.xml
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    Copyright (c) 2011, 2018 Oracle and/or its affiliates. All rights reserved.
+
+    This program and the accompanying materials are made available under the
+    terms of the Eclipse Public License v. 2.0, which is available at
+    http://www.eclipse.org/legal/epl-2.0.
+
+    This Source Code may also be made available under the following Secondary
+    Licenses when the conditions for such availability set forth in the
+    Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+    version 2 with the GNU Classpath Exception, which is available at
+    https://www.gnu.org/software/classpath/license.html.
+
+    SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+
+-->
+
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.glassfish.jersey.tests.integration</groupId>
+        <artifactId>project</artifactId>
+        <version>2.28-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>servlet-3-async</artifactId>
+    <packaging>war</packaging>
+    <name>jersey-tests-integration-servlet-3-async</name>
+
+    <description>Servlet integration test - servlet-3-async - configured via jax-rs application annotated with @javax.ws.rs.ApplicationPath</description>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.glassfish.jersey.containers</groupId>
+            <artifactId>jersey-container-servlet</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.glassfish.jersey.test-framework.providers</groupId>
+            <artifactId>jersey-test-framework-provider-external</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-failsafe-plugin</artifactId>
+            </plugin>
+            <plugin>
+              <groupId>org.mortbay.jetty</groupId>
+              <artifactId>jetty-maven-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
+</project>
diff --git a/tests/integration/servlet-3-async/src/main/java/org/glassfish/jersey/tests/integration/servlet_3_async/AsyncServletResource.java b/tests/integration/servlet-3-async/src/main/java/org/glassfish/jersey/tests/integration/servlet_3_async/AsyncServletResource.java
new file mode 100644
index 0000000..2f96055
--- /dev/null
+++ b/tests/integration/servlet-3-async/src/main/java/org/glassfish/jersey/tests/integration/servlet_3_async/AsyncServletResource.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.tests.integration.servlet_3_async;
+
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.Executors;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+
+import javax.ws.rs.container.AsyncResponse;
+import javax.ws.rs.container.Suspended;
+
+/**
+ * Asynchronous servlet-deployed resource.
+ *
+ * @author Marek Potociar (marek.potociar at oracle.com)
+ */
+@Path("async")
+public class AsyncServletResource {
+    /**
+     * Hello world message.
+     */
+    public static final String HELLO_ASYNC_WORLD = "Hello Async World!";
+    public static final String CANCELED = "Canceled";
+
+    private static BlockingQueue<CanceledRequest> cancelingQueue = new ArrayBlockingQueue<CanceledRequest>(5);
+
+    private static class CanceledRequest {
+        private final String id;
+        private final AsyncResponse asyncResponse;
+
+        private CanceledRequest(String id, AsyncResponse asyncResponse) {
+            this.id = id;
+            this.asyncResponse = asyncResponse;
+        }
+    }
+
+    /**
+     * Get the async "Hello World" message.
+     */
+    @GET
+    @Produces("text/plain")
+    public void get(@Suspended final AsyncResponse ar) {
+        Executors.newSingleThreadExecutor().execute(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    Thread.sleep(100);
+                    ar.resume(HELLO_ASYNC_WORLD);
+                } catch (InterruptedException e) {
+                    e.printStackTrace();
+                }
+            }
+        });
+    }
+
+    /**
+     * Get a canceled request.
+     *
+     * @param id request id.
+     * @throws InterruptedException in case of not being able to put the request
+     *                              to an internal queue for canceling.
+     */
+    @GET
+    @Path("canceled")
+    public void getCanceled(@Suspended final AsyncResponse ar, @QueryParam("id") final String id) throws InterruptedException {
+        cancelingQueue.put(new CanceledRequest(id, ar));
+    }
+
+    /**
+     * Cancel a request that is on top of the canceling queue.
+     *
+     * @return notification message about successful request canceling.
+     * @throws InterruptedException in case of not being able to take a cancelled request
+     *                              from an internal canceling queue.
+     */
+    @POST
+    @Produces("text/plain")
+    @Path("canceled")
+    public String cancel(String requestId) throws InterruptedException {
+        final CanceledRequest canceledRequest = cancelingQueue.take();
+        canceledRequest.asyncResponse.cancel();
+
+        return CANCELED + " " + canceledRequest.id + " by POST " + requestId;
+    }
+
+}
diff --git a/tests/integration/servlet-3-async/src/main/java/org/glassfish/jersey/tests/integration/servlet_3_async/AsyncTimeoutResource.java b/tests/integration/servlet-3-async/src/main/java/org/glassfish/jersey/tests/integration/servlet_3_async/AsyncTimeoutResource.java
new file mode 100644
index 0000000..23c000b
--- /dev/null
+++ b/tests/integration/servlet-3-async/src/main/java/org/glassfish/jersey/tests/integration/servlet_3_async/AsyncTimeoutResource.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.tests.integration.servlet_3_async;
+
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.TimeUnit;
+
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.container.AsyncResponse;
+import javax.ws.rs.container.Suspended;
+import javax.ws.rs.container.TimeoutHandler;
+
+/**
+ * Asynchronous servlet-deployed resource for testing {@link javax.ws.rs.container.AsyncResponse async response} timeouts.
+ *
+ * @author Michal Gajdos
+ */
+@Path("timeout")
+public class AsyncTimeoutResource {
+
+    private static final BlockingQueue<AsyncResponse> queue = new ArrayBlockingQueue<AsyncResponse>(1);
+
+    @GET
+    @Path("suspend")
+    public void suspend(@Suspended final AsyncResponse asyncResponse) {
+        queue.add(asyncResponse);
+    }
+
+    @POST
+    @Path("timeout")
+    public void setTimeOut(final Integer millis) throws InterruptedException {
+        final AsyncResponse asyncResponse = queue.take();
+
+        final boolean timeout1 = asyncResponse.setTimeout(millis, TimeUnit.MILLISECONDS);
+        asyncResponse.setTimeoutHandler(new TimeoutHandler() {
+
+            @Override
+            public void handleTimeout(final AsyncResponse asyncResponse) {
+                final boolean timeout2 = asyncResponse.setTimeout(millis, TimeUnit.MILLISECONDS);
+                asyncResponse.setTimeoutHandler(new TimeoutHandler() {
+
+                    @Override
+                    public void handleTimeout(final AsyncResponse asyncResponse) {
+                        asyncResponse.resume("timeout1=" + timeout1 + "_timeout2=" + timeout2 + "_handled");
+                        asyncResponse.cancel();
+                    }
+                });
+            }
+        });
+    }
+}
diff --git a/tests/integration/servlet-3-async/src/main/java/org/glassfish/jersey/tests/integration/servlet_3_async/Servlet3Async.java b/tests/integration/servlet-3-async/src/main/java/org/glassfish/jersey/tests/integration/servlet_3_async/Servlet3Async.java
new file mode 100644
index 0000000..5787a1d
--- /dev/null
+++ b/tests/integration/servlet-3-async/src/main/java/org/glassfish/jersey/tests/integration/servlet_3_async/Servlet3Async.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.tests.integration.servlet_3_async;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.ws.rs.ApplicationPath;
+import javax.ws.rs.core.Application;
+
+/**
+ * Asynchronous servlet-deployed resource application.
+ *
+ * @author Marek Potociar (marek.potociar at oracle.com)
+ */
+@ApplicationPath("/")
+public class Servlet3Async extends Application {
+
+    @Override
+    public Set<Class<?>> getClasses() {
+        final Set<Class<?>> hashSet = new HashSet<Class<?>>();
+        hashSet.add(AsyncServletResource.class);
+        hashSet.add(AsyncTimeoutResource.class);
+        return hashSet;
+    }
+}
diff --git a/tests/integration/servlet-3-async/src/test/java/org/glassfish/jersey/tests/integration/servlet_3_async/AsyncServletResourceITCase.java b/tests/integration/servlet-3-async/src/test/java/org/glassfish/jersey/tests/integration/servlet_3_async/AsyncServletResourceITCase.java
new file mode 100644
index 0000000..6cdfb9a
--- /dev/null
+++ b/tests/integration/servlet-3-async/src/test/java/org/glassfish/jersey/tests/integration/servlet_3_async/AsyncServletResourceITCase.java
@@ -0,0 +1,294 @@
+/*
+ * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.tests.integration.servlet_3_async;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Logger;
+
+import javax.ws.rs.ProcessingException;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.Response;
+
+import org.glassfish.jersey.internal.guava.ThreadFactoryBuilder;
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.process.JerseyProcessingUncaughtExceptionHandler;
+import org.glassfish.jersey.test.JerseyTest;
+import org.glassfish.jersey.test.external.ExternalTestContainerFactory;
+import org.glassfish.jersey.test.spi.TestContainerException;
+import org.glassfish.jersey.test.spi.TestContainerFactory;
+
+import org.junit.Test;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Asynchronous servlet-deployed resource test.
+ *
+ * @author Marek Potociar (marek.potociar at oracle.com)
+ */
+public class AsyncServletResourceITCase extends JerseyTest {
+    private static final Logger LOGGER = Logger.getLogger(AsyncServletResourceITCase.class.getName());
+
+    private static class ResponseRecord {
+        final int status;
+        final String message;
+
+        private ResponseRecord(int status, String message) {
+            this.status = status;
+            this.message = message;
+        }
+
+        @Override
+        public String toString() {
+            return status + " : \"" + message + '\"';
+        }
+    }
+
+    @Override
+    protected Application configure() {
+        return new Application();
+    }
+
+    @Override
+    protected TestContainerFactory getTestContainerFactory() throws TestContainerException {
+        return new ExternalTestContainerFactory();
+    }
+
+    /**
+     * Test asynchronous servlet-deployed resource.
+     *
+     * @throws InterruptedException in case the waiting for all requests to complete was interrupted.
+     */
+    @Test
+    public void testAsyncServlet() throws InterruptedException {
+        final WebTarget resourceTarget = target("async");
+        resourceTarget.register(LoggingFeature.class);
+        final String expectedResponse = AsyncServletResource.HELLO_ASYNC_WORLD;
+
+        final int MAX_MESSAGES = 50;
+        final int LATCH_WAIT_TIMEOUT = 10 * getAsyncTimeoutMultiplier();
+        final boolean debugMode = false;
+        final boolean sequentialGet = false;
+        final Object sequentialGetLock = new Object();
+
+        final ExecutorService executor = Executors.newCachedThreadPool(new ThreadFactoryBuilder()
+                .setNameFormat("async-resource-test-%d")
+                .setUncaughtExceptionHandler(new JerseyProcessingUncaughtExceptionHandler())
+                .build());
+
+        final Map<Integer, ResponseRecord> getResponses = new ConcurrentHashMap<Integer, ResponseRecord>();
+
+        final CountDownLatch getRequestLatch = new CountDownLatch(MAX_MESSAGES);
+
+        try {
+            for (int i = 0; i < MAX_MESSAGES; i++) {
+                final int requestId = i;
+                executor.submit(new Runnable() {
+
+                    @Override
+                    public void run() {
+                        //noinspection PointlessBooleanExpression,ConstantConditions
+                        if (debugMode || sequentialGet) {
+                            synchronized (sequentialGetLock) {
+                                get();
+                            }
+                        } else {
+                            get();
+                        }
+                    }
+
+                    private void get() {
+                        try {
+                            final Response response = resourceTarget.request().get();
+                            getResponses.put(
+                                    requestId,
+                                    new ResponseRecord(response.getStatus(), response.readEntity(String.class)));
+                        } catch (Throwable t) {
+                            t.printStackTrace();
+                        } finally {
+                            getRequestLatch.countDown();
+                        }
+                    }
+                });
+            }
+
+            //noinspection ConstantConditions
+            if (debugMode) {
+                getRequestLatch.await();
+            } else {
+                assertTrue("Waiting for all GET requests to complete has timed out.", getRequestLatch.await(LATCH_WAIT_TIMEOUT,
+                        TimeUnit.SECONDS));
+            }
+        } finally {
+            executor.shutdownNow();
+        }
+
+        StringBuilder messageBuilder = new StringBuilder();
+        for (Map.Entry<Integer, ResponseRecord> getResponseEntry : getResponses.entrySet()) {
+            messageBuilder.append("GET response for message ")
+                    .append(getResponseEntry.getKey()).append(": ")
+                    .append(getResponseEntry.getValue().toString()).append('\n');
+        }
+        LOGGER.info(messageBuilder.toString());
+
+        assertEquals(MAX_MESSAGES, getResponses.size());
+        for (Map.Entry<Integer, ResponseRecord> entry : getResponses.entrySet()) {
+            assertEquals(
+                    "Unexpected GET response status for request " + entry.getKey(),
+                    200, entry.getValue().status);
+            assertEquals(
+                    "Unexpected GET response message for request " + entry.getKey(),
+                    expectedResponse, entry.getValue().message);
+        }
+    }
+
+    /**
+     * Test canceling of an async request to a servlet-deployed resource.
+     *
+     * @throws InterruptedException in case the waiting for all requests to complete was interrupted.
+     */
+    @Test
+    public void testAsyncRequestCanceling() throws InterruptedException {
+        final WebTarget resourceTarget = target("async/canceled");
+        resourceTarget.register(LoggingFeature.class);
+
+        final int MAX_MESSAGES = 10;
+        final int LATCH_WAIT_TIMEOUT = 10 * getAsyncTimeoutMultiplier();
+        final boolean debugMode = false;
+        final boolean sequentialGet = false;
+        final boolean sequentialPost = false;
+        final Object sequentialGetLock = new Object();
+        final Object sequentialPostLock = new Object();
+
+        final ExecutorService executor = Executors.newCachedThreadPool(new ThreadFactoryBuilder()
+                .setNameFormat("async-canceled-resource-test-%d")
+                .setUncaughtExceptionHandler(new JerseyProcessingUncaughtExceptionHandler())
+                .build());
+
+        final Map<Integer, String> postResponses = new ConcurrentHashMap<Integer, String>();
+        final Map<Integer, String> getResponses = new ConcurrentHashMap<Integer, String>();
+
+        final CountDownLatch postRequestLatch = new CountDownLatch(MAX_MESSAGES);
+        final CountDownLatch getRequestLatch = new CountDownLatch(MAX_MESSAGES);
+
+        try {
+            for (int i = 0; i < MAX_MESSAGES; i++) {
+                final int requestId = i;
+                executor.submit(new Runnable() {
+
+                    @Override
+                    public void run() {
+                        //noinspection PointlessBooleanExpression,ConstantConditions
+                        if (debugMode || sequentialGet) {
+                            synchronized (sequentialGetLock) {
+                                get();
+                            }
+                        } else {
+                            get();
+                        }
+                    }
+
+                    private void get() {
+                        try {
+                            final String response = resourceTarget.queryParam("id", requestId).request().get(String.class);
+                            getResponses.put(requestId, response);
+                        } catch (WebApplicationException ex) {
+                            final Response response = ex.getResponse();
+                            getResponses.put(requestId, response.getStatus() + ": " + response.readEntity(String.class));
+                        } finally {
+                            getRequestLatch.countDown();
+                        }
+                    }
+                });
+                executor.submit(new Runnable() {
+
+                    @Override
+                    public void run() {
+                        //noinspection PointlessBooleanExpression,ConstantConditions
+                        if (debugMode || sequentialPost) {
+                            synchronized (sequentialPostLock) {
+                                post();
+                            }
+                        } else {
+                            post();
+                        }
+                    }
+
+                    private void post() throws ProcessingException {
+                        try {
+                            final String response = resourceTarget.request().post(Entity.text("" + requestId), String.class);
+                            postResponses.put(requestId, response);
+                        } finally {
+                            postRequestLatch.countDown();
+                        }
+                    }
+                });
+            }
+
+            //noinspection ConstantConditions
+            if (debugMode) {
+                postRequestLatch.await();
+                getRequestLatch.await();
+            } else {
+                assertTrue("Waiting for all POST requests to complete has timed out.",
+                        postRequestLatch.await(LATCH_WAIT_TIMEOUT, TimeUnit.SECONDS));
+                assertTrue("Waiting for all GET requests to complete has timed out.", getRequestLatch.await(LATCH_WAIT_TIMEOUT,
+                        TimeUnit.SECONDS));
+            }
+        } finally {
+            executor.shutdownNow();
+        }
+
+        StringBuilder messageBuilder = new StringBuilder();
+        for (Map.Entry<Integer, String> postResponseEntry : postResponses.entrySet()) {
+            messageBuilder.append("POST response for message ")
+                    .append(postResponseEntry.getKey()).append(": ")
+                    .append(postResponseEntry.getValue()).append('\n');
+        }
+        messageBuilder.append('\n');
+        for (Map.Entry<Integer, String> getResponseEntry : getResponses.entrySet()) {
+            messageBuilder.append("GET response for message ")
+                    .append(getResponseEntry.getKey()).append(": ")
+                    .append(getResponseEntry.getValue()).append('\n');
+        }
+        LOGGER.info(messageBuilder.toString());
+
+        assertEquals(MAX_MESSAGES, postResponses.size());
+        for (Map.Entry<Integer, String> postResponseEntry : postResponses.entrySet()) {
+            assertTrue("Unexpected POST notification response for message " + postResponseEntry.getKey(),
+                    postResponseEntry.getValue().startsWith(AsyncServletResource.CANCELED));
+        }
+
+        assertEquals(MAX_MESSAGES, getResponses.size());
+        final Collection<Integer> getResponseKeys = getResponses.keySet();
+        for (int i = 0; i < MAX_MESSAGES; i++) {
+            assertTrue("Detected a GET message response loss: " + i, getResponseKeys.contains(i));
+            final String getResponseEntry = getResponses.get(i);
+            assertTrue("Unexpected canceled GET response status for request " + i,
+                    getResponseEntry.startsWith("503: "));
+        }
+    }
+}
diff --git a/tests/integration/servlet-3-async/src/test/java/org/glassfish/jersey/tests/integration/servlet_3_async/AsyncTimeoutResourceITCase.java b/tests/integration/servlet-3-async/src/test/java/org/glassfish/jersey/tests/integration/servlet_3_async/AsyncTimeoutResourceITCase.java
new file mode 100644
index 0000000..8bfb646
--- /dev/null
+++ b/tests/integration/servlet-3-async/src/test/java/org/glassfish/jersey/tests/integration/servlet_3_async/AsyncTimeoutResourceITCase.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.tests.integration.servlet_3_async;
+
+import java.util.concurrent.Future;
+
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.Response;
+
+import org.glassfish.jersey.logging.LoggingFeature;
+import org.glassfish.jersey.test.JerseyTest;
+import org.glassfish.jersey.test.external.ExternalTestContainerFactory;
+import org.glassfish.jersey.test.spi.TestContainerException;
+import org.glassfish.jersey.test.spi.TestContainerFactory;
+
+import org.junit.Test;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.junit.Assert.assertThat;
+
+/**
+ * Asynchronous servlet-deployed resource for testing {@link javax.ws.rs.container.AsyncResponse async response} timeouts.
+ *
+ * @author Michal Gajdos
+ */
+public class AsyncTimeoutResourceITCase extends JerseyTest {
+
+    @Override
+    protected Application configure() {
+        return new Application();
+    }
+
+    @Override
+    protected TestContainerFactory getTestContainerFactory() throws TestContainerException {
+        return new ExternalTestContainerFactory();
+    }
+
+    @Test
+    public void testTimeout() throws Exception {
+        final WebTarget resourceTarget = target("timeout");
+        resourceTarget.register(LoggingFeature.class);
+        final Future<Response> responseFuture = resourceTarget.path("suspend").request().async().get();
+
+        // Set timeout.
+        assertThat(resourceTarget.path("timeout").request().post(Entity.text("500")).getStatus(), equalTo(204));
+
+        // Wait for it.
+        assertThat(responseFuture.get().readEntity(String.class), equalTo("timeout1=true_timeout2=true_handled"));
+    }
+}