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; * <result name="success">longRunningAction-success.jsp</result> * </action> * </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(); + } +}