WW-3691 Converts BackgroundProcess into interface and uses Executor to execute BackgroundProcess

This commit is contained in:
Lukasz Lenart
2022-10-04 10:12:52 +02:00
parent 58f287bf4a
commit 8f0db1d22a
10 changed files with 513 additions and 153 deletions
@@ -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();
}
}
@@ -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">
@@ -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();
}
}
@@ -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();
}
@@ -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();
}
@@ -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;
try {
final Thread t = new Thread(new Runnable() {
public void run() {
try {
beforeInvocation();
result = invocation.invokeActionOnly();
afterInvocation();
} catch (Exception e) {
exception = e;
}
this.threadName = threadName;
this.threadPriority = threadPriority;
}
done = true;
@Override
public BackgroundProcess prepare() {
try {
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() + " }";
}
}
@@ -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();
}
}
@@ -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;
}
}
}
@@ -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();
}
}
@@ -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();
}
}