This is an automated email from the ASF dual-hosted git repository.

lukaszlenart pushed a commit to branch WW-3691-executor
in repository https://gitbox.apache.org/repos/asf/struts.git

commit 8f0db1d22a5cbfaf43d19628ea548387088f781a
Author: Lukasz Lenart <lukaszlen...@apache.org>
AuthorDate: Tue Oct 4 10:12:52 2022 +0200

    WW-3691 Converts BackgroundProcess into interface and uses Executor to 
execute BackgroundProcess
---
 .../showcase/wait/ThreadPoolExecutorProvider.java  |  56 +++++++
 apps/showcase/src/main/resources/struts-wait.xml   |   3 +
 .../interceptor/ExecuteAndWaitInterceptor.java     |  66 +++++---
 .../interceptor/exec/BackgroundProcess.java        |  41 +++++
 .../struts2/interceptor/exec/ExecutorProvider.java |  38 +++++
 .../StrutsBackgroundProcess.java}                  |  80 ++++++---
 .../interceptor/exec/StrutsExecutorProvider.java   |  53 ++++++
 .../struts2/interceptor/BackgroundProcessTest.java | 104 ------------
 .../interceptor/ExecuteAndWaitInterceptorTest.java |  46 +++++-
 .../exec/StrutsBackgroundProcessTest.java          | 179 +++++++++++++++++++++
 10 files changed, 513 insertions(+), 153 deletions(-)

diff --git 
a/apps/showcase/src/main/java/org/apache/struts2/showcase/wait/ThreadPoolExecutorProvider.java
 
