You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@struts.apache.org by "ASF GitHub Bot (JIRA)" <ji...@apache.org> on 2018/03/20 14:00:01 UTC

[jira] [Commented] (WW-4874) Asynchronous action method

    [ https://issues.apache.org/jira/browse/WW-4874?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=16406363#comment-16406363 ] 

ASF GitHub Bot commented on WW-4874:
------------------------------------

lukaszlenart closed pull request #179: WW-4874 Introduces Async plugin (adds support for async methods)
URL: https://github.com/apache/struts/pull/179
 
 
   

This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:

As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):

diff --git a/apps/showcase/pom.xml b/apps/showcase/pom.xml
index 67921e43b..6b8d6c1f0 100644
--- a/apps/showcase/pom.xml
+++ b/apps/showcase/pom.xml
@@ -89,6 +89,11 @@
             <artifactId>struts2-bean-validation-plugin</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>org.apache.struts</groupId>
+            <artifactId>struts2-async-plugin</artifactId>
+        </dependency>
+
         <dependency>
             <groupId>javax.servlet</groupId>
             <artifactId>servlet-api</artifactId>
diff --git a/apps/showcase/src/main/java/org/apache/struts2/showcase/async/AsyncFilter.java b/apps/showcase/src/main/java/org/apache/struts2/showcase/async/AsyncFilter.java
new file mode 100644
index 000000000..95d98ca53
--- /dev/null
+++ b/apps/showcase/src/main/java/org/apache/struts2/showcase/async/AsyncFilter.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.struts2.showcase.async;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import java.io.IOException;
+
+/**
+ * Filters async actions directly to Struts servlet
+ */
+public class AsyncFilter implements Filter {
+    @Override
+    public void init(FilterConfig filterConfig) throws ServletException {
+
+    }
+
+    @Override
+    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
+        String requestURI = ((HttpServletRequest) servletRequest).getRequestURI();
+        if (!requestURI.contains("/async/receiveNewMessages")) {
+            filterChain.doFilter(servletRequest, servletResponse); // Just continue chain.
+        } else {
+            servletRequest.getRequestDispatcher("/async/receiveNewMessages").forward(servletRequest, servletResponse);
+        }
+    }
+
+    @Override
+    public void destroy() {
+
+    }
+}
diff --git a/apps/showcase/src/main/java/org/apache/struts2/showcase/async/ChatRoomAction.java b/apps/showcase/src/main/java/org/apache/struts2/showcase/async/ChatRoomAction.java
new file mode 100644
index 000000000..5877fe6e1
--- /dev/null
+++ b/apps/showcase/src/main/java/org/apache/struts2/showcase/async/ChatRoomAction.java
@@ -0,0 +1,68 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.struts2.showcase.async;
+
+import com.opensymphony.xwork2.ActionSupport;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Callable;
+
+/**
+ * Example to illustrate the <code>async</code> plugin.
+ */
+public class ChatRoomAction extends ActionSupport {
+    private String message;
+    private Integer lastIndex;
+    private List<String> newMessages;
+
+    private static final List<String> messages = new ArrayList<>();
+
+    public void setMessage(String message) {
+        this.message = message;
+    }
+
+    public void setLastIndex(Integer lastIndex) {
+        this.lastIndex = lastIndex;
+    }
+
+    public List<String> getNewMessages() {
+        return newMessages;
+    }
+
+    public Callable<String> receiveNewMessages() throws Exception {
+        return new Callable<String>() {
+            @Override
+            public String call() throws Exception {
+                while (lastIndex >= messages.size()) {
+                    Thread.sleep(3000);
+                }
+                newMessages = messages.subList(lastIndex, messages.size());
+                return SUCCESS;
+            }
+        };
+    }
+
+    public String sendMessage() {
+        synchronized (messages) {
+            messages.add(message);
+        }
+        return SUCCESS;
+    }
+}
diff --git a/apps/showcase/src/main/resources/struts-async.xml b/apps/showcase/src/main/resources/struts-async.xml
new file mode 100644
index 000000000..faa38656c
--- /dev/null
+++ b/apps/showcase/src/main/resources/struts-async.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+-->
+<!DOCTYPE struts PUBLIC
+	"-//Apache Software Foundation//DTD Struts Configuration 2.5//EN"
+	"http://struts.apache.org/dtds/struts-2.5.dtd">
+
+<struts>
+    <package name="async" extends="json-default" namespace="/async">
+
+        <action name="receiveNewMessages" class="org.apache.struts2.showcase.async.ChatRoomAction" method="receiveNewMessages">
+            <result name="success" type="json">
+                <param name="root">newMessages</param>
+            </result>
+            <result name="timeout" type="json">
+                <param name="root">newMessages</param>
+            </result>
+        </action>
+
+        <action name="sendMessage" class="org.apache.struts2.showcase.async.ChatRoomAction" method="sendMessage">
+            <result name="success" type="json">
+                <param name="root">newMessages</param>
+            </result>
+            <result name="timeout" type="json">
+                <param name="root">newMessages</param>
+            </result>
+        </action>
+
+    </package>
+
+</struts>
diff --git a/apps/showcase/src/main/resources/struts.xml b/apps/showcase/src/main/resources/struts.xml
index 46611f9cd..ee7dbcee3 100644
--- a/apps/showcase/src/main/resources/struts.xml
+++ b/apps/showcase/src/main/resources/struts.xml
@@ -74,6 +74,8 @@
 
     <include file="struts-xslt.xml" />
 
