/* ********************************************************************** ** ** 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.ui.internal.util; import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; 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.ui.progress.IProgressConstants; import org.rssowl.core.util.ITask; import org.rssowl.core.util.LoggingSafeRunnable; import org.rssowl.ui.internal.Activator; import org.rssowl.ui.internal.services.DownloadService; import java.util.Collections; import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicInteger; /** * The {@link DownloadJobQueue} is used by the {@link DownloadService} to queue * requests for files to download. * * @author bpasero */ public class DownloadJobQueue { private final AtomicInteger fScheduledJobs = new AtomicInteger(0); // Count number of running Jobs private final BlockingQueue<DownloadTask> fOpenTasksQueue; private final int fMaxConcurrentJobs; private String fName; /** The Task used for Downloads */ public static abstract class DownloadTask implements ITask { /* * @see org.rssowl.core.util.ITask#run(org.eclipse.core.runtime.IProgressMonitor) */ public IStatus run(IProgressMonitor monitor) { return run(null, monitor); } /** * @param job the {@link Job} that is running this task. * @param monitor The provided monitor can be used to report progress and * respond to cancellation. If the progress monitor has been canceled, the * task should finish its execution at the earliest convenience and return a * result status of severity IStatus.CANCEL. * @return Returns the result of the operation as an instance of * <code>IStatus</code>. * @see org.rssowl.core.util.ITask#run(org.eclipse.core.runtime.IProgressMonitor) */ public abstract IStatus run(Job job, IProgressMonitor monitor); } /** * 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. */ public DownloadJobQueue(String name, int maxConcurrentJobs, int maxQueueSize) { Assert.isNotNull(name); fName = name; fMaxConcurrentJobs = maxConcurrentJobs; fOpenTasksQueue = new LinkedBlockingQueue<DownloadTask>(maxQueueSize); } /** * 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(DownloadTask 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. */ public boolean schedule(List<DownloadTask> tasks) { final int tasksSize = tasks.size(); /* Ignore empty lists */ if (tasksSize == 0) return true; /* Add into List of open tasks */ for (DownloadTask task : tasks) { try { fOpenTasksQueue.put(task); } catch (InterruptedException e) { return false; } } /* 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; } /* Schedule Job */ scheduleTaskJob(); } return true; } private void scheduleTaskJob() { /* Create the Job */ Job job = createTaskJob(); /* Listen to Job's Lifecycle */ job.addJobChangeListener(new JobChangeAdapter() { /* Update Fields when a Job is Done */ @Override public void done(IJobChangeEvent event) { /* Schedule a new Job if there is work left to do */ if (!fOpenTasksQueue.isEmpty()) scheduleTaskJob(); else fScheduledJobs.decrementAndGet(); } }); /* Do not interrupt on any Error */ job.setProperty(IProgressConstants.NO_IMMEDIATE_ERROR_PROMPT_PROPERTY, Boolean.TRUE); /* Schedule it immediately */ job.schedule(); } /** * 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(DownloadTask task) { return fOpenTasksQueue.contains(task); } /* Create a Job for a Task to handle */ private Job createTaskJob() { Job job = new Job(fName) { @Override protected IStatus run(final IProgressMonitor monitor) { final Job job = this; final IStatus[] status = new IStatus[1]; /* Poll the next Task */ final DownloadTask 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 { status[0] = task.run(job, monitor); /* Log anything that is an Error */ if (status[0].getSeverity() == IStatus.ERROR) { if (Activator.getDefault() != null) Activator.getDefault().getLog().log(status[0]); } } }); } /* Inform about cancelation if present */ return monitor.isCanceled() ? Status.CANCEL_STATUS : status[0]; } @Override public boolean belongsTo(Object family) { return family == DownloadJobQueue.this; } }; return job; } /** * 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. */ public void cancel(boolean joinRunning) { synchronized (this) { /* Clear open tasks */ fOpenTasksQueue.clear(); /* Cancel scheduled Jobs */ Job.getJobManager().cancel(this); } /* Join running Jobs if any */ if (joinRunning) { while (Job.getJobManager().find(this).length != 0) { try { Thread.sleep(50); } catch (InterruptedException e) { break; } } } } /** * @return <code>true</code> if there are active download jobs running and * <code>false</code> otherwise. */ public boolean isWorking() { Job[] activeDownloads = Job.getJobManager().find(this); return activeDownloads != null && activeDownloads.length > 0; } }