blob: df4718982f70fd0c2297a898b1c6970133b60078 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2004, 2012 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM - Initial API and implementation
*******************************************************************************/
package org.eclipse.core.internal.jobs;
import org.eclipse.core.internal.runtime.RuntimeLog;
import org.eclipse.core.runtime.*;
import org.eclipse.core.runtime.jobs.*;
/**
* Captures the implicit job state for a given thread.
*/
class ThreadJob extends Job {
/**
* Set to true if this thread job is running in a thread that did
* not own a rule already. This means it needs to acquire the
* rule during beginRule, and must release the rule during endRule.
*/
protected boolean acquireRule = false;
/**
* Indicates that this thread job did report to the progress manager
* that it will be blocked, and therefore when it begins it must
* be reported to the job manager when it is no longer blocked.
*/
boolean isBlocked = false;
/**
* True if this ThreadJob has begun execution
* @GuardedBy("this")
*/
protected boolean isRunning = false;
/**
* Used for diagnosing mismatched begin/end pairs. This field
* is only used when in debug mode, to capture the stack trace
* of the last call to beginRule.
*/
private RuntimeException lastPush = null;
/**
* The actual job that is running in the thread that this
* ThreadJob represents. This will be null if this thread
* job is capturing a rule acquired outside of a job.
* @GuardedBy("JobManager.implicitJobs")
*/
protected Job realJob;
/**
* The stack of rules that have been begun in this thread, but not yet ended.
* @GuardedBy("JobManager.implicitJobs")
*/
private ISchedulingRule[] ruleStack;
/**
* Rule stack pointer.
* INV: {@code 0 <= top <= ruleStack.length}
* @GuardedBy("JobManager.implicitJobs")
*/
private int top;
/**
* Waiting state for thread jobs is independent of the internal state. When
* this variable is true, this ThreadJob is waiting in joinRun()
* @GuardedBy("jobStateLock")
*/
boolean isWaiting;
ThreadJob(ISchedulingRule rule) {
super("Implicit Job"); //$NON-NLS-1$
setSystem(true);
// calling setPriority will try to acquire JobManager.lock, breaking
// lock acquisition protocol. Since we are constructing this thread,
// we can call internalSetPriority
((InternalJob) this).internalSetPriority(Job.INTERACTIVE);
ruleStack = new ISchedulingRule[2];
top = -1;
((InternalJob) this).internalSetRule(rule);
}
boolean isResumingAfterYield() {
return false;
}
/**
* An endRule was called that did not match the last beginRule in
* the stack. Report and log a detailed informational message.
* @param rule The rule that was popped
* @GuardedBy("JobManager.implicitJobs")
*/
private void illegalPop(ISchedulingRule rule) {
StringBuilder buf = new StringBuilder("Attempted to endRule: "); //$NON-NLS-1$
buf.append(rule);
if (top >= 0 && top < ruleStack.length) {
buf.append(", does not match most recent begin: "); //$NON-NLS-1$
buf.append(ruleStack[top]);
} else if (top < 0) {
buf.append(", but there was no matching beginRule"); //$NON-NLS-1$
}
else {
buf.append(", but the rule stack was out of bounds: " + top); //$NON-NLS-1$
}
buf.append(". See log for trace information if rule tracing is enabled."); //$NON-NLS-1$
String msg = buf.toString();
if (JobManager.DEBUG || JobManager.DEBUG_BEGIN_END) {
System.out.println(msg);
Throwable t = lastPush == null ? new IllegalArgumentException() : lastPush;
IStatus error = new Status(IStatus.ERROR, JobManager.PI_JOBS, 1, msg, t);
RuntimeLog.log(error);
}
Assert.isLegal(false, msg);
}
/**
* Client has attempted to begin a rule that is not contained within
* the outer rule.
*/
private void illegalPush(ISchedulingRule pushRule, ISchedulingRule baseRule) {
StringBuilder buf = new StringBuilder("Attempted to beginRule: "); //$NON-NLS-1$
buf.append(pushRule);
buf.append(", does not match outer scope rule: "); //$NON-NLS-1$
buf.append(baseRule);
String msg = buf.toString();
if (JobManager.DEBUG) {
System.out.println(msg);
IStatus error = new Status(IStatus.ERROR, JobManager.PI_JOBS, 1, msg, new IllegalArgumentException());
RuntimeLog.log(error);
}
Assert.isLegal(false, msg);
}
/**
* Returns true if the monitor is canceled, and false otherwise.
* Protects the caller from exception in the monitor implementation.
*/
static private boolean isCanceled(IProgressMonitor monitor) {
try {
return monitor.isCanceled();
} catch (RuntimeException e) {
//logged message should not be translated
IStatus status = new Status(IStatus.ERROR, JobManager.PI_JOBS, JobManager.PLUGIN_ERROR, "ThreadJob.isCanceled", e); //$NON-NLS-1$
RuntimeLog.log(status);
}
return false;
}
/**
* Returns true if this thread job was scheduled and actually started running.
* @GuardedBy("this")
*/
synchronized boolean isRunning() {
return isRunning;
}
/**
* A reentrant method which will run given <code>ThreadJob</code> immediately if there
* are no existing jobs with conflicting rules, or block until the rule can be acquired.
* <ul>
* <li>If given job must block, the <code>LockListener</code> is given a chance to override.
* <li>If override is not granted, then this method will block until the rule is available.
* <li>If <code>LockListener#canBlock</code> returns <code>true</code>, then the <code>monitor</code>
* <i>will not</i> be periodically checked for cancellation. It will only be rechecked if this
* thread is interrupted.
* <li>If <code>LockListener#canBlock</code> returns <code>false</code> the
* <code>monitor</code> <i>will</i> be checked periodically for cancellation.
* </ul>
* When a UI is present, it is recommended that the <code>LockListener</code>
* should not allow the UI thread to block without checking the <code>monitor</code>. This
* ensures that the UI remains responsive.
*
* @see LockListener#aboutToWait(Thread)
* @see LockListener#canBlock()
* @see JobManager#transferRule(ISchedulingRule, Thread)
* @return given job, or the <code>ThreadJob</code> instance that was
* unblocked (due to transferRule) in the case of reentrant invocations of this method.
*
* @param threadJob - job to run or to wait until the rule can be acquired
* @param monitor - The <code>IProgressMonitor</code> used to report blocking status and
* cancellation.
*
* @throws OperationCanceledException if this job was canceled before it was started.
*/
static ThreadJob joinRun(ThreadJob threadJob, IProgressMonitor monitor) {
if (isCanceled(monitor)) {
throw new OperationCanceledException();
}
// check if there is a blocking thread before waiting
InternalJob blockingJob = manager.findBlockingJob(threadJob);
Thread blocker = blockingJob == null ? null : blockingJob.getThread();
ThreadJob result;
boolean interruptedDuringWaitForRun;
try {
// just return if lock listener decided to grant immediate access
if (manager.getLockManager().aboutToWait(blocker)) {
return threadJob;
}
result = waitForRun(threadJob, monitor, blockingJob);
} finally {
// We need to check for interruption unconditionally in order to
// ensure we clear the thread's interrupted state. However, we only
// throw an OperationCanceledException outside of the finally block
// because we only want to throw that exception if we're not already
// throwing some other exception here.
interruptedDuringWaitForRun = Thread.interrupted();
manager.getLockManager().aboutToRelease();
}
// During the call to waitForRun, we use the thread's interrupt flag to
// trigger cancellation, so thread interruption at this time should
// trigger an OCE.
if (interruptedDuringWaitForRun) {
throw new OperationCanceledException();
}
return result;
}
/**
* Waits until given {@code ThreadJob} "runs" (acquires the rule conflicting
* with given {@code blockingJob} or is canceled. While the given
* {@code ThreadJob} waits to acquire the rule, it is put to the
* {@link JobManager#waitingThreadJobs} queue.
*
* @param threadJob
* job which should wait until the rule blocked by the
* {@code blockingJob} can be acquired
* @param monitor
* the {@code IProgressMonitor} used to report blocking status and
* cancellation.
* @param blockingJob
* running or blocked job whose scheduling rule blocks (conflicts
* with) the scheduling rule of the given waiting job, or
* {@code null}
* @return given job, or the <code>ThreadJob</code> instance that was unblocked
* (due to transferRule) in the case of reentrant invocations of this
* method.
*/
private static ThreadJob waitForRun(final ThreadJob threadJob, IProgressMonitor monitor, InternalJob blockingJob) {
// Ask lock manager if it safe to block this thread
final boolean canBlock = manager.getLockManager().canBlock();
ThreadJob result = threadJob;
boolean interrupted = false;
boolean waiting = false;
boolean ruleCompatibleAndTransferred = false;
try {
waitStart(threadJob, monitor, blockingJob);
manager.implicitJobs.addWaiting(threadJob);
waiting = true;
// If we're allowed to block this thread we won't be checking the monitor. In order
// to respond to cancellation, register this monitor with the internal JobManager
// worker thread. The worker thread will check for cancellation and will interrupt
// this thread when the monitor is canceled. T
if (canBlock) {
manager.beginMonitoring(threadJob, monitor);
}
final Thread currentThread = Thread.currentThread();
// Ultimately, this loop will wait until the job "runs" (acquires the rule)
// or is canceled. However, there are many ways for that to occur.
// The exit conditions of this loop are:
// 1) This job no longer conflicts with any other running job.
// 2) The LockManager#aboutToWait allowed this thread to run.
// This usually occurs during reentrant situations. i.e. This is a UI thread,
// and a syncExec is performed from conflicting job/thread.
// 3) A rule is transfered to this thread. This can be invoked programmatically,
// or commonly in JFace via ModalContext (for wizards/etc).
// 4) Monitor is canceled.
while (true) {
// monitor is foreign code so do not hold locks while calling into monitor
if (interrupted || isCanceled(monitor)) {
// Condition #4.
throw new OperationCanceledException();
}
// Try to run the job. If result is null, this job was allowed to run.
// If the result is successful, atomically release thread from waiting
// status.
blockingJob = manager.runNow(threadJob, true);
if (blockingJob == null) {
// Condition #1.
waiting = false;
return threadJob;
}
Thread blocker = blockingJob.getThread();
// the rule could have been transferred to this thread while we were waiting
if (blocker == currentThread && blockingJob instanceof ThreadJob) {
// now we are just the nested acquire case
result = (ThreadJob) blockingJob;
// push expects a compatible rule, otherwise an exception will be thrown
result.push(threadJob.getRule());
// rule was either accepted or both jobs have null rules
ruleCompatibleAndTransferred = true;
result.isBlocked = threadJob.isBlocked;
// Condition #3.
return result;
}
// just return if lock listener decided to grant immediate access
if (manager.getLockManager().aboutToWait(blocker)) {
// Condition #2.
return threadJob;
}
// Notify the lock manager that we're about to block waiting for the scheduling rule
manager.getLockManager().addLockWaitThread(currentThread, threadJob.getRule());
synchronized (blockingJob.jobStateLock) {
try {
// Wait until we are no longer definitely blocked (not running).
// The actual exit conditions are listed above at the beginning of
// this while loop
int state = blockingJob.getState();
//ensure we don't wait forever if the blocker is waiting, because it might have yielded to me
if (state == Job.RUNNING && canBlock) {
blockingJob.jobStateLock.wait();
} else if (state != Job.NONE) {
blockingJob.jobStateLock.wait(250);
}
} catch (InterruptedException e) {
// This thread may be interrupted via two common scenarios. 1) If
// the UISynchronizer is in use and this thread is a UI thread
// and a syncExec() is performed, this thread will be interrupted
// every 1000ms. 2) If this thread is allowed to be blocked and
// the progress monitor was canceled, the internal JobManager
// worker thread will interrupt this thread so cancellation can
// be carried out.
interrupted = true;
}
}
// Going around the loop again. Ensure we're not marked as waiting for the thread
// as external code is run via the monitor (Bug 262032).
manager.getLockManager().removeLockWaitThread(currentThread, threadJob.getRule());
}
} finally {
boolean canStopWaiting;
boolean updateLockState;
if (threadJob != result) {
// The rule which was blocking given threadJob could have been transferred to
// this thread while we were waiting, and if our rule was contained in the
// blocking rule, we can remove this job from the waiting queue
canStopWaiting = ruleCompatibleAndTransferred;
// lock sate should be unchanged, the thread is same as before
updateLockState = false;
} else {
// job acquired blocked rule, so it can be removed from the waiting queue
canStopWaiting = true;
// update the lock state because our thread acquired the rule now
updateLockState = true;
}
waitEnd(threadJob, updateLockState, monitor);
if (canStopWaiting) {
if (waiting) {
manager.implicitJobs.removeWaiting(threadJob);
}
}
if (canBlock) {
// must unregister monitoring this job
manager.endMonitoring(threadJob);
}
}
}
/**
* Pops a rule. Returns true if it was the last rule for this thread
* job, and false otherwise.
* @GuardedBy("JobManager.implicitJobs")
*/
boolean pop(ISchedulingRule rule) {
if (top < 0 || ruleStack[top] != rule) {
illegalPop(rule);
}
ruleStack[top--] = null;
return top < 0;
}
/**
* Adds a new scheduling rule to the stack of rules for this thread. Throws
* a runtime exception if the new rule is not compatible with the base
* scheduling rule for this thread.
* @GuardedBy("JobManager.implicitJobs")
*/
void push(final ISchedulingRule rule) {
final ISchedulingRule baseRule = getRule();
if (++top >= ruleStack.length) {
ISchedulingRule[] newStack = new ISchedulingRule[ruleStack.length * 2];
System.arraycopy(ruleStack, 0, newStack, 0, ruleStack.length);
ruleStack = newStack;
}
ruleStack[top] = rule;
if (JobManager.DEBUG_BEGIN_END) {
lastPush = (RuntimeException) new RuntimeException().fillInStackTrace();
}
//check for containment last because we don't want to fail again on endRule
if (baseRule != null && rule != null && !(baseRule.contains(rule) && baseRule.isConflicting(rule))) {
illegalPush(rule, baseRule);
}
}
/**
* Reset all of this job's fields so it can be reused. Returns false if
* reuse is not possible
* @GuardedBy("JobManager.implicitJobs")
*/
boolean recycle() {
//don't recycle if still running for any reason
if (getState() != Job.NONE) {
return false;
}
//clear and reset all fields
acquireRule = isRunning = isBlocked = false;
realJob = null;
setRule(null);
setThread(null);
if (ruleStack.length != 2) {
ruleStack = new ISchedulingRule[2];
} else {
ruleStack[0] = ruleStack[1] = null;
}
top = -1;
return true;
}
@Override
public IStatus run(IProgressMonitor monitor) {
synchronized (this) {
isRunning = true;
}
return ASYNC_FINISH;
}
/**
* Records the job that is actually running in this thread, if any
* @param realJob The running job
* @GuardedBy("JobManager.implicitJobs")
*/
void setRealJob(Job realJob) {
this.realJob = realJob;
}
/**
* Returns true if this job should cause a self-canceling job
* to cancel itself, and false otherwise.
* @GuardedBy("JobManager.implicitJobs")
*/
boolean shouldInterrupt() {
return realJob == null ? true : !realJob.isSystem();
}
/*
* For debugging purposes only
*/
@Override
public String toString() {
StringBuilder buf = new StringBuilder("ThreadJob"); //$NON-NLS-1$
buf.append('(').append(realJob).append(',').append(getRuleStack()).append(')');
return buf.toString();
}
String getRuleStack() {
StringBuilder buf = new StringBuilder();
buf.append('[');
for (int i = 0; i <= top && i < ruleStack.length; i++) {
buf.append(ruleStack[i]);
if (i != top) {
buf.append(',');
}
}
buf.append(']');
return buf.toString();
}
/**
* Reports that this thread was blocked, but is no longer blocked and is able
* to proceed.
* @param monitor The monitor to report unblocking to.
*/
static private void waitEnd(ThreadJob threadJob, boolean updateLockManager, IProgressMonitor monitor) {
if (updateLockManager) {
final LockManager lockManager = manager.getLockManager();
final Thread currentThread = Thread.currentThread();
if (threadJob.isRunning()) {
lockManager.addLockThread(currentThread, threadJob.getRule());
//need to re-acquire any locks that were suspended while this thread was blocked on the rule
lockManager.resumeSuspendedLocks(currentThread);
} else {
//tell lock manager that this thread gave up waiting
lockManager.removeLockWaitThread(currentThread, threadJob.getRule());
}
}
if (threadJob.isBlocked) {
threadJob.isBlocked = false;
manager.reportUnblocked(monitor);
}
}
/**
* Indicates the start of a wait on a scheduling rule. Report the
* blockage to the progress manager.
* @param monitor The monitor to report blocking to
* @param blockingJob The job that is blocking this thread, or <code>null</code>
*/
static private void waitStart(ThreadJob threadJob, IProgressMonitor monitor, InternalJob blockingJob) {
threadJob.isBlocked = true;
manager.reportBlocked(monitor, blockingJob);
}
/**
* ThreadJobs are one-shot jobs, and they must ignore all attempts to schedule them.
*/
@Override
public boolean shouldSchedule() {
return false;
}
}