+    <include file="struts-async.xml" />
+
     <package name="default" extends="struts-default">
         <interceptors>
             <interceptor-stack name="crudStack">
diff --git a/apps/showcase/src/main/webapp/WEB-INF/decorators/main.jsp b/apps/showcase/src/main/webapp/WEB-INF/decorators/main.jsp
index 727b28c5b..bcbe36113 100644
--- a/apps/showcase/src/main/webapp/WEB-INF/decorators/main.jsp
+++ b/apps/showcase/src/main/webapp/WEB-INF/decorators/main.jsp
@@ -235,6 +235,7 @@
                             <li><s:a value="/token/index.html">Token</s:a></li>
                             <li><s:url var="url" namespace="/modelDriven" action="modelDriven"/><s:a
                                     href="%{url}">Model Driven</s:a></li>
+                            <li><s:a value="/async/index.html">Async</s:a></li>
                         </ul>
                     </li>
                     <li class="dropdown">
diff --git a/apps/showcase/src/main/webapp/WEB-INF/web.xml b/apps/showcase/src/main/webapp/WEB-INF/web.xml
index 204ed6f7d..d69061e9c 100644
--- a/apps/showcase/src/main/webapp/WEB-INF/web.xml
+++ b/apps/showcase/src/main/webapp/WEB-INF/web.xml
@@ -23,7 +23,13 @@
 	xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
 
     <display-name>Struts Showcase Application</display-name>
-	
+
+    <filter>
+        <filter-name>async</filter-name>
+        <filter-class>org.apache.struts2.showcase.async.AsyncFilter</filter-class>
+        <async-supported>true</async-supported>
+    </filter>
+
     <filter>
         <filter-name>struts-prepare</filter-name>
         <filter-class>org.apache.struts2.dispatcher.filter.StrutsPrepareFilter</filter-class>
@@ -40,6 +46,11 @@
        <filter-class>com.opensymphony.sitemesh.webapp.SiteMeshFilter</filter-class>
    </filter>
 