b/apps/showcase/src/main/java/org/apache/struts2/showcase/wait/ThreadPoolExecutorProvider.java
new file mode 100644
index 000000000..ffffc7a1d
--- /dev/null
+++ 
b/apps/showcase/src/main/java/org/apache/struts2/showcase/wait/ThreadPoolExecutorProvider.java
@@ -0,0 +1,56 @@
+/*
+ * 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.wait;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.apache.struts2.interceptor.exec.ExecutorProvider;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.LinkedBlockingDeque;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+public class ThreadPoolExecutorProvider implements ExecutorProvider {
+
+    private static final Logger LOG = 
LogManager.getLogger(ThreadPoolExecutorProvider.class);
+
+    private final ExecutorService executor;
+
+    public ThreadPoolExecutorProvider() {
+        this.executor = new ThreadPoolExecutor(1, 2, 0L, 
TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>());
+    }
+
+    @Override
+    public void execute(Runnable task) {
+        LOG.info("Executing task: {}", task);
+        executor.execute(task);
+    }
+
+    @Override
+    public boolean isShutdown() {
+        return executor.isShutdown();
+    }
+
+    @Override
+    public void shutdown() {
+        LOG.info("Shutting down executor");
+        executor.shutdown();
+    }
+}
diff --git a/apps/showcase/src/main/resources/struts-wait.xml 
b/apps/showcase/src/main/resources/struts-wait.xml
index 8ede40d74..7b6a204a6 100644
--- a/apps/showcase/src/main/resources/struts-wait.xml
+++ b/apps/showcase/src/main/resources/struts-wait.xml
@@ -24,6 +24,9 @@
        "https://struts.apache.org/dtds/struts-2.5.dtd";>
 
 <struts>
+
+    <bean type="org.apache.struts2.interceptor.exec.ExecutorProvider" 
class="org.apache.struts2.showcase.wait.ThreadPoolExecutorProvider"/>
+
     <package name="wait" extends="struts-default" namespace="/wait">
 
         <action name="example1">
diff --git 
a/core/src/main/java/org/apache/struts2/interceptor/ExecuteAndWaitInterceptor.java
 
b/core/src/main/java/org/apache/struts2/interceptor/ExecuteAndWaitInterceptor.java
index b49ebcae7..4a5cb91b4 100644
--- 
a/core/src/main/java/org/apache/struts2/interceptor/ExecuteAndWaitInterceptor.java
+++ 
b/core/src/main/java/org/apache/struts2/interceptor/ExecuteAndWaitInterceptor.java
@@ -22,12 +22,17 @@ import com.opensymphony.xwork2.Action;
 import com.opensymphony.xwork2.ActionContext;
 import com.opensymphony.xwork2.ActionInvocation;
 import com.opensymphony.xwork2.ActionProxy;
+import com.opensymphony.xwork2.config.entities.ResultConfig;
 import com.opensymphony.xwork2.inject.Container;
 import com.opensymphony.xwork2.inject.Inject;
 import com.opensymphony.xwork2.interceptor.MethodFilterInterceptor;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.apache.struts2.ServletActionContext;
+import org.apache.struts2.interceptor.exec.BackgroundProcess;
+import org.apache.struts2.interceptor.exec.ExecutorProvider;
+import org.apache.struts2.interceptor.exec.StrutsBackgroundProcess;
+import org.apache.struts2.interceptor.exec.StrutsExecutorProvider;
 import org.apache.struts2.util.TokenHelper;
 import org.apache.struts2.views.freemarker.FreemarkerResult;
 
@@ -84,7 +89,7 @@ import java.util.Map;
  * <!-- END SNIPPET: description -->
  *
  * <p><u>Interceptor parameters:</u></p>
- *
+ * <p>
  * <!-- START SNIPPET: parameters -->
  *
  * <ul>
@@ -94,11 +99,11 @@ import java.util.Map;
  * <li>delaySleepInterval (optional) - only used with delay. Used for waking 
up at certain intervals to check if the background process is already done. 
Default is 100 millis.</li>
  *
  * </ul>
- *
+ * <p>
  * <!-- END SNIPPET: parameters -->
  *
  * <p><u>Extending the interceptor:</u></p>
- *
+ * <p>
  * <!-- START SNIPPET: extending -->
  * <p>
  * If you wish to make special preparations before and/or after the invocation 
of the background thread, you can extend
@@ -167,9 +172,8 @@ import java.util.Map;
  *     &lt;result 
name="success"&gt;longRunningAction-success.jsp&lt;/result&gt;
  * &lt;/action&gt;
  * </pre>
- *
+ * <p>
  * <!-- END SNIPPET: example -->
- *
  */
 public class ExecuteAndWaitInterceptor extends MethodFilterInterceptor {
 
@@ -186,22 +190,28 @@ public class ExecuteAndWaitInterceptor extends 
MethodFilterInterceptor {
     private int threadPriority = Thread.NORM_PRIORITY;
 
     private Container container;
+    private ExecutorProvider executor;
 
     @Inject
     public void setContainer(Container container) {
         this.container = container;
     }
 
+    @Inject(required = false)
+    public void setExecutorProvider(ExecutorProvider executorProvider) {
+        this.executor = executorProvider;
+    }
+
     /**
      * Creates a new background process
      *
-     * @param name The process name
+     * @param name             The process name
      * @param actionInvocation The action invocation
-     * @param threadPriority The thread priority
+     * @param threadPriority   The thread priority
      * @return The new process
      */
     protected BackgroundProcess getNewBackgroundProcess(String name, 
ActionInvocation actionInvocation, int threadPriority) {
-        return new BackgroundProcess(name + "BackgroundThread", 
actionInvocation, threadPriority);
+        return new StrutsBackgroundProcess(actionInvocation, name + 
"_background-process", threadPriority);
     }
 
     /**
@@ -209,7 +219,6 @@ public class ExecuteAndWaitInterceptor extends 
MethodFilterInterceptor {
      * are mapped to requests.
      *
      * @param proxy action proxy
-     *
      * @return the name of the background thread
      */
     protected String getBackgroundProcessName(ActionProxy proxy) {
@@ -223,10 +232,10 @@ public class ExecuteAndWaitInterceptor extends 
MethodFilterInterceptor {
         ActionProxy proxy = actionInvocation.getProxy();
         String name = getBackgroundProcessName(proxy);
         ActionContext context = actionInvocation.getInvocationContext();
-        Map session = context.getSession();
+        Map<String, Object> session = context.getSession();
         HttpSession httpSession = 
ServletActionContext.getRequest().getSession(true);
 
-        Boolean secondTime  = true;
+        Boolean secondTime = true;
         if (executeAfterValidationPass) {
             secondTime = (Boolean) context.get(KEY);
             if (secondTime == null) {
@@ -250,8 +259,13 @@ public class ExecuteAndWaitInterceptor extends 
MethodFilterInterceptor {
             }
 
             if ((!executeAfterValidationPass || secondTime) && bp == null) {
-                bp = getNewBackgroundProcess(name, actionInvocation, 
threadPriority);
+                bp = getNewBackgroundProcess(name, actionInvocation, 
threadPriority).prepare();
                 session.put(KEY + name, bp);
+                if (executor.isShutdown()) {
+                    LOG.warn("Executor is shutting down, cannot execute a new 
process");
+                    return actionInvocation.invoke();
+                }
+                executor.execute(bp);
                 performInitialDelay(bp); // first time let some time pass 
before showing wait page
                 secondTime = false;
             }
@@ -259,16 +273,16 @@ public class ExecuteAndWaitInterceptor extends 
MethodFilterInterceptor {
             if ((!executeAfterValidationPass || !secondTime) && bp != null && 
!bp.isDone()) {
                 actionInvocation.getStack().push(bp.getAction());
 
-                               final String token = TokenHelper.getToken();
-                               if (token != null) {
-                                       
TokenHelper.setSessionToken(TokenHelper.getTokenName(), token);
+                final String token = TokenHelper.getToken();
+                if (token != null) {
+                    TokenHelper.setSessionToken(TokenHelper.getTokenName(), 
token);
                 }
 
-                Map results = proxy.getConfig().getResults();
+                Map<String, ResultConfig> results = 
proxy.getConfig().getResults();
                 if (!results.containsKey(WAIT)) {
-                       LOG.warn("ExecuteAndWait interceptor has detected that 
no result named 'wait' is available. " +
-                            "Defaulting to a plain built-in wait page. It is 
highly recommend you " +
-                            "provide an action-specific or global result named 
'{}'.", WAIT);
+                    LOG.warn("ExecuteAndWait interceptor has detected that no 
result named 'wait' is available. " +
+                        "Defaulting to a plain built-in wait page. It is 
highly recommend you " +
+                        "provide an action-specific or global result named 
'{}'.", WAIT);
                     // no wait result? hmm -- let's try to do dynamically put 
it in for you!
 
                     //we used to add a fake "wait" result here, since the 
configuration is unmodifiable, that is no longer
@@ -286,7 +300,7 @@ public class ExecuteAndWaitInterceptor extends 
MethodFilterInterceptor {
                 session.remove(KEY + name);
                 actionInvocation.getStack().push(bp.getAction());
 
-                // if an exception occured during action execution, throw it 
here
+                // if an exception occurred during action execution, throw it 
here
                 if (bp.getException() != null) {
                     throw bp.getException();
                 }
@@ -369,5 +383,17 @@ public class ExecuteAndWaitInterceptor extends 
MethodFilterInterceptor {
         this.executeAfterValidationPass = executeAfterValidationPass;
     }
 
+    @Override
+    public void init() {
+        super.init();
+        if (executor == null) {
+            executor = new StrutsExecutorProvider();
+        }
+    }
 
+    @Override
+    public void destroy() {
+        super.destroy();
+        executor.shutdown();
+    }
 }
diff --git 
a/core/src/main/java/org/apache/struts2/interceptor/exec/BackgroundProcess.java 
b/core/src/main/java/org/apache/struts2/interceptor/exec/BackgroundProcess.java
new file mode 100644
index 000000000..732c3d0af
--- /dev/null
+++ 
b/core/src/main/java/org/apache/struts2/interceptor/exec/BackgroundProcess.java
@@ -0,0 +1,41 @@
+/*
+ * 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.interceptor.exec;
+
+import com.opensymphony.xwork2.ActionInvocation;
+import org.apache.struts2.interceptor.ExecuteAndWaitInterceptor;
+
+/**
+ * Interface used to create a background process which will be executed by
+ * {@link ExecuteAndWaitInterceptor}
+ */
+public interface BackgroundProcess extends Runnable {
+
+    BackgroundProcess prepare();
+
+    Object getAction();
+
+    ActionInvocation getInvocation();
+
+    String getResult();
+
+    Exception getException();
+
+    boolean isDone();
+}
diff --git 
a/core/src/main/java/org/apache/struts2/interceptor/exec/ExecutorProvider.java 
b/core/src/main/java/org/apache/struts2/interceptor/exec/ExecutorProvider.java
new file mode 100644
index 000000000..a6b06bde9
--- /dev/null
+++ 
b/core/src/main/java/org/apache/struts2/interceptor/exec/ExecutorProvider.java
@@ -0,0 +1,38 @@
+/*
+ * 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.interceptor.exec;
+
+import java.util.concurrent.ExecutorService;
+
+/**
+ * Interface mimics {@link ExecutorService} to be used with
+ * {@link org.apache.struts2.interceptor.ExecuteAndWaitInterceptor}
+ * to execute {@link BackgroundProcess}
+ *
+ * @since 6.1.0
+ */
+public interface ExecutorProvider {
+
+    void execute(Runnable task);
+
+    boolean isShutdown();
+
+    void shutdown();
+
+}
diff --git 
a/core/src/main/java/org/apache/struts2/interceptor/BackgroundProcess.java 
b/core/src/main/java/org/apache/struts2/interceptor/exec/StrutsBackgroundProcess.java
similarity index 63%
rename from 
core/src/main/java/org/apache/struts2/interceptor/BackgroundProcess.java
rename to 
core/src/main/java/org/apache/struts2/interceptor/exec/StrutsBackgroundProcess.java
index eed1811e0..8f563912b 100644
--- a/core/src/main/java/org/apache/struts2/interceptor/BackgroundProcess.java
+++ 
b/core/src/main/java/org/apache/struts2/interceptor/exec/StrutsBackgroundProcess.java
@@ -16,24 +16,27 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.struts2.interceptor;
-
-import java.io.Serializable;
+package org.apache.struts2.interceptor.exec;
 
 import com.opensymphony.xwork2.ActionContext;
 import com.opensymphony.xwork2.ActionInvocation;
 
+import java.io.Serializable;
+
 /**
  * Background thread to be executed by the ExecuteAndWaitInterceptor.
- *
  */
-public class BackgroundProcess implements Serializable {
+public class StrutsBackgroundProcess implements BackgroundProcess, 
Serializable {
 
     private static final long serialVersionUID = 3884464776311686443L;
 
+    private final String threadName;
+    private final int threadPriority;
+
+    private transient  Thread processThread;
     //WW-4900 transient since 2.5.15
-    transient protected ActionInvocation invocation;
-    transient protected Exception exception;
+    protected transient ActionInvocation invocation;
+    protected transient  Exception exception;
 
     protected String result;
     protected boolean done;
@@ -41,32 +44,47 @@ public class BackgroundProcess implements Serializable {
     /**
      * Constructs a background process
      *
-     * @param threadName The thread name
      * @param invocation The action invocation
-     * @param threadPriority The thread priority
+     * @param threadName The name of background thread
+     * @param threadPriority The priority of background thread
      */
-    public BackgroundProcess(String threadName, final ActionInvocation 
invocation, int threadPriority) {
+    public StrutsBackgroundProcess(ActionInvocation invocation, String 
threadName, int threadPriority) {
         this.invocation = invocation;
+        this.threadName = threadName;
+        this.threadPriority = threadPriority;
+    }
+
+    @Override
+    public BackgroundProcess prepare() {
         try {
-            final Thread t = new Thread(new Runnable() {
-                public void run() {
-                    try {
-                        beforeInvocation();
-                        result = invocation.invokeActionOnly();
-                        afterInvocation();
-                    } catch (Exception e) {
-                        exception = e;
-                    }
-
-                    done = true;
+            processThread = new Thread(() -> {
+                try {
+                    beforeInvocation();
+                    result = invocation.invokeActionOnly();
+                    afterInvocation();
+                } catch (Exception e) {
+                    exception = e;
                 }
+
+                done = true;
             });
-            t.setName(threadName);
-            t.setPriority(threadPriority);
-            t.start();
+            processThread.setName(threadName);
+            processThread.setPriority(threadPriority);
         } catch (Exception e) {
+            done = true;
             exception = e;
         }
+        return this;
+    }
+
+    @Override
+    public void run() {
+        if (processThread == null) {
+            done = true;
+            exception = new IllegalStateException("Background thread " + 
threadName + " has not been prepared!");
+            return;
+        }
+        processThread.start();
     }
 
     /**
@@ -93,8 +111,9 @@ public class BackgroundProcess implements Serializable {
     /**
      * Retrieves the action.
      *
-     * @return  the action.
+     * @return the action.
      */
+    @Override
     public Object getAction() {
         return invocation.getAction();
     }
@@ -104,6 +123,7 @@ public class BackgroundProcess implements Serializable {
      *
      * @return the action invocation
      */
+    @Override
     public ActionInvocation getInvocation() {
         return invocation;
     }
@@ -111,8 +131,9 @@ public class BackgroundProcess implements Serializable {
     /**
      * Gets the result of the background process.
      *
-     * @return  the result; <tt>null</tt> if not done.
+     * @return the result; <tt>null</tt> if not done.
      */
+    @Override
     public String getResult() {
         return result;
     }
@@ -122,6 +143,7 @@ public class BackgroundProcess implements Serializable {
      *
      * @return the exception or <tt>null</tt> if no exception was thrown.
      */
+    @Override
     public Exception getException() {
         return exception;
     }
@@ -131,7 +153,13 @@ public class BackgroundProcess implements Serializable {
      *
      * @return <tt>true</tt> if finished, <tt>false</tt> otherwise
      */
+    @Override
     public boolean isDone() {
         return done;
     }
+
+    @Override
+    public String toString() {
+        return "StrutsBackgroundProcess { name = " + processThread.getName() + 
" }";
+    }
 }
diff --git 
a/core/src/main/java/org/apache/struts2/interceptor/exec/StrutsExecutorProvider.java
 
b/core/src/main/java/org/apache/struts2/interceptor/exec/StrutsExecutorProvider.java
new file mode 100644
index 000000000..7370f318a
--- /dev/null
+++ 
b/core/src/main/java/org/apache/struts2/interceptor/exec/StrutsExecutorProvider.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.interceptor.exec;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+public class StrutsExecutorProvider implements ExecutorProvider {
+
+    private static final Logger LOG = 
LogManager.getLogger(StrutsExecutorProvider.class);
+
+    private final ExecutorService executor;
+
+    public StrutsExecutorProvider() {
+        this.executor = Executors.newSingleThreadExecutor();
+    }
+
+    @Override
+    public void execute(Runnable task) {
+        LOG.debug("Executing task: {}", task);
+        executor.execute(task);
+    }
+
+    @Override
+    public boolean isShutdown() {
+        return executor.isShutdown();
+    }
+
+    @Override
+    public void shutdown() {
+        LOG.debug("Shutting down executor");
+        executor.shutdown();
+    }
+}
diff --git 
a/core/src/test/java/org/apache/struts2/interceptor/BackgroundProcessTest.java 
b/core/src/test/java/org/apache/struts2/interceptor/BackgroundProcessTest.java
deleted file mode 100644
index b811b1dd5..000000000
--- 
a/core/src/test/java/org/apache/struts2/interceptor/BackgroundProcessTest.java
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * 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.interceptor;
-
-import com.mockobjects.servlet.MockHttpServletRequest;
-import com.opensymphony.xwork2.ActionContext;
-import com.opensymphony.xwork2.mock.MockActionInvocation;
-import org.apache.struts2.StrutsInternalTestCase;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.ObjectInputStream;
-import java.io.ObjectOutputStream;
-import java.util.concurrent.Callable;
-import java.util.concurrent.Semaphore;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Test case for BackgroundProcessTest.
- */
-public class BackgroundProcessTest extends StrutsInternalTestCase {
-
-    public void testSerializeDeserialize() throws Exception {
-        final NotSerializableException expectedException = new 
NotSerializableException(new MockHttpServletRequest());
-        final Semaphore lock = new Semaphore(1);
-        lock.acquire();
-        MockActionInvocationWithActionInvoker invocation = new 
MockActionInvocationWithActionInvoker(new Callable<String>() {
-            @Override
-            public String call() throws Exception {
-                lock.release();
-                throw expectedException;
-            }
-        });
-        invocation.setInvocationContext(ActionContext.getContext());
-
-        BackgroundProcess bp = new 
BackgroundProcess("BackgroundProcessTest.testSerializeDeserialize", invocation
-                , Thread.MIN_PRIORITY);
-        if(!lock.tryAcquire(1500L, TimeUnit.MILLISECONDS)) {
-            lock.release();
-            fail("background thread did not release lock on timeout");
-        }
-        lock.release();
-
-        bp.result = "BackgroundProcessTest.testSerializeDeserialize";
-        bp.done = true;
-        Thread.sleep(1000);//give a chance to background thread to set 
exception
-        assertEquals(expectedException, bp.exception);
-
-        ByteArrayOutputStream baos = new ByteArrayOutputStream();
-        ObjectOutputStream oos = new ObjectOutputStream(baos);
-        oos.writeObject(bp);
-        oos.close();
-        byte b[] = baos.toByteArray();
-        baos.close();
-
-        ByteArrayInputStream bais = new ByteArrayInputStream(b);
-        ObjectInputStream ois = new ObjectInputStream(bais);
-        BackgroundProcess deserializedBp = (BackgroundProcess) 
ois.readObject();
-        ois.close();
-        bais.close();
-
-        assertNull("invocation should not be serialized", 
deserializedBp.invocation);
-        assertNull("exception should not be serialized", 
deserializedBp.exception);
-        assertEquals(bp.result, deserializedBp.result);
-        assertEquals(bp.done, deserializedBp.done);
-    }
-
-
-    private class MockActionInvocationWithActionInvoker extends 
MockActionInvocation {
-        private Callable<String> actionInvoker;
-
-        MockActionInvocationWithActionInvoker(Callable<String> actionInvoker){
-            this.actionInvoker = actionInvoker;
-        }
-
-        @Override
-        public String invokeActionOnly() throws Exception {
-            return actionInvoker.call();
-        }
-    }
-
-    private class NotSerializableException extends Exception {
-        private MockHttpServletRequest notSerializableField;
-        NotSerializableException(MockHttpServletRequest notSerializableField) {
-            this.notSerializableField = notSerializableField;
-        }
-    }
-}
\ No newline at end of file
diff --git 
a/core/src/test/java/org/apache/struts2/interceptor/ExecuteAndWaitInterceptorTest.java
 
b/core/src/test/java/org/apache/struts2/interceptor/ExecuteAndWaitInterceptorTest.java
index 08c034b9b..87595d9b5 100644
--- 
a/core/src/test/java/org/apache/struts2/interceptor/ExecuteAndWaitInterceptorTest.java
+++ 
b/core/src/test/java/org/apache/struts2/interceptor/ExecuteAndWaitInterceptorTest.java
@@ -36,9 +36,9 @@ import 
com.opensymphony.xwork2.interceptor.ParametersInterceptor;
 import com.opensymphony.xwork2.mock.MockResult;
 import com.opensymphony.xwork2.ognl.OgnlUtil;
 import com.opensymphony.xwork2.util.location.LocatableProperties;
-import org.apache.struts2.ServletActionContext;
 import org.apache.struts2.StrutsInternalTestCase;
 import org.apache.struts2.dispatcher.HttpParameters;
+import org.apache.struts2.interceptor.exec.ExecutorProvider;
 import org.apache.struts2.views.jsp.StrutsMockHttpServletRequest;
 import org.apache.struts2.views.jsp.StrutsMockHttpSession;
 
@@ -49,6 +49,8 @@ import java.io.ObjectInputStream;
 import java.io.ObjectOutputStream;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 
 /**
  * Test case for ExecuteAndWaitInterceptor.
@@ -78,6 +80,20 @@ public class ExecuteAndWaitInterceptorTest extends 
StrutsInternalTestCase {
         assertEquals("success", result2);
     }
 
+    public void testExecutorProvider() throws Exception {
+        waitInterceptor.setExecutorProvider(new TestExecutorProvider());
+
+        ActionProxy proxy = buildProxy("action1");
+        String result = proxy.execute();
+        assertEquals("wait", result);
+
+        Thread.sleep(1000);
+
+        ActionProxy proxy2 = buildProxy("action1");
+        String result2 = proxy2.execute();
+        assertEquals("success", result2);
+    }
+
     public void testTwoWait() throws Exception {
         waitInterceptor.setDelay(0);
         waitInterceptor.setDelaySleepInterval(0);
@@ -226,7 +242,11 @@ public class ExecuteAndWaitInterceptorTest extends 
StrutsInternalTestCase {
             .withServletRequest(request)
             .getContextMap();
 
+        container.inject(waitInterceptor);
         container.inject(parametersInterceptor);
+
+        waitInterceptor.init();
+        parametersInterceptor.init();
     }
 
     protected void tearDown() throws Exception {
@@ -250,8 +270,6 @@ public class ExecuteAndWaitInterceptorTest extends 
StrutsInternalTestCase {
         }
 
         public void loadPackages() throws ConfigurationException {
-
-
             // interceptors
             waitInterceptor = new ExecuteAndWaitInterceptor();
             parametersInterceptor = new ParametersInterceptor();
@@ -273,5 +291,27 @@ public class ExecuteAndWaitInterceptorTest extends 
StrutsInternalTestCase {
         }
 
     }
+
 }
 
+class TestExecutorProvider implements ExecutorProvider {
+
+    private final ExecutorService executor = 
Executors.newSingleThreadExecutor();
+
+    @Override
+    public void execute(Runnable task) {
+        executor.execute(task);
+    }
+
+    @Override
+    public boolean isShutdown() {
+        return executor.isShutdown();
+    }
+
+    @Override
+    public void shutdown() {
+        executor.shutdown();
+    }
+}
+
+
diff --git 
a/core/src/test/java/org/apache/struts2/interceptor/exec/StrutsBackgroundProcessTest.java
 
b/core/src/test/java/org/apache/struts2/interceptor/exec/StrutsBackgroundProcessTest.java
new file mode 100644
index 000000000..64ae48771
--- /dev/null
+++ 
b/core/src/test/java/org/apache/struts2/interceptor/exec/StrutsBackgroundProcessTest.java
@@ -0,0 +1,179 @@
+/*
+ * 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.interceptor.exec;
+
+import com.mockobjects.servlet.MockHttpServletRequest;
+import com.opensymphony.xwork2.ActionContext;
+import com.opensymphony.xwork2.ActionInvocation;
+import com.opensymphony.xwork2.mock.MockActionInvocation;
+import org.apache.struts2.StrutsInternalTestCase;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Test case for BackgroundProcessTest.
+ */
+public class StrutsBackgroundProcessTest extends StrutsInternalTestCase {
+
+    private final ExecutorService executor = 
Executors.newSingleThreadExecutor();
+
+    public void testSerializeDeserialize() throws Exception {
+        final NotSerializableException expectedException = new 
NotSerializableException(new MockHttpServletRequest());
+        final Semaphore lock = new Semaphore(1);
+        lock.acquire();
+        MockActionInvocationWithActionInvoker invocation = new 
MockActionInvocationWithActionInvoker(() -> {
+            lock.release();
+            throw expectedException;
+        });
+        invocation.setInvocationContext(ActionContext.getContext());
+
+        StrutsBackgroundProcess bp = (StrutsBackgroundProcess) new 
StrutsBackgroundProcess(
+            invocation,
+            "BackgroundProcessTest.testSerializeDeserialize",
+            Thread.MIN_PRIORITY
+        ).prepare();
+        executor.execute(bp);
+
+        if (!lock.tryAcquire(1500L, TimeUnit.MILLISECONDS)) {
+            lock.release();
+            fail("background thread did not release lock on timeout");
+        }
+        lock.release();
+
+        bp.result = "BackgroundProcessTest.testSerializeDeserialize";
+        bp.done = true;
+        Thread.sleep(1000);//give a chance to background thread to set 
exception
+        assertEquals(expectedException, bp.exception);
+
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        ObjectOutputStream oos = new ObjectOutputStream(baos);
+        oos.writeObject(bp);
+        oos.close();
+        byte[] b = baos.toByteArray();
+        baos.close();
+
+        ByteArrayInputStream bais = new ByteArrayInputStream(b);
+        ObjectInputStream ois = new ObjectInputStream(bais);
+        StrutsBackgroundProcess deserializedBp = (StrutsBackgroundProcess) 
ois.readObject();
+        ois.close();
+        bais.close();
+
+        assertNull("invocation should not be serialized", 
deserializedBp.invocation);
+        assertNull("exception should not be serialized", 
deserializedBp.exception);
+        assertEquals(bp.result, deserializedBp.result);
+        assertEquals(bp.done, deserializedBp.done);
+    }
+
+    public void testMultipleProcesses() throws InterruptedException {
+        Random random = new SecureRandom();
+        AtomicInteger mutableState = new AtomicInteger(0);
+        MockActionInvocationWithActionInvoker invocation = new 
MockActionInvocationWithActionInvoker(() -> {
+            Thread.sleep(Math.max(50, random.nextInt(150)));
+            mutableState.getAndIncrement();
+            return "done";
+        });
+
+        List<BackgroundProcess> bps = new ArrayList<>();
+        for (int i = 0; i < 100; i++) {
+            String name = String.format("Order: %s", i);
+            BackgroundProcess bp = new LockBackgroundProcess(invocation, 
name).prepare();
+            bps.add(bp);
+            executor.execute(bp);
+        }
+
+        Thread.sleep(300);
+
+        for (BackgroundProcess bp : bps) {
+            assertTrue("Process is still active: " + bp, bp.isDone());
+        }
+        assertEquals(100, mutableState.get());
+    }
+
+    public void testUnpreparedProcess() throws ExecutionException, 
InterruptedException, TimeoutException {
+        // given
+        MockActionInvocationWithActionInvoker invocation = new 
MockActionInvocationWithActionInvoker(() -> "done");
+        BackgroundProcess bp = new StrutsBackgroundProcess(invocation, 
"Unprepared", Thread.NORM_PRIORITY);
+
+        // when
+        executor.submit(bp).get(1000, TimeUnit.MILLISECONDS);
+
+        // then
+        assertTrue(bp.isDone());
+        assertEquals("Background thread Unprepared has not been prepared!", 
bp.getException().getMessage());
+    }
+
+    private static class MockActionInvocationWithActionInvoker extends 
MockActionInvocation {
+        private final Callable<String> actionInvoker;
+
+        MockActionInvocationWithActionInvoker(Callable<String> actionInvoker) {
+            this.actionInvoker = actionInvoker;
+        }
+
+        @Override
+        public String invokeActionOnly() throws Exception {
+            return actionInvoker.call();
+        }
+    }
+
+    private static class NotSerializableException extends Exception {
+        private MockHttpServletRequest notSerializableField;
+
+        NotSerializableException(MockHttpServletRequest notSerializableField) {
+            this.notSerializableField = notSerializableField;
+        }
+    }
+
+}
+
+class LockBackgroundProcess extends StrutsBackgroundProcess {
+
+    private final Object lock = LockBackgroundProcess.class;
+
+    public LockBackgroundProcess(ActionInvocation invocation, String name) {
+        super(invocation, name, Thread.NORM_PRIORITY);
+    }
+
+    @Override
+    public void run() {
+        synchronized (lock) {
+            super.run();
+        }
+    }
+
+    @Override
+    protected void afterInvocation() throws Exception {
+        super.afterInvocation();
+        lock.notify();
+    }
+}


Reply via email to