/* * sulky-modules - several general-purpose modules. * Copyright (C) 2007-2015 Joern Huxhorn * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ /* * Copyright 2007-2015 Joern Huxhorn * * Licensed 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 de.huxhorn.sulky.tasks; import de.huxhorn.sulky.io.IOUtilities; import java.awt.EventQueue; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.locks.ReentrantReadWriteLock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * <p>A TaskManager is used to create Tasks for given Callables.</p> * * @param <T> the type of the result. */ //@ThreadSafe public class TaskManager<T> { /** * The states a TaskManager can be in. A TaskManager can't be restarted. */ public enum State { INITIALIZED, RUNNING, STOPPED } private final Logger logger = LoggerFactory.getLogger(TaskManager.class); private final ReentrantReadWriteLock tasksLock; private final ReentrantReadWriteLock taskListenersLock; private boolean usingEventQueue; private final ExecutorService executorService; //@GuardedBy("tasksLock") private final List<Task<T>> internalCreatedTasks; //@GuardedBy("tasksLock") private final Map<Long, Task<T>> tasks; //@GuardedBy("tasksLock") private final Map<Integer, Task<T>> callableTasks; //@GuardedBy("tasksLock") private final List<ProgressChange<T>> internalProgressChanges; //@GuardedBy("tasksLock") private long nextTaskId; //@GuardedBy("taskListenersLock") private final List<TaskListener<T>> taskListeners; private final PropertyChangeListener progressChangeListener; private Thread resultPollerThread; private State state; /** * Creates a new task manager with a cached thread pool. * * By default, it is not using the event dispatch thread to fire task events. */ public TaskManager() { this(Executors.newCachedThreadPool(), false); } /** * Creates a new task manager with the given executor service. * * By default, it is not using the event dispatch thread to fire task events. * * @param executorService the executor service to be used by this task manager. Must not be null. * @throws IllegalArgumentException if executorService is null. */ public TaskManager(ExecutorService executorService) { this(executorService, false); } /** * Creates a new task manager with the given executor service and the given us. * * @param executorService the executor service to be used by this task manager. Must not be null. * @param usingEventQueue whether or not the event dispatch thread should be used to fire task events. * @throws IllegalArgumentException if executorService is null. */ public TaskManager(ExecutorService executorService, boolean usingEventQueue) { if(executorService == null) { throw new IllegalArgumentException("executorService must not be null!"); } this.tasksLock = new ReentrantReadWriteLock(true); this.taskListenersLock = new ReentrantReadWriteLock(true); this.nextTaskId = 1; this.usingEventQueue = usingEventQueue; this.internalCreatedTasks = new ArrayList<>(); this.tasks = new HashMap<>(); this.callableTasks = new HashMap<>(); this.progressChangeListener = new ProgressChangeListener(); this.internalProgressChanges = new ArrayList<>(); this.taskListeners = new LinkedList<>(); this.executorService = executorService; this.state = State.INITIALIZED; } /** * Starts up this task manager. * * @throws IllegalStateException if the task manager was not INITIALIZED. */ public void startUp() { if(state == State.INITIALIZED) { resultPollerThread = new Thread(new TaskResultPoller(), "TaskResultPoller"); resultPollerThread.setDaemon(true); resultPollerThread.start(); state = State.RUNNING; } else { throw new IllegalStateException("You tried to start a task manager but it's state was " + state + " instead of INITIALIZED!"); } } /** * Shuts down this task manager including the used executor service. * This call is simply ignored if the task manager was not running. * No TaskListener calls will be executed after this method was executed. */ public void shutDown() { if(state == State.RUNNING) { state = State.STOPPED; resultPollerThread.interrupt(); executorService.shutdownNow(); } } /** * @return the state this task manager is in. */ public State getState() { return state; } /** * Starts a task. * * @param callable the callable that will be used to create a task. * @param name name of the task, need not be unique. * @return the started Task. * @throws IllegalStateException if the task manager is not running. * @throws IllegalArgumentException if the name is null or the Callable was already started. * @see de.huxhorn.sulky.tasks.Task * @see de.huxhorn.sulky.tasks.ProgressingCallable */ public Task<T> startTask(Callable<T> callable, String name) { return startTask(callable, name, null, null); } /** * Starts a task. * * @param callable the callable that will be used to create a task. * @param name name of the task, need not be unique. * @param description optional human-readable descriiption of what this task is about. * @return the started Task. * @throws IllegalStateException if the task manager is not running. * @throws IllegalArgumentException if the name is null or the Callable was already started. * @see de.huxhorn.sulky.tasks.Task * @see de.huxhorn.sulky.tasks.ProgressingCallable */ public Task<T> startTask(Callable<T> callable, String name, String description) { return startTask(callable, name, description, null); } /** * Starts a task. * * @param callable the callable that will be used to create a task. * @param name name of the task, need not be unique. * @param description optional human-readable descriiption of what this task is about. * @param metaData optional meta data to be associated with this task. * @return the started Task. * @throws IllegalStateException if the task manager is not running. * @throws IllegalArgumentException if the name is null or the Callable was already started. * @see de.huxhorn.sulky.tasks.Task * @see de.huxhorn.sulky.tasks.ProgressingCallable */ public Task<T> startTask(Callable<T> callable, String name, String description, Map<String, String> metaData) { if(state != State.RUNNING) { throw new IllegalStateException("You tried to start a task but the task managers state was " + state + " instead of RUNNING!"); } if(name == null) { throw new IllegalArgumentException("name must not be null!"); } if(callable instanceof ProgressingCallable) { ProgressingCallable pcallable = (ProgressingCallable) callable; pcallable.addPropertyChangeListener(progressChangeListener); if(logger.isDebugEnabled()) logger.debug("Added progress change listener to callable."); } int callableIdentity = System.identityHashCode(callable); Future<T> future = executorService.submit(callable); Task<T> task; ReentrantReadWriteLock.WriteLock lock = tasksLock.writeLock(); lock.lock(); try { if(callableTasks.containsKey(callableIdentity)) { throw new IllegalArgumentException("Callable is already scheduled!"); } long newId = nextTaskId++; task = new TaskImpl<>(newId, this, future, callable, name, description, metaData); internalCreatedTasks.add(task); tasks.put(newId, task); callableTasks.put(callableIdentity, task); } finally { lock.unlock(); } return task; } public int getNumberOfTasks() { ReentrantReadWriteLock.ReadLock lock = tasksLock.readLock(); lock.lock(); try { return tasks.size(); } finally { lock.unlock(); } } /** * Returns the Task associated with the Task ID. * * @param taskId the Task ID for which the Task should be resolved. * @return the Task associated with the Task ID. */ public Task<T> getTaskById(long taskId) { ReentrantReadWriteLock.ReadLock lock = tasksLock.readLock(); lock.lock(); try { return tasks.get(taskId); } finally { lock.unlock(); } } /** * Returns the Task associated with the given Callable. * * @param callable the Callable for which the Task should be resolved. * @return the Task associated with the given Callable. */ public Task<T> getTaskByCallable(Callable<T> callable) { int callableIdentity = System.identityHashCode(callable); ReentrantReadWriteLock.ReadLock lock = tasksLock.readLock(); lock.lock(); try { return callableTasks.get(callableIdentity); } finally { lock.unlock(); } } /** * Returns a Map containing all Tasks with their Task ID as key. * * @return a Map containing all Tasks with their Task ID as key. */ public Map<Long, Task<T>> getTasks() { Map<Long, Task<T>> result; ReentrantReadWriteLock.ReadLock lock = tasksLock.readLock(); lock.lock(); try { result = new HashMap<>(tasks); } finally { lock.unlock(); } return result; } /** * Returns true if this TaskManager calls TaskListeners on the event dispatcher thread, false otherwise. * * @return true if this TaskManager calls TaskListeners on the event dispatcher thread, false otherwise. */ public boolean isUsingEventQueue() { return usingEventQueue; } /** * Set whether or not this TaskManager calls TaskListeners on the event dispatcher thread. * * @param usingEventQueue whether or not this TaskManager calls TaskListeners on the event dispatcher thread. */ public void setUsingEventQueue(boolean usingEventQueue) { this.usingEventQueue = usingEventQueue; } private static class ProgressChange<V> { private final int progress; private final Task<V> task; ProgressChange(Task<V> task, int progress) { this.progress = progress; this.task = task; } int getProgress() { return progress; } Task<V> getTask() { return task; } } private class TaskResultPoller implements Runnable { private final Logger logger = LoggerFactory.getLogger(TaskResultPoller.class); private static final long POLL_INTERVAL = 200; public void run() { for(;;) { try { List<ProgressChange<T>> progressChanges = null; List<Task<T>> doneTasks = null; List<Task<T>> createdTasks = null; ReentrantReadWriteLock.WriteLock lock = tasksLock.writeLock(); lock.lock(); try { if(internalCreatedTasks.size() > 0) { createdTasks = new ArrayList<>(internalCreatedTasks); internalCreatedTasks.clear(); } if(internalProgressChanges.size() > 0) { progressChanges = new ArrayList<>(internalProgressChanges); internalProgressChanges.clear(); } for(Map.Entry<Long, Task<T>> entry : tasks.entrySet()) { Task<T> task = entry.getValue(); if(task.getFuture().isDone()) { if(doneTasks == null) { doneTasks = new ArrayList<>(); } doneTasks.add(task); } } if(doneTasks != null) { for(Task task : doneTasks) { int callableIdentity = System.identityHashCode(task.getCallable()); tasks.remove(task.getId()); callableTasks.remove(callableIdentity); } } } finally { lock.unlock(); } if(createdTasks != null || doneTasks != null || progressChanges != null) { ResultListenerFireRunnable runnable = new ResultListenerFireRunnable(createdTasks, doneTasks, progressChanges); if(usingEventQueue) { EventQueue.invokeLater(runnable); } else { runnable.run(); } } Thread.sleep(POLL_INTERVAL); } catch(InterruptedException e) { if(logger.isDebugEnabled()) logger.debug("Interrupted...", e); IOUtilities.interruptIfNecessary(e); break; } } } } /** * Adds the given TaskListener. * * @param listener the TaskListener to add. */ public void addTaskListener(TaskListener<T> listener) { ReentrantReadWriteLock.WriteLock lock = taskListenersLock.writeLock(); lock.lock(); try { taskListeners.add(listener); } finally { lock.unlock(); } } /** * Removes the given TaskListener. * * @param listener the TaskListener to remove. */ public void removeTaskListener(TaskListener<T> listener) { ReentrantReadWriteLock.WriteLock lock = taskListenersLock.writeLock(); lock.lock(); try { taskListeners.remove(listener); } finally { lock.unlock(); } } private class ResultListenerFireRunnable implements Runnable { private final Logger logger = LoggerFactory.getLogger(TaskManager.class); private List<Task<T>> createdTasks; private List<Task<T>> done; private List<TaskListener<T>> clonedListeners; private List<ProgressChange<T>> progressChanges; ResultListenerFireRunnable(List<Task<T>> createdTasks, List<Task<T>> done, List<ProgressChange<T>> progressChanges) { this.createdTasks = createdTasks; this.done = done; this.progressChanges = progressChanges; } public void run() { ReentrantReadWriteLock.ReadLock lock = taskListenersLock.readLock(); lock.lock(); try { this.clonedListeners = new ArrayList<>(taskListeners); } finally { lock.unlock(); } // fire creation of tasks before any other events if(createdTasks != null) { for(Task<T> current : createdTasks) { fireCreatedEvent(current); } } // then fire changes of progress before finish, cancel or fail if(progressChanges != null) { for(ProgressChange<T> current : progressChanges) { fireProgressEvent(current.getTask(), current.getProgress()); } } if(done != null) { for(Task<T> task : done) { Callable<T> callable = task.getCallable(); if(callable instanceof ProgressingCallable) { // remove propertyChangeListener... ProgressingCallable pc = (ProgressingCallable) callable; pc.removePropertyChangeListener(progressChangeListener); if(logger.isDebugEnabled()) logger.debug("Removed progress change listener from callable."); } Future<T> future = task.getFuture(); if(future.isCancelled()) { fireCanceledEvent(task); } else { // at this point we are sure that the future finished either // successfully or with an error. try { fireFinishedEvent(task, future.get()); } catch(InterruptedException e) { if(logger.isInfoEnabled()) logger.info("Interrupted...", e); IOUtilities.interruptIfNecessary(e); } catch(ExecutionException e) { fireExceptionEvent(task, e); } } } } } private void fireCreatedEvent(Task<T> task) { for(TaskListener<T> listener : clonedListeners) { try { listener.taskCreated(task); } catch(Throwable t) { if(logger.isErrorEnabled()) { logger .error("TaskListener " + listener + " threw an exception while progressUpdated was called!", t); } IOUtilities.interruptIfNecessary(t); } } } private void fireProgressEvent(Task<T> task, int progress) { for(TaskListener<T> listener : clonedListeners) { try { listener.progressUpdated(task, progress); } catch(Throwable t) { if(logger.isErrorEnabled()) { logger .error("TaskListener " + listener + " threw an exception while progressUpdated was called!", t); } IOUtilities.interruptIfNecessary(t); } } } private void fireExceptionEvent(Task<T> task, ExecutionException exception) { for(TaskListener<T> listener : clonedListeners) { try { listener.executionFailed(task, exception); } catch(Throwable t) { if(logger.isErrorEnabled()) { logger .error("TaskListener " + listener + " threw an exception while executionFailed was called!", t); } IOUtilities.interruptIfNecessary(t); } } } private void fireFinishedEvent(Task<T> task, T result) { for(TaskListener<T> listener : clonedListeners) { try { listener.executionFinished(task, result); } catch(Throwable t) { if(logger.isErrorEnabled()) { logger .error("TaskListener " + listener + " threw an exception while executionFinished was called!", t); } IOUtilities.interruptIfNecessary(t); } } } private void fireCanceledEvent(Task<T> task) { for(TaskListener<T> listener : clonedListeners) { try { listener.executionCanceled(task); } catch(Throwable t) { if(logger.isErrorEnabled()) { logger .error("TaskListener " + listener + " threw an exception while executionCanceled was called!", t); } IOUtilities.interruptIfNecessary(t); } } } } private class ProgressChangeListener implements PropertyChangeListener { public void propertyChange(PropertyChangeEvent evt) { Object source = evt.getSource(); Object newValue = evt.getNewValue(); if(source instanceof ProgressingCallable && newValue instanceof Integer && ProgressingCallable.PROGRESS_PROPERTY_NAME.equals(evt.getPropertyName())) { int progress = (Integer) newValue; int callableIdentity = System.identityHashCode(source); ReentrantReadWriteLock.WriteLock lock = tasksLock.writeLock(); // using write lock because internalProgressChanges is changed. lock.lock(); try { Task<T> task = callableTasks.get(callableIdentity); if(task != null) { internalProgressChanges.add(new ProgressChange<>(task, progress)); } } finally { lock.unlock(); } } } } //@Immutable private static final class TaskImpl<V> implements Task<V> { private final long id; private final TaskManager<V> taskManager; private final Future<V> future; private final Callable<V> callable; private final String name; private final String description; private final Map<String, String> metaData; TaskImpl(long id, TaskManager<V> taskManager, Future<V> future, Callable<V> callable, String name, String description, Map<String, String> metaData) { this.id = id; this.taskManager = taskManager; this.callable = callable; this.future = future; this.name = name; this.description = description; if(metaData != null) { this.metaData = new HashMap<>(metaData); } else { this.metaData = null; } } public long getId() { return id; } public String getName() { return name; } public String getDescription() { return description; } public Map<String, String> getMetaData() { if(metaData == null) { return null; } return Collections.unmodifiableMap(metaData); } public Future<V> getFuture() { return future; } public Callable<V> getCallable() { return callable; } public TaskManager<V> getTaskManager() { return taskManager; } public int getProgress() { if(callable instanceof ProgressingCallable) { ProgressingCallable pc = (ProgressingCallable) callable; return pc.getProgress(); } return -1; } @Override public boolean equals(Object o) { if(this == o) return true; if(!(o instanceof TaskImpl)) return false; TaskImpl<?> task = (TaskImpl<?>) o; if(id != task.id) return false; if(taskManager != task.getTaskManager()) return false; return true; } @Override public int hashCode() { return (int) id; } @Override public String toString() { StringBuilder result = new StringBuilder(); result.append("Task[id=").append(id) .append(", name=\"").append(name).append("\""); result.append(", progress="); int progress = getProgress(); if(progress < 0) { result.append("unknown"); } else { result.append(progress); } result.append("]"); return result.toString(); } } }