package org.peerbox.watchservice; import java.nio.file.Path; import java.util.Iterator; import java.util.Map; import java.util.concurrent.BlockingQueue; import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import net.engio.mbassy.listener.Handler; import org.hive2hive.core.exceptions.AbortModificationCode; import org.hive2hive.core.exceptions.ErrorCode; import org.hive2hive.core.exceptions.Hive2HiveException; import org.hive2hive.core.exceptions.NoPeerConnectionException; import org.hive2hive.core.exceptions.NoSessionException; import org.hive2hive.processframework.exceptions.ProcessExecutionException; import org.peerbox.app.config.IPeerWaspConfig; import org.peerbox.app.manager.ProcessHandle; import org.peerbox.app.manager.file.FileInfo; import org.peerbox.app.manager.file.IFileManager; import org.peerbox.app.manager.file.messages.FileExecutionFailedMessage; import org.peerbox.app.manager.file.messages.FileExecutionStartedMessage; import org.peerbox.app.manager.file.messages.FileExecutionSucceededMessage; import org.peerbox.events.IMessage; import org.peerbox.forcesync.ForceSyncCompleteMessage; import org.peerbox.forcesync.ForceSyncMessage; import org.peerbox.notifications.InformationNotification; import org.peerbox.view.tray.SynchronizationCompleteNotification; import org.peerbox.view.tray.SynchronizationErrorsResolvedNotification; import org.peerbox.view.tray.SynchronizationStartsNotification; import org.peerbox.watchservice.filetree.IFileTree; import org.peerbox.watchservice.filetree.composite.FileComponent; import org.peerbox.watchservice.filetree.composite.FolderComposite; import org.peerbox.watchservice.states.ExecutionHandle; import org.peerbox.watchservice.states.LocalMoveState; import org.peerbox.watchservice.states.StateType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.collect.SetMultimap; import com.google.inject.Inject; /** * The ActionExecutor service conducts the aggregation of events for every file * and folder and uses a * {@link FileComponentQueue.src.main.java .org.peerbox.watchservice.ActionQueue * ActionQueue} for this purpose, which runs in a separate thread. Ready * actions, for which no new events have been observed for a specified time * span, are executed. The class maintains asynchronous handles for ongoing * executions to check whether they conclude successfully or not and react * accordingly. * * This class uses the {@link org.peerbox.events.MessageBus MessageBus} instance * of the injected {@link org.peerbox.watchservice.FileEventManager * FileEventManager} instance to publish * {@link org.peerbox.view.tray.SynchronizationErrorsResolvedNotification * SynchronizationErrorsResolvedNotification}, * {@link org.peerbox.app.manager.file.messages.FileExecutionStartedMessage * FileExecutionStartedMessage}, * {@link org.peerbox.app.manager.file.messages.FileExecutionSucceededMessage * FileExecutionSucceededMessage}, * {@link org.peerbox.app.manager.file.messages.FileExecutionFailedMessage * FileExecutionFailedMessage}, and * {@link org.peerbox.view.tray.SynchronizationCompleteNotification * SynchronizationCompleteNotification}. * * @author albrecht * */ public class ActionExecutor implements Runnable { private static final Logger logger = LoggerFactory.getLogger(ActionExecutor.class); private IPeerWaspConfig peerWaspConfig; private final IFileManager fileManager; private final FileEventManager fileEventManager; /** Set to false if not interested in result of network transactions **/ private boolean waitForActionCompletion = true; /** * Queue to store the handles of executing transactions, ending transactions * are examined asynchronously **/ private final BlockingQueue<ExecutionHandle> asyncHandles; private Thread asyncHandlesThread; private Thread executorThread; private boolean forceSyncRunning = false; /** * @param eventManager * determines the event handling * @param fileManager * is passed when actions are executed to access the H2H API. * @param peerWaspConfig * defines important runtime parameters like the number of * concurrently executed actions or the aggregation time span for * events. */ @Inject public ActionExecutor(final FileEventManager eventManager, final IFileManager fileManager, IPeerWaspConfig peerWaspConfig) { this.fileEventManager = eventManager; this.fileManager = fileManager; this.peerWaspConfig = peerWaspConfig; this.asyncHandles = new LinkedBlockingQueue<ExecutionHandle>(); } public void start() { // executor / asyncHandles thread must not exist already if (executorThread != null || asyncHandlesThread != null) { throw new IllegalStateException(String.format( "Calling start() is not allowed when action executor is already running " + "(executorThread=%s, asyncHandlesThread=%s)", executorThread, asyncHandlesThread)); } executorThread = new Thread(this, "ActionExecutorThread"); executorThread.start(); asyncHandlesThread = new Thread(new ExecutingActionHandler(), "AsyncActionHandlerThread"); asyncHandlesThread.start(); } public void stop() { if (executorThread != null) { executorThread.interrupt(); executorThread = null; } if (asyncHandlesThread != null) { asyncHandlesThread.interrupt(); asyncHandlesThread = null; } asyncHandles.clear(); } @Override public void run() { processActions(); } /** * Processes the actions in the action queue, one by one. For each action, * the thread checks if a slot is free (i.e. the upper bound of concurrent * executions is not reached) and if the timestamp (updated on every event) * of the action is old enough to conclude the event aggregation for this * action. Besides that, the method checks if the ancestors of a file have * been uploaded to the network yet, to prevent a * {@link org.hive2hive.core. src.main.java.org.hive2hive.core.exceptions.ParentInUserProfileNotFoundException} * * @throws IllegalFileLocation * @throws NoPeerConnectionException * @throws NoSessionException */ private synchronized void processActions() { while (true) { if (Thread.currentThread().isInterrupted()) { // quit processing loop if thread was interrupted return; } FileComponent next = null; try { next = fileEventManager.getFileComponentQueue().take(); if (!isFileComponentReady(next)) { updateFileComponentQueueAndWait(next); continue; } if (isTimerReady(next.getAction()) && isExecuteSlotFree()) { removeFromDeleted(next); removeFromCreated(next); removeFromFailed(next.getPath()); logger.debug("Start execution: {}", next.getPath()); ExecutionHandle ehandle = next.getAction().execute( fileManager); if (waitForActionCompletion) { if (ehandle != null && ehandle.getProcessHandle() != null) { logger.debug("Put into async handles!"); asyncHandles.put(ehandle); FileInfo file = new FileInfo(next); publishMessage(new FileExecutionStartedMessage( file, next.getAction().getCurrentState() .getStateType())); } else { // This happens with actions in // InitialState/EstablishedState FileInfo file = new FileInfo(next); publishMessage(new FileExecutionSucceededMessage( file, next.getAction().getCurrentState() .getStateType())); } if (asyncHandles.size() != 0) { publishMessage(new SynchronizationStartsNotification()); } } else { onActionExecuteSucceeded(next.getAction()); } } else { if (!isExecuteSlotFree()) { logger.debug("All slots used! Current jobs: "); logRunningJobs(); } fileEventManager.getFileComponentQueue().add(next); wait(calculateWaitTime(next)); } } catch (InterruptedException iex) { // happens if interrupted during blocking wait on queue logger.error("Thead interrup occurred: {}", iex.getMessage(), iex); return; } catch (NoSessionException nse) { logger.warn("No session - cannot execute pending actions.", nse); } catch (NoPeerConnectionException npc) { logger.warn( "No peer connection - cannot execute pending actions.", npc); } catch (Exception e) { logger.error("Exception occurred: {}", e.getMessage(), e); } } } private void updateFileComponentQueueAndWait(FileComponent next) throws InterruptedException { logger.debug("Component {} is not ready yet!", next.getPath()); fileEventManager.getFileComponentQueue().remove(next); next.getAction().updateTimestamp(); fileEventManager.getFileComponentQueue().add(next); wait(calculateWaitTime(next)); } private boolean isFileComponentReady(FileComponent next) { return next.isReady(); } /** * Checks whether an action is ready to be executed * * @param action * Action to be executed * @return true if ready to be executed, false otherwise */ private boolean isTimerReady(IAction action) { long ageMs = getActionAge(action); if (action.getCurrentState().getStateType() == StateType.LOCAL_CREATE) { return ageMs >= peerWaspConfig.getLongAggregationIntervalInMillis(); } else { return ageMs >= peerWaspConfig.getAggregationIntervalInMillis(); } } /** * Computes the age of an action * * @param action * @return age in ms */ private long getActionAge(IAction action) { return System.currentTimeMillis() - action.getTimestamp(); } private long calculateWaitTime(FileComponent action) { long timeToWait = peerWaspConfig.getAggregationIntervalInMillis() - getActionAge(action.getAction()) + 1L; if (timeToWait < 500L) { // wait at least some time timeToWait = 500L; } return timeToWait; } private boolean isExecuteSlotFree() { return asyncHandles.size() < peerWaspConfig.getNumberOfExecutionSlots(); } public void setWaitForActionCompletion(boolean wait) { this.waitForActionCompletion = wait; } public BlockingQueue<ExecutionHandle> getRunningJobs() { return asyncHandles; } public IPeerWaspConfig getPeerWaspConfig() { return peerWaspConfig; } /** * @return the file tree */ private IFileTree getFileTree() { return fileEventManager.getFileTree(); } private void logRunningJobs() { Iterator<ExecutionHandle> it = asyncHandles.iterator(); int index = 0; while (it.hasNext()) { ExecutionHandle next = it.next(); IAction tmpAction = next.getAction(); logger.trace("[{}] {} - {}", index, tmpAction.getCurrentStateName(), tmpAction.getFile() .getPath()); ++index; } } private void removeFromDeleted(FileComponent next) { SetMultimap<String, FileComponent> deletedByContent = getFileTree() .getDeletedByContentHash(); SetMultimap<String, FolderComposite> deletedByStructure = getFileTree() .getDeletedByStructureHash(); removeComponentFromSetMultimap(next, deletedByContent, deletedByStructure); } private void removeFromCreated(FileComponent next) { SetMultimap<String, FileComponent> createdByContent = getFileTree() .getCreatedByContentHash(); SetMultimap<String, FolderComposite> createdByStructure = getFileTree() .getCreatedByStructureHash(); removeComponentFromSetMultimap(next, createdByContent, createdByStructure); } private void removeFromFailed(Path failedOperation) { fileEventManager.getFailedOperations().remove(failedOperation); if (fileEventManager.getFailedOperations().size() == 0) { publishMessage(new SynchronizationErrorsResolvedNotification()); } } private void publishMessage(IMessage message) { if (fileEventManager.getMessageBus() != null) { fileEventManager.getMessageBus().publish(message); } } private void removeComponentFromSetMultimap(FileComponent toRemove, SetMultimap<String, FileComponent> byContent, SetMultimap<String, FolderComposite> byStructure) { Iterator<Map.Entry<String, FileComponent>> componentIterator = byContent .entries().iterator(); // .sameHashes.iterator(); if (toRemove.isFile()) { synchronized (byContent) { while (componentIterator.hasNext()) { FileComponent candidate = componentIterator.next() .getValue(); if (candidate.getPath().toString() .equals(toRemove.getPath().toString())) { componentIterator.remove(); break; } if (candidate.getAction().getCurrentState().getStateType() == StateType.LOCAL_CREATE) { if (System.currentTimeMillis() - candidate.getAction().getTimestamp() > peerWaspConfig .getLongAggregationIntervalInMillis()) { logger.trace("Remove old entry: {}", candidate.getPath()); componentIterator.remove(); } } else { if (System.currentTimeMillis() - candidate.getAction().getTimestamp() > peerWaspConfig .getAggregationIntervalInMillis()) { logger.trace("Remove old entry: {}", candidate.getPath()); componentIterator.remove(); } } } } } else { Iterator<Map.Entry<String, FolderComposite>> folderIterator = byStructure .entries().iterator(); synchronized (byStructure) { while (folderIterator.hasNext()) { Map.Entry<String, FolderComposite> candidate = folderIterator .next(); if (candidate.getValue().getPath().toString() .equals(toRemove.getPath().toString())) { folderIterator.remove(); break; } } } } } private void onActionExecuteSucceeded(final IAction action) { final FileComponent file = action.getFile(); logger.debug("Action succeeded: {} {}.", file.getPath(), action.getCurrentStateName()); // inform GUI to adjust icon FileInfo fileHelper = new FileInfo(file); if (action.getCurrentState().getStateType() == StateType.LOCAL_MOVE) { LocalMoveState state = (LocalMoveState) action.getCurrentState(); FileInfo source = new FileInfo(state.getSourcePath(), file.isFolder()); publishMessage(new FileExecutionSucceededMessage(source, fileHelper, action.getCurrentState().getStateType())); } else { publishMessage(new FileExecutionSucceededMessage(fileHelper, action .getCurrentState().getStateType())); } boolean changedWhileExecuted = action.getChangedWhileExecuted(); if(action.getCurrentState().getStateType() != StateType.LOCAL_HARD_DELETE){ file.setIsUploaded(true); } else { file.setIsUploaded(false); } action.onSucceeded(); if (changedWhileExecuted) { logger.trace( "File: {} changed during the execution process to state {}. " + "Put back into the queue", file.getPath(), action.getCurrentStateName()); action.updateTimestamp(); fileEventManager.getFileComponentQueue().add(file); } } private void handleExecutionError(IAction action, ProcessExecutionException pex) { final FileComponent file = action.getFile(); logger.error("Action failed: {}", file.getPath(), pex); action.onFailed(); boolean errorHandled = false; if (pex != null && pex.getCause() != null) { if (pex.getCause() instanceof Hive2HiveException) { Hive2HiveException h2hex = (Hive2HiveException) pex.getCause(); if (h2hex.getError() != null) { ErrorCode error = h2hex.getError(); errorHandled = handleErrorByCode(action, error); } } } if (!errorHandled) { handleErrorDefault(action); } } private void handleErrorDefault(IAction action) { final Path path = action.getFile().getPath(); logger.trace( "Default Error Handling: Re-initiate execution - {} - {} - attempt({}).", path, action.getCurrentStateName(), action.getExecutionAttempts()); if (action.getExecutionAttempts() <= peerWaspConfig .getMaximalExecutionAttempts()) { action.updateTimestamp(); fileEventManager.getFileComponentQueue().add(action.getFile()); } else { FileInfo file = new FileInfo(action.getFile()); publishMessage(new FileExecutionFailedMessage(file)); fileEventManager .getMessageBus() .post(new InformationNotification("Synchronization error ", "Operation on " + path + " failed")).now(); logger.error( "To many attempts, action of {} has not been executed again.", path); onActionExecuteSucceeded(action); fileEventManager.getFailedOperations().add( action.getFile().getPath()); fileEventManager.initiateForceSync(action.getFile().getPath() .getParent()); } } private boolean handleErrorByCode(IAction action, ErrorCode error) { final Path path = action.getFile().getPath(); if (error == AbortModificationCode.SAME_CONTENT) { logger.debug( "Update of file {} failed, content hash did not change", path); FileComponent notModified = getFileTree().getFile(path); if (notModified == null) { logger.trace("FileComponent not found (null): {}", path); } FileInfo file = new FileInfo(action.getFile()); publishMessage(new FileExecutionSucceededMessage(file, action .getCurrentState().getStateType())); action.onSucceeded(); return true; } else if (error == AbortModificationCode.FOLDER_UPDATE) { logger.debug( "Attempt to update folder {} failed as folder cannot be updated.", path); return true; } else if (error == AbortModificationCode.ROOT_DELETE_ATTEMPT) { logger.debug( "Attempt to delete the root folder {} failed (operation not allowed)", path); return true; } else if (error == AbortModificationCode.NO_WRITE_PERM) { // This happens when a user creates a file in a read-only folder. logger.debug( "Attempt to delete or write to {} failed. No write-permissions.", path); return true; } return false; // error not handled } @Handler public void onForceSync(ForceSyncMessage message) { logger.trace("Force Synchronization on {}: Handle ongoing executions"); setForceSyncRunning(true); } @Handler public void onForceSyncComplete(ForceSyncCompleteMessage message) { logger.trace("Forced synchronization terminated."); setForceSyncRunning(false); } public void setForceSyncRunning(boolean isRunning) { forceSyncRunning = isRunning; } private class ExecutingActionHandler implements Runnable { @Override public void run() { processExecutingActions(); } private void processExecutingActions() { while (true) { try { // did someone call stop()? return if so. if (Thread.currentThread().isInterrupted()) { return; } if(asyncHandles.isEmpty()){ publishMessage(new SynchronizationCompleteNotification()); } ExecutionHandle next = asyncHandles.take(); // if (next.getAction().getFile().getParent() == null) { // logger.trace( // "File {} is not attached to the filetree anymore. Ignore response from network.", // next.getAction().getFile().getPath()); // continue; // } try { if (forceSyncRunning) { logger.trace( "FileComponent {} in state {} is discarded due to force sync!", next.getAction().getFile().getPath(), next .getAction().getCurrentState() .getStateType()); continue; } // check whether there is a process attached ProcessHandle<Void> process = next.getProcessHandle(); if (process != null) { process.getFuture().get(5, TimeUnit.SECONDS); } onActionExecuteSucceeded(next.getAction()); if (asyncHandles.size() == 0) { publishMessage(new SynchronizationCompleteNotification()); } next.setTimeouts(0); } catch (ExecutionException eex) { ProcessExecutionException pex = null; if (eex.getCause() instanceof ProcessExecutionException) { pex = (ProcessExecutionException) eex.getCause(); } handleExecutionError(next.getAction(), pex); next.setTimeouts(0); } catch (CancellationException | InterruptedException e) { logger.warn( "Exception while getting future result: {}", e.getMessage()); } catch (TimeoutException tex) { logger.debug( "Could not get result of failed item, timed out. {}", next.getAction().getFile().getPath()); // add it again and try later if (next.getTimeouts() < 10) { next.incrementTimeouts(); asyncHandles.put(next); } else { fileEventManager.getFailedOperations().add( next.getAction().getFile().getPath()); fileEventManager.initiateForceSync(next.getAction() .getFile().getPath().getParent()); } } } catch (InterruptedException iex) { // happens if stop() is called and waiting thread is interrupted logger.warn("ExecutingActionHandler interruped."); return; } catch (Exception e) { logger.warn("Exception in processFailedActions: ", e); } } } } }