com.sworddance.taskcontrol.TaskControl.java Source code

Java tutorial

Introduction

Here is the source code for com.sworddance.taskcontrol.TaskControl.java

Source

/*
 * 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 com.sworddance.taskcontrol;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import static java.util.concurrent.TimeUnit.*;

import org.apache.commons.logging.Log;

import com.sworddance.util.ApplicationIllegalArgumentException;

/**
 * manages a collection of {@link TaskGroup}s that contain tasks that need to
 * be executed. Currently only 1 TaskGroup can be assigned. this will change in
 * the near future.
 *
 *
 */
public class TaskControl implements Runnable {
    private final ThreadFactory threadFactory;

    private final BlockingQueue<PrioritizedTask> eligibleTasks;

    private final ThreadPoolExecutor executor;

    /**
     * Lock used to signal when new jobs are ready
     * or the TaskControl's state has changed, for example, it is being shutdown.
     */
    private final Lock stateChangeNotificator;

    private final Condition newTasks;

    private boolean dumpTaskGroupStats;
    private Runnable processQueue = new ProcessQueue();

    private AtomicInteger currentTaskGroup = new AtomicInteger(0);

    private CountDownLatch shutDown = new CountDownLatch(1);

    /*
     * task groups that have jobs that should be chosen from
     */
    private List<TaskGroup<?>> taskGroups = new CopyOnWriteArrayList<TaskGroup<?>>();

    private List<PrioritizedTask> runningTaskList = new CopyOnWriteArrayList<PrioritizedTask>();

    // only needed because no way of finding out from PooledExecutor if there
    // are jobs still running
    private final AtomicInteger runningTasks;

    private Log log;

    private boolean privateThreadFactory;

    @SuppressWarnings("unchecked")
    public TaskControl(Comparator<PrioritizedTask> activeComparator, int maxThreads, ThreadFactory threadFactory,
            Log log) {
        this.log = log;
        ApplicationIllegalArgumentException.notNull(activeComparator, "activeComparator");
        this.eligibleTasks = new PriorityBlockingQueue<PrioritizedTask>(20, activeComparator);
        this.stateChangeNotificator = new ReentrantLock();
        this.newTasks = this.stateChangeNotificator.newCondition();
        this.runningTasks = new AtomicInteger(0);
        this.threadFactory = threadFactory;
        int keepAliveTime = 10;

        int corePoolSize = 1;
        this.executor = new ThreadPoolExecutor(corePoolSize, Math.max(corePoolSize, maxThreads), keepAliveTime,
                MICROSECONDS, (BlockingQueue) this.eligibleTasks, threadFactory);
        this.stayActive = true;
    }

    public TaskControl(Comparator<PrioritizedTask> activeComparator, int maxThreads, Log log) {
        this(activeComparator, maxThreads, new ThreadFactoryImpl(), log);
        this.privateThreadFactory = true;
    }

    public TaskControl(int maxThreads, Log log) {
        this(new PriorityEligibleWorkItemComparator(), maxThreads, log);
    }

    public TaskControl(Log log) {
        this(5, log);
    }

    @SuppressWarnings("unchecked")
    public TaskGroup<?> newTaskGroup(String name) {
        TaskGroup<?> taskGroup = new TaskGroup(name);
        taskGroup.setLog(this.getLog());
        taskGroup.setTaskControl(this);
        return taskGroup;
    }

    /**
     * Add {@link TaskGroup} to TaskControl. If there are no tasks in the TaskGroup, the TaskGroup will
     * immediately have it's result set to null and not actually be added.
     * @param taskGroup
     */
    public void addTaskGroup(TaskGroup<?> taskGroup) {
        if (!taskGroup.prepareToRun()) {
            return;
        }
        taskGroup.setTaskControl(this);
        this.taskGroups.add(taskGroup);
        this.stateChanged();
    }

    /**
     * entered by external thread to notify the TaskControl of taskCompletion.
     * @param task
     *
     */
    private void taskComplete(PrioritizedTask task) {
        if (!this.runningTaskList.remove(task)) {
            this.getLog().debug("removing task that was not on running list");
        }
        this.runningTasks.decrementAndGet();
        this.stateChanged();
    }

