package com.vitco.manager.async; import com.vitco.manager.thread.LifeTimeThread; import com.vitco.manager.thread.ThreadManagerInterface; import org.springframework.beans.factory.annotation.Autowired; import javax.annotation.PostConstruct; import java.util.ArrayList; import java.util.HashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * Manages Async Actions */ public class AsyncActionManager { private ThreadManagerInterface threadManager; // set the action handler @Autowired public final void setThreadManager(ThreadManagerInterface threadManager) { this.threadManager = threadManager; } // list of new action private final ArrayList<AsyncAction> newActions = new ArrayList<AsyncAction>(); // list of actions private final ArrayList<String> stack = new ArrayList<String>(); // retry to execute when the main stack is empty private final ArrayList<String> idleStack = new ArrayList<String>(); // list of current action names private final HashMap<String, AsyncAction> actionNames = new HashMap<String, AsyncAction>(); public final void removeAsyncAction(String actionName) { if (null != actionNames.remove(actionName)) { stack.remove(actionName); idleStack.remove(actionName); } } // Note: re-adding an action does not ensure that the action // is at the end of the queue! public final void addAsyncAction(AsyncAction action) { synchronized (newActions) { newActions.add(action); } synchronized (workerThread) { workerThread.notify(); } } // needs to be one as those tasks can not be executed in parallel! // Note: ExecutorService is much faster than using a new thread to // execute each AsyncAction private final ExecutorService executor = Executors.newSingleThreadExecutor(); private final LifeTimeThread workerThread = new LifeTimeThread() { @Override public void onAfterStop() { executor.shutdown(); // Wait until all threads are finish //noinspection StatementWithEmptyBody while (!executor.isTerminated()) {} } @Override public void loop() throws InterruptedException { // add new tasks synchronized (newActions) { if (!newActions.isEmpty()) { for (AsyncAction action : newActions) { String actionName = action.name; if (null == actionNames.put(actionName, action)) { stack.add(actionName); } } newActions.clear(); } } // handle stack execution if (!stack.isEmpty()) { // fetch action String actionName = stack.remove(0); final AsyncAction action = actionNames.get(actionName); if (action.ready()) { // remove first in case the action adds // itself to the cue again (e.g. for refreshWorld()) actionNames.remove(actionName); executor.execute(action); } else { idleStack.add(actionName); } } else { // -- the main stack is empty // add <ready> idle stack back to main stack if (!idleStack.isEmpty()) { for (String asyncAction : idleStack) { if (actionNames.get(asyncAction).ready()) { stack.add(asyncAction); } } idleStack.removeAll(stack); } if (stack.isEmpty()) { synchronized (workerThread) { // sometimes notify is "too early"/fails(?), so we need a timeout here if (idleStack.isEmpty()) { // no actions waiting, we can be "lazy" workerThread.wait(500); } else { // there are some actions not ready yet, so we should check often workerThread.wait(50); } } } } } }; @PostConstruct public void init() { threadManager.manage(workerThread); } }