/* ********************************************************************** **
** Copyright notice **
** **
** (c) 2005-2009 RSSOwl Development Team **
** http://www.rssowl.org/ **
** **
** All rights reserved **
** **
** This program and the accompanying materials are made available under **
** the terms of the Eclipse Public License v1.0 which accompanies this **
** distribution, and is available at: **
** http://www.rssowl.org/legal/epl-v10.html **
** **
** A copy is found in the file epl-v10.html and important notices to the **
** license from the team is found in the textfile LICENSE.txt distributed **
** in this package. **
** **
** This copyright notice MUST APPEAR in all copies of the file! **
** **
** Contributors: **
** RSSOwl Development Team - initial API and implementation **
** **
** ********************************************************************** */
package org.rssowl.core.util;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.core.runtime.QualifiedName;
import org.eclipse.core.runtime.SafeRunner;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.IJobChangeEvent;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.core.runtime.jobs.JobChangeAdapter;
import org.eclipse.osgi.util.NLS;
import org.rssowl.core.internal.Activator;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
/**
* This class allows to add <code>Runnables</code> into a Queue to process them
* in Jobs up to a certain amount of allowed parallel Jobs.
*
* @author bpasero
*/
public class JobQueue {
/* Helper for the Progress Monitor */
private static final double TOTAL_TASK_WORK_LOAD = 9900;
private static final double TOTAL_PROGRESS_WORK_LOAD = 10000;
/* Delay in ms for the Progress Job to update the Monitor */
private static final int PROGRESS_UPDATE_DELAY = 300;
/** This was copied from IProgressConstants to avoid UI dependancy */
public static final QualifiedName NO_IMMEDIATE_ERROR_PROMPT_PROPERTY = new QualifiedName("org.eclipse.ui.workbench.progress", "delayErrorPrompt"); //$NON-NLS-1$ //$NON-NLS-2$
private final Job fProgressJob;
private final int fMaxConcurrentJobs;
private final int fProgressDelay;
private final String fName;
private String fTaskPrefix;
private final boolean fShowProgress;
private boolean fIsUnknownProgress;
private final ListenerList fListeners = new ListenerList();
/* These fields are accessed from N Jobs concurrently */
private volatile boolean fProgressJobScheduled;
private volatile String fCurrentTask = ""; //$NON-NLS-1$
private final AtomicInteger fTotalWork = new AtomicInteger(0); // Number of Tasks in Total
private final AtomicInteger fWorkDone = new AtomicInteger(0); // Number of finished Tasks
private final AtomicInteger fProgressShown = new AtomicInteger(0); // Number of Progress Shown
private final AtomicInteger fProgressBuf = new AtomicInteger(0); // Buffer for the Progress Monitor
private final AtomicInteger fScheduledJobs = new AtomicInteger(0); // Count number of running Jobs
private final AtomicBoolean fIsSealed = new AtomicBoolean(false);
private final BlockingQueue<ITask> fOpenTasksQueue;
/**
* Creates an instance of <code>JobQueue</code> that allows to add
* <code>Runnables</code> into a Queue to process them in Jobs up to a certain
* amount of allowed parallel Jobs.
*
* @param name A human-readable name that is displayed in the Progress-View
* while the Queue is processed.
* @param maxConcurrentJobs The maximum number of concurrent running Tasks.
* @param maxQueueSize The maximum number of tasks that this queue will accept
* before blocking.
* @param showProgress If TRUE, show Progress of Jobs running from Queue.
* @param progressDelay The time in milliseconds to wait before showing any
* progress. This is useful in case the Tasks finish very quickly. Setting it
* to 0 will show Progress instantly with no delay.
*/
public JobQueue(String name, int maxConcurrentJobs, int maxQueueSize, boolean showProgress, int progressDelay) {
this(name, name, maxConcurrentJobs, maxQueueSize, showProgress, progressDelay);
}
/**
* Creates an instance of <code>JobQueue</code> that allows to add
* <code>Runnables</code> into a Queue to process them in Jobs up to a certain
* amount of allowed parallel Jobs.
*
* @param globalName A human-readable name that is displayed in the
* Progress-View while the Queue is processed.
* @param taskPrefix A human-readable prefix that is shown before the name of
* a task that is currently processed.
* @param maxConcurrentJobs The maximum number of concurrent running Tasks.
* @param maxQueueSize The maximum number of tasks that this queue will accept
* before blocking.
* @param showProgress If TRUE, show Progress of Jobs running from Queue.
* @param progressDelay The time in milliseconds to wait before showing any
* progress. This is useful in case the Tasks finish very quickly. Setting it
* to 0 will show Progress instantly with no delay.
*/
public JobQueue(String globalName, String taskPrefix, int maxConcurrentJobs, int maxQueueSize, boolean showProgress, int progressDelay) {
Assert.isNotNull(globalName);
Assert.isNotNull(taskPrefix);
Assert.isLegal(progressDelay >= 0, "JobQueue Progress delay is negative"); //$NON-NLS-1$
fName = globalName;
fTaskPrefix = taskPrefix;
fMaxConcurrentJobs = maxConcurrentJobs;
fShowProgress = showProgress;
fProgressDelay = progressDelay;
fOpenTasksQueue = new LinkedBlockingQueue<ITask>(maxQueueSize);
/* Eagerly create the Progress-Job if we need one */
if (showProgress)
fProgressJob = createProgressJob();
else
fProgressJob = null;
}
/**
* @param isUnknownProgress Sets the progress reporting of the progress Job
* used for this {@link JobQueue} to {@link IProgressMonitor#UNKNOWN} if
* <code>true</code>.
*/
public void setUnknownProgress(boolean isUnknownProgress) {
fIsUnknownProgress = isUnknownProgress;
}
/**
* Cancels all Jobs that belong to this Queue. Optionally the caller may
* decide to join the running Jobs that are not yet done. Note that this will
* <em>block</em> the calling Thread until all running Tasks have finished so
* this should only be considered for <em>short-running</em> Tasks.
*
* @param joinRunning If <code>TRUE</code>, join the running Jobs that are not
* yet done.
* @param seal if <code>true</code> this queue is sealed and no tasks will
* ever be scheduled anymore.
*/
public void cancel(boolean joinRunning, boolean seal) {
synchronized (this) {
/* Seal */
if (seal)
seal();
/* Clear open tasks */
fOpenTasksQueue.clear();
/* Cancel scheduled Jobs */
Job.getJobManager().cancel(this);
/* Cancel Progress Job */
if (fProgressJob != null)
fProgressJob.cancel();
}
/* Join running Jobs if any */
if (joinRunning) {
while (Job.getJobManager().find(this).length != 0) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
break;
}
}
}
}
/**
* Seals this queue so that no task can be added anymore.
*/
public void seal() {
fIsSealed.set(true);
}
/**
* Determines whether the given Task is already queued in this Queue. That is,
* the Task is scheduled and did not yet run to completion.
*
* @param task The Task to check for being queued in this Queue.
* @return <code>TRUE</code> in case the given Task is already queued in this
* Queue, meaning that it has been scheduled but did not yet complete
* execution, <code>FALSE</code> otherwise.
*/
public boolean isQueued(ITask task) {
return fOpenTasksQueue.contains(task);
}
/**
* Adds the given Task into the Queue waiting if necessary for space to become
* available. The Task is processed in a <code>Job</code> once the number of
* parallel processed Tasks is below <code>MAX_SCHEDULED_JOBS</code>.
*
* @param task The Task to add into this Queue.
* @return {@code true} if all the tasks were scheduled or {@code false} if
* some tasks were not scheduled because the current thread was interrupted.
*/
public boolean schedule(ITask task) {
return schedule(Collections.singletonList(task));
}
/**
* Adds the given List of Tasks into the Queue waiting is necessary for space
* to become available. Each Runnable is processed in a <code>Job</code> once
* the number of parallel processed Tasks is below
* <code>MAX_SCHEDULED_JOBS</code>.
*
* @param tasks The Tasks to add into this Queue.
* @return {@code true} if all the tasks were scheduled or {@code false} if
* some tasks were not scheduled because the current thread was interrupted or
* this queue is sealed.
*/
public boolean schedule(List<ITask> tasks) {
final int tasksSize = tasks.size();
/* Ignore empty lists */
if (tasksSize == 0)
return true;
/* Return if Queue is Sealed */
if (fIsSealed.get())
return false;
/* Add into List of open tasks */
for (ITask task : tasks) {
try {
fOpenTasksQueue.put(task);
/* Adjust Total Work Counter */
fTotalWork.incrementAndGet();
} catch (InterruptedException e) {
return false;
}
}
/* Schedule Job if not yet done */
if (!fProgressJobScheduled && fShowProgress) {
fProgressJobScheduled = true;
fProgressJob.schedule(fProgressDelay);
}
/* Optimisation: We are able to release the calling thread without locking. */
if (fScheduledJobs.get() >= fMaxConcurrentJobs)
return true;
/* Start a new Job for each free Slot */
for (int i = 0; i < tasksSize && !fOpenTasksQueue.isEmpty(); ++i) {
/* Never exceed max number of allowed concurrent Jobs */
if (fScheduledJobs.incrementAndGet() > fMaxConcurrentJobs) {
fScheduledJobs.decrementAndGet();
break;
}
/* Create the Job */
Job job = createJob();
/* Listen to Job's Lifecycle */
job.addJobChangeListener(new JobChangeAdapter() {
/* Update Fields when a Job is Done */
@Override
public void done(IJobChangeEvent event) {
/* Re-Schedule this Job if there is work left to do */
if (!fOpenTasksQueue.isEmpty())
event.getJob().schedule();
else
fScheduledJobs.decrementAndGet();
}
});
/* Do not interrupt on any Error */
job.setProperty(NO_IMMEDIATE_ERROR_PROMPT_PROPERTY, Boolean.TRUE);
/*
* Workaround: Since we are using our own Job for displaying Progress, we
* don't want these Jobs show up in the Progress View. There is currently
* no bug-free solution of aggregating the Progress of N Jobs into a
* single Monitor.
*/
job.setSystem(true);
/* Schedule it immediately */
job.schedule();
}
return true;
}
/* Create a Job for a Task to handle */
private Job createJob() {
Job job = new Job("") { //$NON-NLS-1$
@Override
protected IStatus run(final IProgressMonitor monitor) {
/* Poll the next Task */
final ITask task = fOpenTasksQueue.poll();
/* Queue is empty - so all work is done */
if (task == null)
return Status.OK_STATUS;
/* Perform the Operation if not yet Cancelled */
if (!monitor.isCanceled()) {
SafeRunner.run(new LoggingSafeRunnable() {
public void run() throws Exception {
fCurrentTask = task.getName();
IStatus status = task.run(monitor);
/* Log anything that is an Error or Warning */
if (status.getSeverity() == IStatus.ERROR || status.getSeverity() == IStatus.WARNING) {
if (Activator.getDefault() != null)
Activator.getDefault().getLog().log(status);
}
}
});
/* Update Work Fields if not cancelled meanwhile */
if (!monitor.isCanceled()) {
fWorkDone.incrementAndGet();
/* Calculate the Progress */
int workDifference = fTotalWork.get() - fWorkDone.get();
if (workDifference > 0 && fProgressJobScheduled) {
int progress = (int) ((TOTAL_TASK_WORK_LOAD - fProgressShown.get()) / workDifference);
fProgressShown.addAndGet(progress);
fProgressBuf.addAndGet(progress);
}
}
}
/* Inform about cancelation if present */
return monitor.isCanceled() ? Status.CANCEL_STATUS : Status.OK_STATUS;
}
@Override
public boolean belongsTo(Object family) {
return family == JobQueue.this;
}
};
return job;
}
/**
* Returns <code>TRUE</code> in case the JobQueue has finished all open Tasks.
*
* @return <code>TRUE</code> in case the JobQueue has finished all open Tasks,
* <code>FALSE</code> otherwise.
*/
public synchronized boolean isEmpty() {
return internalIsEmpty();
}
private boolean internalIsEmpty() {
return fTotalWork.get() - fWorkDone.get() == 0;
}
/**
* @param listener The Listener to add to the List of Listeners.
*/
public void addJobQueueListener(JobQueueListener listener) {
fListeners.add(listener);
}
/**
* @param listener The Listener to remove from the List of Listeners.
*/
public void removeJobQueueListener(JobQueueListener listener) {
fListeners.remove(listener);
}
/* Creates the Job for displaying Progress while Tasks are processed */
private Job createProgressJob() {
return new Job(fName) {
private int fLastWorkDone = -1;
private String fLastTask;
@Override
protected IStatus run(IProgressMonitor monitor) {
boolean interrupted = false;
/* Indicate Beginning if there is still work to do */
if (!internalIsEmpty())
monitor.beginTask(fName, fIsUnknownProgress ? IProgressMonitor.UNKNOWN : (int) TOTAL_PROGRESS_WORK_LOAD);
/* Update Progress while not Cancelled and not Done */
while (!monitor.isCanceled() && !internalIsEmpty()) {
/* Update the Task Label if there was an Update */
if (fCurrentTask != null && ((fLastWorkDone != fWorkDone.get()) || !StringUtils.isSet(fLastTask))) {
fLastWorkDone = fWorkDone.get();
fLastTask = fCurrentTask;
monitor.setTaskName(formatTask());
}
/* Increment Monitor Progress */
if (fProgressBuf.get() > 0) {
monitor.worked(fProgressBuf.get());
fProgressBuf.set(0);
}
try {
Thread.sleep(PROGRESS_UPDATE_DELAY);
} catch (InterruptedException e) {
interrupted = true;
break;
}
}
/* Always call done() even if canceled */
monitor.done();
/* Task Processing has been canceled */
if (monitor.isCanceled())
Job.getJobManager().cancel(JobQueue.this);
/* Be ready for the next Tasks */
synchronized (JobQueue.this) {
reset();
}
fLastWorkDone = -1;
fLastTask = null;
notifyWorkDone();
/* Inform about cancellation if present */
if (monitor.isCanceled() || interrupted)
return Status.CANCEL_STATUS;
return Status.OK_STATUS;
}
@Override
public boolean belongsTo(Object family) {
return family == JobQueue.this;
}
};
}
private void notifyWorkDone() {
Object listeners[] = fListeners.getListeners();
for (Object element : listeners) {
final JobQueueListener listener = (JobQueueListener) element;
SafeRunner.run(new LoggingSafeRunnable() {
public void run() throws Exception {
listener.workDone();
}
});
}
}
private String formatTask() {
int workDone = fWorkDone.get();
Object[] bindings = Arrays.asList(fTaskPrefix, String.valueOf((workDone != 0 ? workDone : 1)), String.valueOf(fTotalWork.get()), fCurrentTask.replaceAll("&", "&&")).toArray(); //$NON-NLS-1$//$NON-NLS-2$
String str = NLS.bind(Messages.JobQueue_TASK_NAME, bindings);
return str;
}
/* Reset fields and cancel all Jobs of this Family */
private void reset() {
fScheduledJobs.set(0);
fProgressJobScheduled = false;
fWorkDone.set(0);
fTotalWork.set(0);
fProgressBuf.set(0);
fProgressShown.set(0);
fCurrentTask = ""; //$NON-NLS-1$
fOpenTasksQueue.clear();
}
}