+    <filter-mapping>
+        <filter-name>async</filter-name>
+        <url-pattern>/async/*</url-pattern>
+    </filter-mapping>
+
     <filter-mapping>
         <filter-name>struts-prepare</filter-name>
         <url-pattern>/*</url-pattern>
@@ -113,6 +124,13 @@
         <load-on-startup>1</load-on-startup>
     </servlet>
 
+    <servlet>
+        <servlet-name>strutsServlet</servlet-name>
+        <servlet-class>org.apache.struts2.dispatcher.servlet.StrutsServlet</servlet-class>
+        <load-on-startup>1</load-on-startup>
+        <async-supported>true</async-supported>
+    </servlet>
+
     <servlet-mapping>
         <servlet-name>dwr</servlet-name>
         <url-pattern>/dwr/*</url-pattern>
@@ -128,6 +146,11 @@
         <url-pattern>*.vm</url-pattern>
     </servlet-mapping>
 
+    <servlet-mapping>
+        <servlet-name>strutsServlet</servlet-name>
+        <url-pattern>/async/receiveNewMessages</url-pattern>
+    </servlet-mapping>
+
     <!-- END SNIPPET: dwr -->
 
     <!-- SNIPPET START: example.velocity.filter.chain
diff --git a/apps/showcase/src/main/webapp/async/index.html b/apps/showcase/src/main/webapp/async/index.html
new file mode 100644
index 000000000..3726eb7f8
--- /dev/null
+++ b/apps/showcase/src/main/webapp/async/index.html
@@ -0,0 +1,119 @@
+<!--
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+-->
+<html>
+<head>
+	<title>Struts2 Showcase - Async Example</title>
+	<script>
+        function User () {
+            this.lastIndex = 0;
+            this.sendMessage = function(message) {
+                $.ajax({
+                    url: "sendMessage",
+                    data: {
+                        message: message
+                    }
+                });
+            };
+            this.receiveNewMessages = function() {
+                $.ajax({
+                    url: "receiveNewMessages",
+                    context: this,
+                    data: {
+                        lastIndex: this.lastIndex
+                    },
+                    success: function (result) {
+                        if (result != null) {//result is null on timeout
+                            var msgs = $('#msgs');
+                            var messages = msgs.val();
+                            msgs.val(messages + result + '\n');
+                            this.lastIndex += result.length;
+                        }
+                        this.receiveNewMessages();
+                    }
+                });
+            };
+            this.receiveNewMessages();
+        }
+
+        var user;
+        
+        function sendMessage() {
+            var msg = $('#msg');
+            var message = msg.val();
+            if (message.length > 0) {
+                user.sendMessage(message);
+                msg.val('');
+            }
+            return false;
+        }
+
+        $(function() {
+            user = new User();
+        });
+
+	</script>
+</head>
+
+<body>
+<div class="page-header">
+	<h1>Async Example</h1>
+</div>
+
+
+
+<div class="container-fluid">
+	<div class="row">
+		<div class="col-md-12" style="text-align: center;">
+
+			<p>
+				These examples illustrate Struts build in support for async request processing.
+			</p>
+			<p>
+				When you have a process that takes a long time, it can make your app not scalable under heavy load conditions.
+				Scalability limitations include running out of memory or exhausting the pool of container threads.
+				To create scalable web applications, you must ensure that no threads associated with a request
+				are sitting idle, so the container can use them to process new requests.
+				Asynchronous processing refers to assigning these blocking operations to a new thread and returning
+				the thread associated with the request immediately to the container.
+                <br/> Reference: <a href="https://docs.oracle.com/javaee/7/tutorial/servlets012.htm">Asynchronous Processing</a>
+				<br/> An interesting and vital use case for the async request processing is server push.
+				A good solution is to use the Servlet 3.0+ asynchronous feature.
+			</p>
+
+			<br/>
+            <h2>Example: A minimal chat room using server push</h2>
+            <h3>Open current page in different tabs, browsers and computers then send messages.</h3>
+            <h4>This is a minimal chat room which uses server push to retrieve new messages.
+            It doesn't poll the server frequently to check if a new message is available to display.
+            Instead it waits for the server to push back new messages. This approach has two obvious advantages:
+            low-lag communication without requests being sent, and no waste of server resources and network bandwidth.</h4>
+            <h5>Reference: <a href="https://www.javaworld.com/article/2077995/java-concurrency/java-concurrency-asynchronous-processing-support-in-servlet-3-0.html">
+                Asynchronous processing support in Servlet 3.0</a></h5>
+            <textarea id="msgs" cols="40" rows="5" title="messages" readonly></textarea><br/>
+			<form>
+				<input name="msg" id="msg" type="text" title="message" required />
+				<input type="submit" value="Send" onclick="return sendMessage();" />
+			</form>
+		</div>
+	</div>
+</div>
+</body>
+</html>
diff --git a/apps/showcase/src/test/java/it/org/apache/struts2/showcase/AsyncTest.java b/apps/showcase/src/test/java/it/org/apache/struts2/showcase/AsyncTest.java
new file mode 100644
index 000000000..dc93ba5cf
--- /dev/null
+++ b/apps/showcase/src/test/java/it/org/apache/struts2/showcase/AsyncTest.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package it.org.apache.struts2.showcase;
+
+public class AsyncTest extends ITBaseTest {
+    public void testChatRoom() throws InterruptedException {
+        beginAt("/async/index.html");
+
+        setTextField("msg", "hello");
+        submit();
+        Thread.sleep(4000);
+        assertTextInElement("msgs", "hello");
+    }
+}
diff --git a/core/src/main/java/com/opensymphony/xwork2/AsyncManager.java b/core/src/main/java/com/opensymphony/xwork2/AsyncManager.java
new file mode 100644
index 000000000..5bada9c77
--- /dev/null
+++ b/core/src/main/java/com/opensymphony/xwork2/AsyncManager.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package com.opensymphony.xwork2;
+
+import java.util.concurrent.Callable;
+
+/**
+ * Adds support for invoke async actions. This allows us to support action methods that return {@link Callable}
+ * as well as invoking them in separate not-container thread then executing the result in another container thread.
+ *
+ * @since 2.6
+ */
+public interface AsyncManager {
+    boolean hasAsyncActionResult();
+
+    Object getAsyncActionResult();
+
+    void invokeAsyncAction(Callable asyncAction);
+}
diff --git a/core/src/main/java/com/opensymphony/xwork2/DefaultActionInvocation.java b/core/src/main/java/com/opensymphony/xwork2/DefaultActionInvocation.java
index 6b039d680..77acc3cdd 100644
--- a/core/src/main/java/com/opensymphony/xwork2/DefaultActionInvocation.java
+++ b/core/src/main/java/com/opensymphony/xwork2/DefaultActionInvocation.java
@@ -40,6 +40,7 @@
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.Callable;
 
 /**
  * The Default ActionInvocation implementation
@@ -71,6 +72,8 @@
     protected Container container;
     protected UnknownHandlerManager unknownHandlerManager;
     protected OgnlUtil ognlUtil;
+    protected AsyncManager asyncManager;
+    protected Callable asyncAction;
     protected WithLazyParams.LazyParamInjector lazyParamInjector;
 
     public DefaultActionInvocation(final Map<String, Object> extraContext, final boolean pushAction) {
@@ -108,6 +111,11 @@ public void setOgnlUtil(OgnlUtil ognlUtil) {
         this.ognlUtil = ognlUtil;
     }
 
+    @Inject(required=false)
+    public void setAsyncManager(AsyncManager asyncManager) {
+        this.asyncManager = asyncManager;
+    }
+
     public Object getAction() {
         return action;
     }
@@ -237,49 +245,61 @@ public String invoke() throws Exception {
                 throw new IllegalStateException("Action has already executed");
             }
 
-            if (interceptors.hasNext()) {
-                final InterceptorMapping interceptorMapping = interceptors.next();
-                String interceptorMsg = "interceptorMapping: " + interceptorMapping.getName();
-                UtilTimerStack.push(interceptorMsg);
-                try {
-                    Interceptor interceptor = interceptorMapping.getInterceptor();
-                    if (interceptor instanceof WithLazyParams) {
-                        interceptor = lazyParamInjector.injectParams(interceptor, interceptorMapping.getParams(), invocationContext);
+            if (asyncManager == null || !asyncManager.hasAsyncActionResult()) {
+                if (interceptors.hasNext()) {
+                    final InterceptorMapping interceptorMapping = interceptors.next();
+                    String interceptorMsg = "interceptorMapping: " + interceptorMapping.getName();
+                    UtilTimerStack.push(interceptorMsg);
+                    try {
+                        Interceptor interceptor = interceptorMapping.getInterceptor();
+                        if (interceptor instanceof WithLazyParams) {
+                            interceptor = lazyParamInjector.injectParams(interceptor, interceptorMapping.getParams(), invocationContext);
+                        }
+                        resultCode = interceptor.intercept(DefaultActionInvocation.this);
+                    } finally {
+                        UtilTimerStack.pop(interceptorMsg);
                     }
-                    resultCode = interceptor.intercept(DefaultActionInvocation.this);
-                } finally {
-                    UtilTimerStack.pop(interceptorMsg);
+                } else {
+                    resultCode = invokeActionOnly();
                 }
             } else {
-                resultCode = invokeActionOnly();
+                Object asyncActionResult = asyncManager.getAsyncActionResult();
+                if (asyncActionResult instanceof Throwable) {
+                    throw new Exception((Throwable) asyncActionResult);
+                }
+                asyncAction = null;
+                resultCode = saveResult(proxy.getConfig(), asyncActionResult);
             }
 
-            // this is needed because the result will be executed, then control will return to the Interceptor, which will
-            // return above and flow through again
-            if (!executed) {
-                if (preResultListeners != null) {
-                    LOG.trace("Executing PreResultListeners for result [{}]", result);
-
-                    for (Object preResultListener : preResultListeners) {
-                        PreResultListener listener = (PreResultListener) preResultListener;
-
-                        String _profileKey = "preResultListener: ";
-                        try {
-                            UtilTimerStack.push(_profileKey);
-                            listener.beforeResult(this, resultCode);
-                        }
-                        finally {
-                            UtilTimerStack.pop(_profileKey);
+            if (asyncManager == null || asyncAction == null) {
+                // this is needed because the result will be executed, then control will return to the Interceptor, which will
+                // return above and flow through again
+                if (!executed) {
+                    if (preResultListeners != null) {
+                        LOG.trace("Executing PreResultListeners for result [{}]", result);
+
+                        for (Object preResultListener : preResultListeners) {
+                            PreResultListener listener = (PreResultListener) preResultListener;
+
+                            String _profileKey = "preResultListener: ";
+                            try {
+                                UtilTimerStack.push(_profileKey);
+                                listener.beforeResult(this, resultCode);
+                            } finally {
+                                UtilTimerStack.pop(_profileKey);
+                            }
                         }
                     }
-                }
 
-                // now execute the result, if we're supposed to
-                if (proxy.getExecuteResult()) {
-                    executeResult();
-                }
+                    // now execute the result, if we're supposed to
+                    if (proxy.getExecuteResult()) {
+                        executeResult();
+                    }
 
-                executed = true;
+                    executed = true;
+                }
+            } else {
+                asyncManager.invokeAsyncAction(asyncAction);
             }
 
             return resultCode;
@@ -495,6 +515,9 @@ protected String saveResult(ActionConfig actionConfig, Object methodResult) {
             // Wire the result automatically
             container.inject(explicitResult);
             return null;
+        } else if (methodResult instanceof Callable) {
+            asyncAction = (Callable) methodResult;
+            return null;
         } else {
             return (String) methodResult;
         }
diff --git a/core/src/main/java/org/apache/struts2/dispatcher/Dispatcher.java b/core/src/main/java/org/apache/struts2/dispatcher/Dispatcher.java
index dcc5fe72a..dd2f76bea 100644
--- a/core/src/main/java/org/apache/struts2/dispatcher/Dispatcher.java
+++ b/core/src/main/java/org/apache/struts2/dispatcher/Dispatcher.java
@@ -561,8 +561,16 @@ public void serviceAction(HttpServletRequest request, HttpServletResponse respon
             String name = mapping.getName();
             String method = mapping.getMethod();
 
-            ActionProxy proxy = getContainer().getInstance(ActionProxyFactory.class).createActionProxy(
-                    namespace, name, method, extraContext, true, false);
+            ActionProxy proxy;
+
+            //check if we are probably in an async resuming
+            ActionInvocation invocation = ActionContext.getContext().getActionInvocation();
+            if (invocation == null || invocation.isExecuted()) {
+                proxy = getContainer().getInstance(ActionProxyFactory.class).createActionProxy(namespace, name, method,
+                        extraContext, true, false);
+            } else {
+                proxy = invocation.getProxy();
+            }
 
             request.setAttribute(ServletActionContext.STRUTS_VALUESTACK_KEY, proxy.getInvocation().getStack());
 
diff --git a/core/src/main/java/org/apache/struts2/dispatcher/PrepareOperations.java b/core/src/main/java/org/apache/struts2/dispatcher/PrepareOperations.java
index 354cad7fa..c216cdfb0 100644
--- a/core/src/main/java/org/apache/struts2/dispatcher/PrepareOperations.java
+++ b/core/src/main/java/org/apache/struts2/dispatcher/PrepareOperations.java
@@ -79,9 +79,12 @@ public ActionContext createActionContext(HttpServletRequest request, HttpServlet
             // detected existing context, so we are probably in a forward
             ctx = new ActionContext(new HashMap<>(oldContext.getContextMap()));
         } else {
-            ValueStack stack = dispatcher.getContainer().getInstance(ValueStackFactory.class).createValueStack();
-            stack.getContext().putAll(dispatcher.createContextMap(request, response, null));
-            ctx = new ActionContext(stack.getContext());
+            ctx = ServletActionContext.getActionContext(request);   //checks if we are probably in an async
+            if (ctx == null) {
+                ValueStack stack = dispatcher.getContainer().getInstance(ValueStackFactory.class).createValueStack();
+                stack.getContext().putAll(dispatcher.createContextMap(request, response, null));
+                ctx = new ActionContext(stack.getContext());
+            }
         }
         request.setAttribute(CLEANUP_RECURSION_COUNTER, counter);
         ActionContext.setContext(ctx);
diff --git a/core/src/test/java/com/opensymphony/xwork2/DefaultActionInvocationTest.java b/core/src/test/java/com/opensymphony/xwork2/DefaultActionInvocationTest.java
index 4e75acda2..91b6b11b9 100644
--- a/core/src/test/java/com/opensymphony/xwork2/DefaultActionInvocationTest.java
+++ b/core/src/test/java/com/opensymphony/xwork2/DefaultActionInvocationTest.java
@@ -22,6 +22,7 @@
 import com.opensymphony.xwork2.config.entities.InterceptorMapping;
 import com.opensymphony.xwork2.config.entities.ResultConfig;
 import com.opensymphony.xwork2.config.providers.XmlConfigurationProvider;
+import com.opensymphony.xwork2.interceptor.PreResultListener;
 import com.opensymphony.xwork2.mock.MockActionProxy;
 import com.opensymphony.xwork2.mock.MockInterceptor;
 import com.opensymphony.xwork2.mock.MockResult;
@@ -33,6 +34,9 @@
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
 
 
 /**
@@ -322,6 +326,92 @@ public void testInvokeWithLazyParams() throws Exception {
         assertEquals("this is blah", action.getName());
     }
 
+    public void testInvokeWithAsyncManager() throws Exception {
+        DefaultActionInvocation dai = new DefaultActionInvocation(new HashMap<String, Object>(), false);
+        dai.stack = container.getInstance(ValueStackFactory.class).createValueStack();
+
+        final Semaphore lock = new Semaphore(1);
+        lock.acquire();
+        dai.setAsyncManager(new AsyncManager() {
+            Object asyncActionResult;
+            @Override
+            public boolean hasAsyncActionResult() {
+                return asyncActionResult != null;
+            }
+
+            @Override
+            public Object getAsyncActionResult() {
+                return asyncActionResult;
+            }
+
+            @Override
+            public void invokeAsyncAction(Callable asyncAction) {
+                try {
+                    asyncActionResult = asyncAction.call();
+                } catch (Exception e) {
+                    asyncActionResult = e;
+                }
+                lock.release();
+            }
+        });
+
+        dai.action = new Callable<Callable<String>>() {
+            @Override
+            public Callable<String> call() throws Exception {
+                return new Callable<String>() {
+                    @Override
+                    public String call() throws Exception {
+                        return "success";
+                    }
+                };
+            }
+        };
+
+        MockActionProxy actionProxy = new MockActionProxy();
+        actionProxy.setMethod("call");
+        dai.proxy = actionProxy;
+
+        final boolean[] preResultExecuted = new boolean[1];
+        dai.addPreResultListener(new PreResultListener() {
+            @Override
+            public void beforeResult(ActionInvocation invocation, String resultCode) {
+                preResultExecuted[0] = true;
+            }
+        });
+
+        List<InterceptorMapping> interceptorMappings = new ArrayList<>();
+        MockInterceptor mockInterceptor1 = new MockInterceptor();
+        mockInterceptor1.setFoo("test1");
+        mockInterceptor1.setExpectedFoo("test1");
+        interceptorMappings.add(new InterceptorMapping("test1", mockInterceptor1));
+        dai.interceptors = interceptorMappings.iterator();
+
+        dai.ognlUtil = new OgnlUtil();
+
+        dai.invoke();
+
+        assertTrue("interceptor1 should be executed", mockInterceptor1.isExecuted());
+        assertFalse("preResultListener should no be executed", preResultExecuted[0]);
+        assertNotNull("an async action should be saved", dai.asyncAction);
+        assertFalse("invocation should not be executed", dai.executed);
+        assertNull("a null result should be passed to upper and wait for the async result", dai.resultCode);
+
+        if(lock.tryAcquire(1500L, TimeUnit.MILLISECONDS)) {
+            try {
+                dai.invoke();
+                assertTrue("preResultListener should be executed", preResultExecuted[0]);
+                assertNull("async action should be cleared", dai.asyncAction);
+                assertTrue("invocation should be executed", dai.executed);
+                assertEquals("success", dai.resultCode);
+            } finally {
+                lock.release();
+            }
+        } else {
+            lock.release();
+            fail("async result did not received on timeout!");
+        }
+    }
+
     public void testActionEventListener() throws Exception {
         ActionProxy actionProxy = actionProxyFactory.createActionProxy("",
                 "ExceptionFoo", "exceptionMethod", new HashMap<String, Object>());
diff --git a/core/src/test/java/org/apache/struts2/dispatcher/DispatcherTest.java b/core/src/test/java/org/apache/struts2/dispatcher/DispatcherTest.java
index 7e25fb11f..6ff918653 100644
--- a/core/src/test/java/org/apache/struts2/dispatcher/DispatcherTest.java
+++ b/core/src/test/java/org/apache/struts2/dispatcher/DispatcherTest.java
@@ -20,7 +20,9 @@
 
 import com.mockobjects.dynamic.C;
 import com.mockobjects.dynamic.Mock;
+import com.opensymphony.xwork2.ActionContext;
 import com.opensymphony.xwork2.ObjectFactory;
+import com.opensymphony.xwork2.StubValueStack;
 import com.opensymphony.xwork2.XWorkConstants;
 import com.opensymphony.xwork2.config.Configuration;
 import com.opensymphony.xwork2.config.ConfigurationManager;
@@ -30,8 +32,12 @@
 import com.opensymphony.xwork2.inject.Container;
 import com.opensymphony.xwork2.interceptor.Interceptor;
 import com.opensymphony.xwork2.LocalizedTextProvider;
+import com.opensymphony.xwork2.mock.MockActionInvocation;
+import com.opensymphony.xwork2.mock.MockActionProxy;
+import org.apache.struts2.ServletActionContext;
 import org.apache.struts2.StrutsConstants;
 import org.apache.struts2.StrutsInternalTestCase;
+import org.apache.struts2.dispatcher.mapper.ActionMapping;
 import org.apache.struts2.dispatcher.multipart.MultiPartRequestWrapper;
 import org.apache.struts2.util.ObjectFactoryDestroyable;
 import org.springframework.mock.web.MockHttpServletRequest;
@@ -321,6 +327,30 @@ public void testIsMultipartRequest() throws Exception {
         assertTrue(du.isMultipartRequest(req));
     }
 
+    public void testServiceActionResumePreviousProxy() throws Exception {
+        Dispatcher du = initDispatcher(Collections.<String, String>emptyMap());
+
+        MockActionInvocation mai = new MockActionInvocation();
+        ActionContext.getContext().setActionInvocation(mai);
+
+        MockActionProxy actionProxy = new MockActionProxy();
+        actionProxy.setInvocation(mai);
+        mai.setProxy(actionProxy);
+
+        mai.setStack(new StubValueStack());
+
+        HttpServletRequest req = new MockHttpServletRequest();
+        req.setAttribute(ServletActionContext.STRUTS_VALUESTACK_KEY, mai.getStack());
+
+        assertFalse(actionProxy.isExecutedCalled());
+
+        du.setDevMode("false");
+        du.setHandleException("false");
+        du.serviceAction(req, null, new ActionMapping());
+
+        assertTrue("should execute previous proxy", actionProxy.isExecutedCalled());
+    }
+
     class InternalConfigurationManager extends ConfigurationManager {
     	public boolean destroyConfiguration = false;
 
diff --git a/core/src/test/java/org/apache/struts2/dispatcher/PrepareOperationsTest.java b/core/src/test/java/org/apache/struts2/dispatcher/PrepareOperationsTest.java
new file mode 100644
index 000000000..02b705b11
--- /dev/null
+++ b/core/src/test/java/org/apache/struts2/dispatcher/PrepareOperationsTest.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.struts2.dispatcher;
+
+import com.opensymphony.xwork2.ActionContext;
+import com.opensymphony.xwork2.StubValueStack;
+import org.apache.struts2.ServletActionContext;
+import org.apache.struts2.StrutsInternalTestCase;
+import org.springframework.mock.web.MockHttpServletRequest;
+
+import javax.servlet.http.HttpServletRequest;
+
+public class PrepareOperationsTest extends StrutsInternalTestCase {
+    public void testCreateActionContextWhenRequestHasOne() {
+        HttpServletRequest req = new MockHttpServletRequest();
+        StubValueStack stack = new StubValueStack();
+        req.setAttribute(ServletActionContext.STRUTS_VALUESTACK_KEY, stack);
+
+        PrepareOperations prepare = new PrepareOperations(null);
+
+        ActionContext.setContext(null);
+        ActionContext actionContext = prepare.createActionContext(req, null);
+
+        assertEquals(stack.getContext(), actionContext.getContextMap());
+    }
+}
diff --git a/plugins/async/pom.xml b/plugins/async/pom.xml
new file mode 100644
index 000000000..b92e8ad60
--- /dev/null
+++ b/plugins/async/pom.xml
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+-->
+<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.apache.struts</groupId>
+        <artifactId>struts2-plugins</artifactId>
+        <version>2.6-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>struts2-async-plugin</artifactId>
+    <name>Struts 2 Async Plugin</name>
+    <packaging>jar</packaging>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>javax.servlet-api</artifactId>
+            <version>3.0.1</version>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>mockobjects</groupId>
+            <artifactId>mockobjects-core</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-web</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/plugins/async/src/main/java/org/apache/struts2/async/AsyncAction.java b/plugins/async/src/main/java/org/apache/struts2/async/AsyncAction.java
new file mode 100644
index 000000000..1edf5d45a
--- /dev/null
+++ b/plugins/async/src/main/java/org/apache/struts2/async/AsyncAction.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.struts2.async;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.Executor;
+
+/**
+ * A {@link Callable} with a timeout value and an {@link Executor}.
+ *
+ * @since 2.6
+ */
+public class AsyncAction implements Callable {
+
+    /**
+     * The action invocation was successful but did not return the result before timeout.
+     */
+    public static final String TIMEOUT = "timeout";
+
+    private Callable callable;
+    private Long timeout;
+    private Executor executor;
+
+    public AsyncAction(Callable callable) {
+        this.callable = callable;
+    }
+
+    public AsyncAction(long timeout, Callable callable) {
+        this(callable);
+        this.timeout = timeout;
+    }
+
+    public AsyncAction(Executor executor, Callable callable) {
+        this(callable);
+        this.executor = executor;
+    }
+
+    public AsyncAction(long timeout, Executor executor, Callable callable) {
+        this(timeout, callable);
+        this.executor = executor;
+    }
+
+    public Long getTimeout() {
+        return timeout;
+    }
+
+    public Executor getExecutor() {
+        return executor;
+    }
+
+    @Override
+    public Object call() throws Exception {
+        return callable.call();
+    }
+}
diff --git a/plugins/async/src/main/java/org/apache/struts2/async/DefaultAsyncManager.java b/plugins/async/src/main/java/org/apache/struts2/async/DefaultAsyncManager.java
new file mode 100644
index 000000000..8543590bb
--- /dev/null
+++ b/plugins/async/src/main/java/org/apache/struts2/async/DefaultAsyncManager.java
@@ -0,0 +1,149 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.struts2.async;
+
+import com.opensymphony.xwork2.AsyncManager;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.apache.struts2.ServletActionContext;
+
+import javax.servlet.AsyncContext;
+import javax.servlet.AsyncEvent;
+import javax.servlet.AsyncListener;
+import javax.servlet.http.HttpServletRequest;
+import java.io.IOException;
+import java.util.concurrent.Callable;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Implements {@link AsyncManager} to add support for invoke async actions via Servlet 3's API.
+ *
+ * @since 2.6
+ */
+public class DefaultAsyncManager implements AsyncManager, AsyncListener {
+    private static final Logger LOG = LogManager.getLogger(DefaultAsyncManager.class);
+    private static final AtomicInteger threadCount = new AtomicInteger(0);
+
+    private AsyncContext asyncContext;
+    private boolean asyncActionStarted;
+    private Boolean asyncCompleted;
+    private Object asyncActionResult;
+
+    @Override
+    public void invokeAsyncAction(final Callable asyncAction) {
+        if (asyncActionStarted) {
+            return;
+        }
+
+        Long timeout = null;
+        Executor executor = null;
+        if (asyncAction instanceof AsyncAction) {
+            AsyncAction customAsyncAction = (AsyncAction) asyncAction;
+            timeout = customAsyncAction.getTimeout();
+            executor = customAsyncAction.getExecutor();
+        }
+
+        HttpServletRequest req = ServletActionContext.getRequest();
+        asyncActionResult = null;
+        asyncCompleted = false;
+
+        if (asyncContext == null || !req.isAsyncStarted()) {
+            asyncContext = req.startAsync(req, ServletActionContext.getResponse());
+            asyncContext.addListener(this);
+            if (timeout != null) {
+                asyncContext.setTimeout(timeout);
+            }
+        }
+        asyncActionStarted = true;
+        LOG.debug("Async processing started for " + asyncContext);
+
+        final Runnable task = new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    setAsyncActionResultAndDispatch(asyncAction.call());
+                } catch (Throwable e) {
+                    setAsyncActionResultAndDispatch(e);
+                }
+            }
+        };
+        if (executor != null) {
+            executor.execute(task);
+        } else {
+            final Thread thread = new Thread(new Runnable() {
+                @Override
+                public void run() {
+                    try {
+                        task.run();
+                    } finally {
+                        threadCount.decrementAndGet();
+                    }
+                }
+            }, this.getClass().getSimpleName() + "-" + threadCount.incrementAndGet());
+            thread.start();
+        }
+    }
+
+    private void setAsyncActionResultAndDispatch(Object asyncActionResult) {
+        this.asyncActionResult = asyncActionResult;
+
+        String log = "Async result [" + asyncActionResult + "] of " + asyncContext;
+        if (asyncCompleted) {
+            LOG.debug(log + " - could not complete result executing due to timeout or network error");
+        } else {
+            LOG.debug(log + " - dispatching request to execute result in container");
+            asyncContext.dispatch();
+        }
+    }
+
+    @Override
+    public boolean hasAsyncActionResult() {
+        return asyncActionResult != null;
+    }
+
+    @Override
+    public Object getAsyncActionResult() {
+        return asyncActionResult;
+    }
+
+    @Override
+    public void onComplete(AsyncEvent asyncEvent) throws IOException {
+        asyncContext = null;
+        asyncCompleted = true;
+    }
+
+    @Override
+    public void onTimeout(AsyncEvent asyncEvent) throws IOException {
+        LOG.debug("Processing timeout for " + asyncEvent.getAsyncContext());
+        setAsyncActionResultAndDispatch(AsyncAction.TIMEOUT);
+    }
+
+    @Override
+    public void onError(AsyncEvent asyncEvent) throws IOException {
+        Throwable e = asyncEvent.getThrowable();
+        LOG.error("Processing error for " + asyncEvent.getAsyncContext(), e);
+        setAsyncActionResultAndDispatch(e);
+    }
+
+    @Override
+    public void onStartAsync(AsyncEvent asyncEvent) throws IOException {
+
+    }
+}
diff --git a/plugins/async/src/main/resources/struts-plugin.xml b/plugins/async/src/main/resources/struts-plugin.xml
new file mode 100644
index 000000000..fb71372d9
--- /dev/null
+++ b/plugins/async/src/main/resources/struts-plugin.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+-->
+<!DOCTYPE struts PUBLIC
+        "-//Apache Software Foundation//DTD Struts Configuration 2.5//EN"
+        "http://struts.apache.org/dtds/struts-2.5.dtd">
+
+<struts>
+    <bean type="com.opensymphony.xwork2.AsyncManager" name="default"
+          class="org.apache.struts2.async.DefaultAsyncManager" scope="prototype" />
+</struts>
diff --git a/plugins/async/src/test/java/org/apache/struts2/async/DefaultAsyncManagerTest.java b/plugins/async/src/test/java/org/apache/struts2/async/DefaultAsyncManagerTest.java
new file mode 100644
index 000000000..f4bc21bef
--- /dev/null
+++ b/plugins/async/src/test/java/org/apache/struts2/async/DefaultAsyncManagerTest.java
@@ -0,0 +1,113 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.struts2.async;
+
+import com.opensymphony.xwork2.XWorkTestCase;
+import org.apache.struts2.ServletActionContext;
+import org.springframework.mock.web.MockAsyncContext;
+import org.springframework.mock.web.MockHttpServletRequest;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+
+public class DefaultAsyncManagerTest extends XWorkTestCase {
+    public void testInvokeAsyncAction() throws Exception {
+        final MockHttpServletRequest request = new MockHttpServletRequest();
+        request.setAsyncSupported(true);
+
+        ServletActionContext.setRequest(request);
+
+        final Semaphore lock = new Semaphore(1);
+        lock.acquire();
+
+        AsyncAction asyncAction = new AsyncAction(new Callable() {
+            @Override
+            public Object call() throws Exception {
+                final MockAsyncContext mockAsyncContext = (MockAsyncContext) request.getAsyncContext();
+                mockAsyncContext.addDispatchHandler(new Runnable() {
+                    @Override
+                    public void run() {
+                        mockAsyncContext.complete();
+                        lock.release();
+                    }
+                });
+
+                return "success";
+            }
+        });
+
+        DefaultAsyncManager asyncManager = new DefaultAsyncManager();
+        asyncManager.invokeAsyncAction(asyncAction);
+        asyncManager.invokeAsyncAction(asyncAction);    // duplicate invoke should not raise any problem
+
+        if (lock.tryAcquire(1500L, TimeUnit.MILLISECONDS)) {
+            try {
+                assertTrue("an async result is expected", asyncManager.hasAsyncActionResult());
+                assertEquals("success", asyncManager.getAsyncActionResult());
+            } finally {
+                lock.release();
+            }
+        } else {
+            lock.release();
+            fail("async result did not received on timeout!");
+        }
+    }
+
+    public void testInvokeAsyncActionException() throws Exception {
+        final MockHttpServletRequest request = new MockHttpServletRequest();
+        request.setAsyncSupported(true);
+
+        ServletActionContext.setRequest(request);
+
+        final Semaphore lock = new Semaphore(1);
+        lock.acquire();
+
+        final Exception expected = new Exception();
+        AsyncAction asyncAction = new AsyncAction(new Callable() {
+            @Override
+            public Object call() throws Exception {
+                final MockAsyncContext mockAsyncContext = (MockAsyncContext) request.getAsyncContext();
+                mockAsyncContext.addDispatchHandler(new Runnable() {
+                    @Override
+                    public void run() {
+                        mockAsyncContext.complete();
+                        lock.release();
+                    }
+                });
+
+                throw expected;
+            }
+        });
+
+        DefaultAsyncManager asyncManager = new DefaultAsyncManager();
+        asyncManager.invokeAsyncAction(asyncAction);
+
+        if (lock.tryAcquire(1500L, TimeUnit.MILLISECONDS)) {
+            try {
+                assertTrue("an async result is expected", asyncManager.hasAsyncActionResult());
+                assertEquals(expected, asyncManager.getAsyncActionResult());
+            } finally {
+                lock.release();
+            }
+        } else {
+            fail("async result did not received on timeout!");
+        }
+    }
+}
diff --git a/plugins/pom.xml b/plugins/pom.xml
index e313cba3d..915f433b6 100644
--- a/plugins/pom.xml
+++ b/plugins/pom.xml
@@ -56,6 +56,7 @@
         <module>spring</module>
         <module>testng</module>
         <module>tiles</module>