    /**
     * Used to notify TaskControl that it should wake up because there may be
     * changes to the state of things. This can happen if TaskGroups get new
     * tasks added to them.
     *
     */
    public void stateChanged() {
        this.stateChangeNotificator.lock();
        try {
            this.newTasks.signal();
        } finally {
            this.stateChangeNotificator.unlock();
        }
    }

    public void run() {
        try {
            this.prepareToRun();
            while (this.stillRunning()) {
                this.stateChangeNotificator.lock();
                try {
                    if (!this.isTaskReady()) {
                        if (!this.newTasks.await(60, SECONDS)) {
                            continue;
                        }
                    }
                } finally {
                    this.stateChangeNotificator.unlock();
                }
                this.processQueue.run();
            }
        } catch (Exception e) {
            this.getLog().warn("TaskControl ending with exception", e);
        } finally {
            this.shutdownNow();
        }
    }

    /**
     * all tasks should be done at this point, but if they are not they probably
     * should be killed.
     */
    public void shutdownNow() {
        this.shutDown.countDown();
        // do graceful shutdown... otherwise there is a slight race condition
        // with the Worker threads as they wrap up processing the last of the
        // jobs. (case: jobs has decremented runningTasks, but worker thread not
        // yet complete.) Don't want to use shutdownNow because that does
        // interrupt

        this.executor.shutdown();
        try {
            // executor.awaitTerminationAfterShutdown() - don't use race
            // condition where
            // poolsize ended up == 0 but still in wait
            this.executor.awaitTermination(1000L, MILLISECONDS);
        } catch (InterruptedException e) {
        }
        if (this.privateThreadFactory) {
            ((ThreadFactoryImpl) this.threadFactory).shutDownNow();
        }
        for (TaskGroup<?> taskGroup : this.taskGroups) {
            taskGroup.shutdownNow(this.dumpTaskGroupStats);
        }
        // if calling from outside - wake up main thread.
        this.stateChanged();
    }

    protected void prepareToRun() {
    }

    /**
     * Determine if there are any tasks that need to be moved from the possible
     * Queue to the eligibleTask queue. It is possible that there are possible
     * jobs but no eligible jobs.
     *
     * Note the deadlock potential here as this method is called from within 2
     * other sync blocks Don't make public and monitor usage.
     * TODO race conditions if adding taskGroups
     * @return true if any possible task can be run.
     */
    private boolean isTaskReady() {
        TaskGroup<?> group = this.getCurrentTaskGroup();
        if (group != null && group.isTaskReady()) {
            return true;
        }
        int originalIndex = this.currentTaskGroup.get();
        ArrayList<TaskGroup<?>> copy = new ArrayList<TaskGroup<?>>(this.taskGroups);

        for (int index = originalIndex + 1; index < copy.size(); index++) {
            group = copy.get(index);
            if (group != null && group.isTaskReady()) {
                this.currentTaskGroup.compareAndSet(originalIndex, index);
                return true;
            }
        }
        for (int index = 0; index < originalIndex; index++) {
            group = copy.get(index);
            if (group != null && group.isTaskReady()) {
                this.currentTaskGroup.compareAndSet(originalIndex, index);
                return true;
            }
        }
        return false;
    }

    private TaskGroup<?> getCurrentTaskGroup() {
        TaskGroup<?> taskGroup;
        do {
            int index = this.currentTaskGroup.get();
            try {
                taskGroup = this.taskGroups.get(index);
                return taskGroup;
            } catch (IndexOutOfBoundsException e) {
                if (index == 0) {
                    // no taskGroups left
                    return null;
                } else {
                    this.currentTaskGroup.set(0);
                    continue;
                }
            }
        } while (true);
    }

    public void setStayActive(boolean stayActive) {
        this.stayActive = stayActive;
        this.stateChanged();
    }

    public boolean isStayActive() {
        return this.stayActive;
    }

    // variables for debugging hung TaskControl
    private int lastRunningTaskSize = Integer.MIN_VALUE;

    private int lastEligibleTasksSize = Integer.MIN_VALUE;

    private boolean lastIsTaskNOTReady;

    /**
     * do not automatically shut down if there are no jobs.
     */
    private boolean stayActive;

