blob: a6f6439489b1d17eae75a0139d424cb321896635 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2014 Google Inc and others.
* 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.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Thirumala Reddy Mutchukota - initial API and implementation
*******************************************************************************/
package org.eclipse.core.internal.jobs;
import java.util.*;
import org.eclipse.core.runtime.*;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.core.runtime.jobs.JobGroup;
/**
* Internal implementation class for job groups.
*
* @noextend This class is not intended to be extended by clients. All job groups
* must be subclasses of the API <code>org.eclipse.core.runtime.jobs.JobGroup</code> class.
*/
public class InternalJobGroup {
/**
* The maximum amount of time to wait on {@link #jobGroupStateLock}.
* Determines how often the progress monitor is checked for cancellation.
*/
private static final long MAX_WAIT_INTERVAL = 100;
/**
* This lock will be held while performing state changes on this job group. It is
* also used as a notifier to wake up the threads waiting for this job group to complete.
*
* External code is never called while holding this lock, thus removing the hold and wait
* condition necessary for deadlock.
*
* @GuardedBy("itself")
*/
private final Object jobGroupStateLock = new Object();
private static final JobManager manager = JobManager.getInstance();
private final String name;
private final int maxThreads;
private volatile int state = JobGroup.NONE;
private volatile MultiStatus result;
private final Set<InternalJob> runningJobs = new HashSet<>();
private final Set<InternalJob> otherActiveJobs = new HashSet<>();
private final List<IStatus> results = new ArrayList<>();
private boolean cancelingDueToError;
private int failedJobsCount;
private int canceledJobsCount;
private int seedJobsCount;
private int seedJobsRemainingCount;
protected InternalJobGroup(String name, int maxThreads, int seedJobsCount) {
Assert.isNotNull(name);
Assert.isLegal(maxThreads >= 0);
Assert.isLegal(seedJobsCount > 0);
this.name = name;
this.maxThreads = maxThreads;
this.seedJobsCount = seedJobsCount;
this.seedJobsRemainingCount = seedJobsCount;
}
protected String getName() {
return name;
}
protected int getMaxThreads() {
return maxThreads;
}
protected MultiStatus getResult() {
return result;
}
protected int getState() {
return state;
}
protected List<Job> getActiveJobs() {
return manager.find(this);
}
protected void cancel() {
manager.cancel(this);
}
protected boolean join(long timeout, IProgressMonitor monitor) throws InterruptedException, OperationCanceledException {
return manager.join(this, timeout, monitor);
}
/**
* Called by the JobManager when the state of a job belonging to this group has changed.
* Must be called from JobManager#changeState
*
* @param job a job belonging to this group
* @param oldState the old state of the job
* @param newState the new state of the job
* @GuardedBy("JobManager.lock")
*/
final void jobStateChanged(InternalJob job, int oldState, int newState) {
switch (oldState) {
case Job.NONE :
break;
case Job.SLEEPING :
case Job.WAITING :
otherActiveJobs.remove(job);
break;
case Job.RUNNING :
runningJobs.remove(job);
break;
default :
Assert.isLegal(false, "Invalid job state: " + job + ", state: " + oldState); //$NON-NLS-1$//$NON-NLS-2$
break;
}
switch (newState) {
case Job.NONE :
break;
case Job.SLEEPING :
case Job.WAITING :
otherActiveJobs.add(job);
break;
case Job.RUNNING :
runningJobs.add(job);
break;
default :
Assert.isLegal(false, "Invalid job state: " + job + ", state: " + newState); //$NON-NLS-1$//$NON-NLS-2$
break;
}
/*
* We can determine if the job that is being scheduled is one of its job group's "seed"
* jobs by retrieving the currently running job for this thread. If there is a running
* job on this thread or that job's job group is the same as this job's job group, then
* this job is being scheduled in response to discovering more work, so it is not one
* of its group's seed jobs, otherwise it is.
*/
if (job.internalGetState() == InternalJob.ABOUT_TO_SCHEDULE && getGroupOfCurrentlyRunningJob() != job.getJobGroup()) {
seedJobsRemainingCount--;
}
if (oldState == Job.RUNNING && newState == Job.NONE) {
IStatus jobResult = job.getResult();
Assert.isLegal(jobResult != null);
if (cancelingDueToError && jobResult.getSeverity() == IStatus.CANCEL)
return;
results.add(jobResult);
int jobResultSeverity = jobResult.getSeverity();
if (jobResultSeverity == IStatus.ERROR) {
failedJobsCount++;
} else if (jobResultSeverity == IStatus.CANCEL) {
canceledJobsCount++;
}
}
//make sure this job group is running
if (getState() == JobGroup.NONE && getActiveJobsCount() > 0) {
synchronized (jobGroupStateLock) {
state = JobGroup.ACTIVE;
jobGroupStateLock.notifyAll();
}
}
}
/**
* Returns the job group of the job currently running in this thread. Will return null if
* either this thread is not running a job or the job is not part of a job group.
*/
private JobGroup getGroupOfCurrentlyRunningJob() {
Job job = manager.currentJob();
return job == null ? null : job.getJobGroup();
}
/**
* Called by the JobManager to signify that the group canceling reason is changed.
* Must be called from JobManager#cancel(InternalJobGroup).
*
* @param cancelDueToError <code>true</code> if the group is getting canceled because
* the <code>shouldCancel(IStatus, int, int)</code> method returned <code>true</code>,
* <code>false</code> otherwise.
* @GuardedBy("JobManager.lock")
*/
final void updateCancelingReason(boolean cancelDueToError) {
cancelingDueToError = cancelDueToError;
if (!cancelDueToError) {
// add a dummy cancel status to the results to make sure the combined status
// will be of severity CANCEL.
results.add(Status.CANCEL_STATUS);
}
}
/**
* Called by the JobManager to signify that the group is getting canceled.
* Must be called from JobManager#cancel(InternalJobGroup).
*
* @param cancelDueToError <code>true</code> if the group is getting canceled because
* the <code>shouldCancel(IStatus, int, int)</code> method returned <code>true</code>,
* <code>false</code> otherwise.
* @GuardedBy("JobManager.lock")
* @GuardedBy("jobGroupStateLock")
*/
final void cancelAndNotify(boolean cancelDueToError) {
synchronized (jobGroupStateLock) {
state = JobGroup.CANCELING;
updateCancelingReason(cancelDueToError);
jobGroupStateLock.notifyAll();
}
for (Job job : internalGetActiveJobs())
job.cancel();
}
/**
* Called by the JobManager to notify the group when the last job belonging
* to the group has finished execution. Must be called from JobManager#updateJobGroup.
*
* @param groupResult the combined status of the job group
* @GuardedBy("JobManager.lock")
* @GuardedBy("jobGroupStateLock")
*/
final void endJobGroup(MultiStatus groupResult) {
synchronized (jobGroupStateLock) {
if (seedJobsRemainingCount > 0 && !groupResult.matches(IStatus.CANCEL))
throw new IllegalStateException("Invalid initial jobs remaining count"); //$NON-NLS-1$
state = JobGroup.NONE;
result = groupResult;
results.clear();
cancelingDueToError = false;
failedJobsCount = 0;
canceledJobsCount = 0;
seedJobsRemainingCount = seedJobsCount;
jobGroupStateLock.notifyAll();
}
}
final List<Job> internalGetActiveJobs() {
List<Job> activeJobs = new ArrayList<>(runningJobs.size() + otherActiveJobs.size());
for (InternalJob job : runningJobs)
activeJobs.add((Job) job);
for (InternalJob job : otherActiveJobs)
activeJobs.add((Job) job);
return activeJobs;
}
/**
* Called by the JobManager when updating the job group status. Prevents the job group
* from entering the NONE state prematurely, which can happen if all scheduled jobs
* run to completion while the master thread still has more "seed" jobs to schedule.
*
* @return the count of initial jobs remaining to be scheduled
*/
final int getseedJobsRemainingCount() {
return seedJobsRemainingCount;
}
final int getActiveJobsCount() {
return runningJobs.size() + otherActiveJobs.size();
}
final int getRunningJobsCount() {
return runningJobs.size();
}
final int getFailedJobsCount() {
return failedJobsCount;
}
final int getCanceledJobsCount() {
return canceledJobsCount;
}
final List<IStatus> getCompletedJobResults() {
return new ArrayList<>(results);
}
protected boolean shouldCancel(IStatus lastCompletedJobResult, int numberOfFailedJobs, int numberOfCanceledJobs) {
return numberOfFailedJobs > 0;
}
protected MultiStatus computeGroupResult(List<IStatus> jobResults) {
List<IStatus> importantResults = new ArrayList<>();
for (IStatus jobResult : jobResults) {
if (jobResult.getSeverity() != IStatus.OK)
importantResults.add(jobResult);
}
if (importantResults.isEmpty())
return new MultiStatus("org.eclipse.core.jobs", 0, name, null); //$NON-NLS-1$
String pluginId = importantResults.get(0).getPlugin();
IStatus[] groupResults = importantResults.toArray(new IStatus[importantResults.size()]);
return new MultiStatus(pluginId, 0, groupResults, name, null);
}
/**
* Implementation of joining a job group.
* @param remainingTime
* @return <code>true</code> if the join completed, and false otherwise (still waiting).
*/
boolean doJoin(long remainingTime) throws InterruptedException {
synchronized (jobGroupStateLock) {
if (getState() == JobGroup.NONE)
return true;
// If remaining time is greater than MAX_WAIT_INTERVAL, sleep only for
// MAX_WAIT_INTERVAL instead to be more responsive to monitor cancellation.
long sleepTime = remainingTime != 0 && remainingTime <= MAX_WAIT_INTERVAL ? remainingTime : MAX_WAIT_INTERVAL;
jobGroupStateLock.wait(sleepTime);
return getState() == JobGroup.NONE;
}
}
}