+        <module>async</module>
     </modules>
 
     <dependencies>
diff --git a/pom.xml b/pom.xml
index 672b126da..169ce8bee 100644
--- a/pom.xml
+++ b/pom.xml
@@ -592,6 +592,11 @@
                 <artifactId>struts2-gxp-plugin</artifactId>
                 <version>${project.version}</version>
             </dependency>
+            <dependency>
+                <groupId>org.apache.struts</groupId>
+                <artifactId>struts2-async-plugin</artifactId>
+                <version>${project.version}</version>
+            </dependency>
             <dependency>
                 <groupId>org.apache.struts</groupId>
                 <artifactId>struts2-osgi-admin-bundle</artifactId>


 

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
users@infra.apache.org


> Asynchronous action method
> --------------------------
>
>                 Key: WW-4874
>                 URL: https://issues.apache.org/jira/browse/WW-4874
>             Project: Struts 2
>          Issue Type: New Feature
>          Components: Core Actions, Dispatch Filter
>            Reporter: Yasser Zamani
>            Priority: Major
>              Labels: action, asynchronous
>             Fix For: 2.6
>
>   Original Estimate: 1,344h
>  Remaining Estimate: 1,344h
>
> User will be able to return {{java.util.concurrent.Callable<String>}} in their actions. Struts when sees such result, runs {{resultCode = result.call();}} in it's own managed thread pool but exits from servlet's main thread with a null result, i.e. gives back main thread to container and leaves response open for concurrent processing. When {{resultCode = result.call();}} returned, Struts calls {{javax.servlet.AsyncContext.dispatch()}} and {{resumes request processing}} within a container's thread servlet to generate the appropriate result for user according to {{resultCode}}.
> This adds better support for SLS (Short request processing, Long action execution, Short response processing) via Servlet 3's Async API.
> Support of other cases like SSL (e.g. a download server) or LLL(e.g. a video converter server) is still open.



--
This message was sent by Atlassian JIRA
(v7.6.3#76005)