    /**
     * if the thread has not been interrupted and the eligibleTask list is not
     * empty or there are possible jobs to move from to the eligibleTask queue
     * then keep running.
     *
     * @return
     * @throws InterruptedException
     */
    private boolean stillRunning() {
        try {
            if (Thread.currentThread().isInterrupted() || this.shutDown.await(0, MILLISECONDS)) {
                this.getLog().debug("TaskControl interrupted");
                return false;
            }
        } catch (InterruptedException e) {
            return false;
        }
        if (!this.stayActive) {
            // make sure that the only eligible task isn't being passed to a worker
            // thread when there are no other running tasks
            this.lastRunningTaskSize = this.runningTasks.get();
            this.lastEligibleTasksSize = this.eligibleTasks.size();
            this.lastIsTaskNOTReady = !this.isTaskReady();
            if (this.lastRunningTaskSize == 0 && this.lastEligibleTasksSize == 0 && this.lastIsTaskNOTReady) {
                this.getLog().debug("TaskControl ending -- nothing left to run");
                return false;
            }
        }
        return true;
    }

    public void setLog(Log log) {
        this.log = log;
    }

    public Log getLog() {
        return this.log;
    }

    /**
     * @param dumpTaskGroupStats the dumpTaskGroupStats to set
     */
    public void setDumpTaskGroupStats(boolean dumpTaskGroupStats) {
        this.dumpTaskGroupStats = dumpTaskGroupStats;
    }

    /**
     * @return the dumpTaskGroupStats
     */
    public boolean isDumpTaskGroupStats() {
        return this.dumpTaskGroupStats;
    }

    /**
     * this class actually moves the eligible tasks from the eligible
     * list to the {@link PriorityBlockingQueue} the the current task executor
     * reads from to pick up new tasks to execute.
     * @author Patrick Moore
     */
    private class ProcessQueue implements Runnable {
        /**
         * Called from when new possible tasks are added or when a running task
         * completes its execution. Responsible for determining which of the
         * possible tasks should be transfered to the eligibleTasks queue that
         * is used by the PooledExecutor to chose the next task.
         */
        public void run() {
            while (TaskControl.this.isTaskReady()) {
                // note that there is no guarentee that the task is the same as
                // this one
                final PrioritizedTask nextTask = this.nextTaskFromCurrentGroup();

                // verify that we have retrieved a valid object.
                if (!nextTask.isReadyToRun()) {
                    throw new RuntimeException("PrioritizedTask contract violation, "
                            + "or Comparator violation - why is this object not ready to run");
                }
                // wrap so that there is no reliance on the task doing the
                // correct notification.
                TaskControl.this.executor.execute(new TaskWrapper(nextTask) {
                    @Override
                    public void run() {
                        try {
                            this.getWrappedTask().run();
                        } finally {
                            TaskControl.this.taskComplete(this.getWrappedTask());
                        }
                    }
                });
                // we tell the executor to create additional threads because
                // otherwise
                // if min>#threads>max and a thread is waiting additional
                // threads will
                // not be created even if that means that there are waiting
                // tasks to be
                // run (12/07/2005).
                TaskControl.this.executor.prestartCoreThread();
                /*
                 * Incrementing runningTasks must occur here, not by a call from
                 * nextTask.run(). The reason is that otherwise there is a race
                 * condition: 1. eligibleTasks isEmpty 2. nextTask added to
                 * eligibleTasks 3. the worker thread pulls off nextTask from
                 * eligibleTasks (eligibleTasks empty again) 4.
                 * TaskControl.stillRunning executes and determines it should
                 * shutdown because the eligibleTask queue is empty and there
                 * are no more tasks that can run. 5. worker thread tries to run
                 * nextTask.
                 */
                TaskControl.this.runningTasks.incrementAndGet();
                TaskControl.this.runningTaskList.add(nextTask);
            }
        }

        private PrioritizedTask nextTaskFromCurrentGroup() {
            PrioritizedTask nextTask = null;
            do {
                TaskGroup<?> taskGroup = TaskControl.this.getCurrentTaskGroup();
                nextTask = taskGroup.nextTask();
            } while (nextTask == null);
            TaskControl.this.currentTaskGroup.incrementAndGet();
            return nextTask;
        }
    }
}