package fr.openwide.core.test.jpa.more.business; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import java.io.Serializable; import java.text.MessageFormat; import java.util.Date; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.LinkedBlockingQueue; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.springframework.transaction.support.TransactionTemplate; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.google.common.base.Supplier; import com.google.common.collect.Maps; import com.google.common.util.concurrent.RateLimiter; import fr.openwide.core.jpa.business.generic.service.IEntityService; import fr.openwide.core.jpa.exception.SecurityServiceException; import fr.openwide.core.jpa.exception.ServiceException; import fr.openwide.core.jpa.more.business.task.model.AbstractTask; import fr.openwide.core.jpa.more.business.task.model.QueuedTaskHolder; import fr.openwide.core.jpa.more.business.task.model.TaskExecutionResult; import fr.openwide.core.jpa.more.business.task.service.IQueuedTaskHolderManager; import fr.openwide.core.jpa.more.business.task.service.IQueuedTaskHolderService; import fr.openwide.core.jpa.more.business.task.util.TaskResult; import fr.openwide.core.jpa.more.business.task.util.TaskStatus; import fr.openwide.core.test.jpa.more.business.task.config.TestTaskManagementConfig; @ContextConfiguration(classes = TestTaskManagementConfig.class) public class TestTaskManagement extends AbstractJpaMoreTestCase { @Autowired private IEntityService entityService; @Autowired private IQueuedTaskHolderManager manager; @Autowired private IQueuedTaskHolderService taskHolderService; private TransactionTemplate transactionTemplate; /** * A utility used to check that a given task has been correctly executed. * <p>Designed to always reference the same value, even having been serialized with Jackson. */ protected static class StaticValueAccessor<T> implements Supplier<T>, Serializable { private static final long serialVersionUID = 1L; protected static final ConcurrentMap<Integer, Object> values = Maps.newConcurrentMap(); protected static volatile int idCounter = 0; private int id; public StaticValueAccessor() { this.id = ++idCounter; } public int getId() { return id; } public void setId(int id) { this.id = id; } @Override @SuppressWarnings("unchecked") public T get() { return (T) values.get(id); } public void set(T value) { values.put(id, value); } } private static abstract class AbstractTestTask extends AbstractTask { private static final long serialVersionUID = 1L; public AbstractTestTask() { super("task", "type", new Date()); } } public static class SimpleTestTask<T> extends AbstractTestTask { private static final long serialVersionUID = 1L; private StaticValueAccessor<T> valueAccessor; private T expectedValue; @JsonIgnoreProperties("stackTrace") private TaskExecutionResult expectedResult; private int timeToWaitMs = 0; protected SimpleTestTask() { } public SimpleTestTask(StaticValueAccessor<T> valueAccessor, T expectedValue, TaskExecutionResult expectedResult) { super(); this.valueAccessor = valueAccessor; this.expectedValue = expectedValue; this.expectedResult = expectedResult; } @Override protected TaskExecutionResult doTask() throws Exception { if (timeToWaitMs != 0) { Thread.sleep(timeToWaitMs); } valueAccessor.set(expectedValue); return expectedResult; } public StaticValueAccessor<T> getValueAccessor() { return valueAccessor; } public void setValueAccessor(StaticValueAccessor<T> valueAccessor) { this.valueAccessor = valueAccessor; } public T getExpectedValue() { return expectedValue; } public void setExpectedValue(T expectedValue) { this.expectedValue = expectedValue; } public TaskExecutionResult getExpectedResult() { return expectedResult; } public void setExpectedResult(TaskExecutionResult expectedResult) { this.expectedResult = expectedResult; } public int getTimeToWaitMs() { return timeToWaitMs; } public void setTimeToWaitMs(int timeToWaitMs) { this.timeToWaitMs = timeToWaitMs; } } @Autowired public void setTransactionManager(PlatformTransactionManager transactionManager) { transactionTemplate = new TransactionTemplate(transactionManager); } @Override protected void cleanAll() throws ServiceException, SecurityServiceException { cleanEntities(taskHolderService); super.cleanAll(); } protected void waitTaskConsumption() { waitTaskConsumption(false, true); } protected void waitTaskConsumption(boolean returnIfStopped, boolean waitForRunning) { RateLimiter rateLimiter = RateLimiter.create(1); int tryCount = 0; while (true) { tryCount++; rateLimiter.acquire(); if ( // if returnIfStopped == true, we consider that wait is done (returnIfStopped || manager.isActive()) && manager.getNumberOfWaitingTasks() == 0 // if we don't wait for running, ignore manager.getNumberOfRunningTasks() && (!waitForRunning || manager.getNumberOfRunningTasks() == 0)) { break; } if (tryCount > 10) { throw new IllegalStateException(MessageFormat.format("Task queue not empty after {0} tries.", tryCount)); } } } @Test public void simple() throws Exception { final StaticValueAccessor<String> result = new StaticValueAccessor<>(); final StaticValueAccessor<Long> taskHolderId = new StaticValueAccessor<>(); transactionTemplate.execute(new TransactionCallbackWithoutResult() { @Override public void doInTransactionWithoutResult(TransactionStatus status) { try { QueuedTaskHolder taskHolder = manager.submit( new SimpleTestTask<>(result, "success", TaskExecutionResult.completed()) ); taskHolderId.set(taskHolder.getId()); } catch (ServiceException e) { throw new IllegalStateException(e); } } }); entityService.flush(); entityService.clear(); waitTaskConsumption(); QueuedTaskHolder taskHolder = taskHolderService.getById(taskHolderId.get()); assertEquals(TaskStatus.COMPLETED, taskHolder.getStatus()); assertEquals(TaskResult.SUCCESS, taskHolder.getResult()); assertEquals("success", result.get()); } @Test public void noTransaction() throws Exception { final StaticValueAccessor<String> result = new StaticValueAccessor<>(); QueuedTaskHolder taskHolder = manager.submit( new SimpleTestTask<>(result, "success", TaskExecutionResult.completed()) ); entityService.flush(); entityService.clear(); waitTaskConsumption(); taskHolder = taskHolderService.getById(taskHolder.getId()); assertEquals(TaskStatus.COMPLETED, taskHolder.getStatus()); assertEquals(TaskResult.SUCCESS, taskHolder.getResult()); assertEquals("success", result.get()); } /** * Queue submit is transaction-aware ; we test here that task is pushed to queue after transaction commit. */ @Test public void submitInLongTransaction() throws Exception { final StaticValueAccessor<String> result = new StaticValueAccessor<>(); final StaticValueAccessor<String> result2 = new StaticValueAccessor<>(); final StaticValueAccessor<Long> taskHolderId = new StaticValueAccessor<>(); // concurrency mechanism to ensure that task 1 is pushed to queue before task 2 // for each step, offer then take must be done final LinkedBlockingQueue<Boolean> concurrentLinkedQueue = new LinkedBlockingQueue<Boolean>(1); Runnable runnable = new Runnable() { @Override public void run() { transactionTemplate.execute(new TransactionCallbackWithoutResult() { @Override public void doInTransactionWithoutResult(TransactionStatus status) { try { QueuedTaskHolder taskHolder = manager.submit( new SimpleTestTask<>(result, "success", TaskExecutionResult.completed()) ); // signal that submit is called for task 1 concurrentLinkedQueue.offer(true); // STEP 1 signal taskHolderId.set(taskHolder.getId()); // wait for task 2 to be pushed and committed concurrentLinkedQueue.offer(true); // STEP 2 wait // Check that the task has not been consumed during this transaction // (which could be aborted) // and that task 2 is allowed to be done assertNull(result.get()); } catch (ServiceException e) { throw new IllegalStateException(e); } } }); } }; // thread needed so that second task can be run and completed before the above transaction Thread t = new Thread(runnable); t.start(); // wait task 1 submit (submitted but not committed, so task not in queue) concurrentLinkedQueue.take(); // STEP 1 wait // push another task transactionTemplate.execute(new TransactionCallbackWithoutResult() { @Override public void doInTransactionWithoutResult(TransactionStatus status) { try { manager.submit( new SimpleTestTask<>(result2, "success", TaskExecutionResult.completed()) ); } catch (ServiceException e) { throw new IllegalStateException(e); } } }); // signal that task 2 submit transaction is completed concurrentLinkedQueue.offer(true); // STEP 2 signal // wait for task 2 transaction & post-transaction completion while (t.isAlive()) { t.join(); } waitTaskConsumption(); entityManagerClear(); // finally, task 2 is done QueuedTaskHolder taskHolder = taskHolderService.getById(taskHolderId.get()); assertEquals(TaskStatus.COMPLETED, taskHolder.getStatus()); assertEquals(TaskResult.SUCCESS, taskHolder.getResult()); assertEquals("success", result.get()); } public static class SelfInterruptingTask<T> extends SimpleTestTask<T> { private static final long serialVersionUID = 1L; @Autowired private IQueuedTaskHolderManager manager; protected SelfInterruptingTask() { super(); } public SelfInterruptingTask(StaticValueAccessor<T> valueAccessor, T expectedValue, TaskExecutionResult expectedResult) { super(valueAccessor, expectedValue, expectedResult); } @Override protected TaskExecutionResult doTask() throws Exception { // Stop the manager, then wait for it to interrupt us (waiting duration specified through setTimeToWait()) manager.stop(); return super.doTask(); } } @Test public void interrupt() throws Exception { final StaticValueAccessor<String> result = new StaticValueAccessor<>(); final StaticValueAccessor<Long> taskHolderId = new StaticValueAccessor<>(); transactionTemplate.execute(new TransactionCallbackWithoutResult() { @Override public void doInTransactionWithoutResult(TransactionStatus status) { try { // This task will stop the manager during its execution SelfInterruptingTask<String> testTask = new SelfInterruptingTask<>(result, "success", TaskExecutionResult.completed()); // we want to force an interruption testTask.setTimeToWaitMs(2000); QueuedTaskHolder taskHolder = manager.submit(testTask); taskHolderId.set(taskHolder.getId()); } catch (ServiceException e) { throw new IllegalStateException(e); } } }); entityService.flush(); entityService.clear(); waitTaskConsumption(true, true); QueuedTaskHolder taskHolder = taskHolderService.getById(taskHolderId.get()); assertEquals(TaskStatus.INTERRUPTED, taskHolder.getStatus()); assertEquals(TaskResult.FATAL, taskHolder.getResult()); assertNull(result.get()); } }