/*******************************************************************************
* Copyright (c) 2004, 2006 IBM Corporation 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:
* 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.ISchedulingRule;
import org.eclipse.core.runtime.jobs.Job;
/**
* Captures the implicit job state for a given thread.
*/
class ThreadJob extends Job {
/**
* The notifier is a shared object used to wake up waiting thread jobs
* when another job completes that is releasing a scheduling rule.
*/
static final Object notifier = new Object();
private final JobManager manager;
/**
* 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
*/
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.
*/
protected Job realJob;
/**
* The stack of rules that have been begun in this thread, but not yet ended.
*/
private ISchedulingRule[] ruleStack;
/**
* Rule stack pointer.
*/
private int top;
ThreadJob(JobManager manager, ISchedulingRule rule) {
super("Implicit Job"); //$NON-NLS-1$
this.manager = manager;
setSystem(true);
setPriority(Job.INTERACTIVE);
ruleStack = new ISchedulingRule[2];
top = -1;
setRule(rule);
}
/**
* 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
*/
private void illegalPop(ISchedulingRule rule) {
StringBuffer buf = new StringBuffer("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) {
StringBuffer buf = new StringBuffer("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.
*/
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.
*/
synchronized boolean isRunning() {
return isRunning;
}
/**
* Schedule the job and block the calling thread until the job starts running.
* Returns the ThreadJob instance that was started.
*/
ThreadJob joinRun(IProgressMonitor monitor) {
if (isCanceled(monitor))
throw new OperationCanceledException();
//check if there is a blocking thread before waiting
InternalJob blockingJob = manager.findBlockingJob(this);
Thread blocker = blockingJob == null ? null : blockingJob.getThread();
ThreadJob result = this;
try {
//just return if lock listener decided to grant immediate access
if (manager.getLockManager().aboutToWait(blocker))
return this;
try {
waitStart(monitor, blockingJob);
final Thread currentThread = Thread.currentThread();
while (true) {
if (isCanceled(monitor))
throw new OperationCanceledException();
//try to run the job
if (manager.runNow(this))
return this;
//update blocking job
blockingJob = manager.findBlockingJob(this);
//the rule could have been transferred to this thread while we were waiting
blocker = blockingJob == null ? null : blockingJob.getThread();
if (blocker == currentThread && blockingJob instanceof ThreadJob) {
//now we are just the nested acquire case
result = (ThreadJob)blockingJob;
result.push(getRule());
result.isBlocked = this.isBlocked;
return result;
}
//just return if lock listener decided to grant immediate access
if (manager.getLockManager().aboutToWait(blocker))
return this;
//must lock instance before calling wait
synchronized (notifier) {
try {
notifier.wait(250);
} catch (InterruptedException e) {
//ignore
}
}
}
} finally {
if (this == result)
waitEnd(monitor);
}
} finally {
manager.getLockManager().aboutToRelease();
}
}
/**
* Pops a rule. Returns true if it was the last rule for this thread
* job, and false otherwise.
*/
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.
*/
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))
illegalPush(rule, baseRule);
}
/**
* Reset all of this job's fields so it can be reused. Returns false if
* reuse is not possible
*/
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;
}
/** (non-Javadoc)
* @see org.eclipse.core.runtime.jobs.Job#run(org.eclipse.core.runtime.IProgressMonitor)
*/
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
*/
void setRealJob(Job realJob) {
this.realJob = realJob;
}
/**
* Returns true if this job should cause a self-canceling job
* to cancel itself, and false otherwise.
*/
boolean shouldInterrupt() {
return realJob == null ? true : !realJob.isSystem();
}
/* (non-javadoc)
* For debugging purposes only
*/
public String toString() {
StringBuffer buf = new StringBuffer("ThreadJob"); //$NON-NLS-1$
buf.append('(').append(realJob).append(',').append('[');
for (int i = 0; i <= top && i < ruleStack.length; i++)
buf.append(ruleStack[i]).append(',');
buf.append(']').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.
*/
private void waitEnd(IProgressMonitor monitor) {
final LockManager lockManager = manager.getLockManager();
final Thread currentThread = Thread.currentThread();
if (isRunning()) {
lockManager.addLockThread(currentThread, 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, getRule());
}
}
/**
* Indicates the start of a wait on a scheduling rule. Report the
* blockage to the progress manager and update the lock manager.
* @param monitor The monitor to report blocking to
* @param blockingJob The job that is blocking this thread, or <code>null</code>
*/
private void waitStart(IProgressMonitor monitor, InternalJob blockingJob) {
manager.getLockManager().addLockWaitThread(Thread.currentThread(), getRule());
isBlocked = true;
manager.reportBlocked(monitor, blockingJob);
}
}