Bug 105821: Support Job#join with timeout and progress monitor.
Change-Id: I6582e4b8d204fde1f3cf2db8ab220aaed20d02fc
Signed-off-by: Thirumala Reddy Mutchukota <thirumala@google.com>
diff --git a/bundles/org.eclipse.core.jobs/src/org/eclipse/core/internal/jobs/InternalJob.java b/bundles/org.eclipse.core.jobs/src/org/eclipse/core/internal/jobs/InternalJob.java
index 89941c0..f16029b 100644
--- a/bundles/org.eclipse.core.jobs/src/org/eclipse/core/internal/jobs/InternalJob.java
+++ b/bundles/org.eclipse.core.jobs/src/org/eclipse/core/internal/jobs/InternalJob.java
@@ -8,7 +8,9 @@
* Contributors:
* IBM - Initial API and implementation
* Stephan Wahlbrink - Fix for bug 200997.
- * Thirumala Reddy Mutchukota - Bug 432049, JobGroup API and implementation
+ * Thirumala Reddy Mutchukota (thirumala@google.com) -
+ * Bug 432049, JobGroup API and implementation
+ * Bug 105821, Support for Job#join with timeout and progress monitor
*******************************************************************************/
package org.eclipse.core.internal.jobs;
@@ -390,7 +392,14 @@
* @see Job#join()
*/
protected void join() throws InterruptedException {
- manager.join(this);
+ manager.join(this, 0, null);
+ }
+
+ /* (non-Javadoc)
+ * @see Job#join(long, IProgressMonitor)
+ */
+ protected boolean join(long timeout, IProgressMonitor joinMonitor) throws InterruptedException, OperationCanceledException {
+ return manager.join(this, timeout, joinMonitor);
}
/**
diff --git a/bundles/org.eclipse.core.jobs/src/org/eclipse/core/internal/jobs/JobManager.java b/bundles/org.eclipse.core.jobs/src/org/eclipse/core/internal/jobs/JobManager.java
index 85a9722..ca47549 100644
--- a/bundles/org.eclipse.core.jobs/src/org/eclipse/core/internal/jobs/JobManager.java
+++ b/bundles/org.eclipse.core.jobs/src/org/eclipse/core/internal/jobs/JobManager.java
@@ -11,7 +11,9 @@
* Danail Nachev - Fix for bug 109898
* Mike Moreaty - Fix for bug 289790
* Oracle Corporation - Fix for bug 316839
- * Thirumala Reddy Mutchukota - Bug 432049, JobGroup API and implementation
+ * Thirumala Reddy Mutchukota (thirumala@google.com) -
+ * Bug 432049, JobGroup API and implementation
+ * Bug 105821, Support for Job#join with timeout and progress monitor
* Jan Koehnlein - Fix for bug 60964 (454698)
* Terry Parker - Bug 457504, Publish a job group's final status to IJobChangeListeners
*******************************************************************************/
@@ -58,6 +60,11 @@
*/
public static final int PLUGIN_ERROR = 2;
+ /**
+ * Determines how often the progress monitor is checked for cancellation during the join call.
+ */
+ private static final long MAX_WAIT_INTERVAL = 100;
+
private static final String OPTION_DEADLOCK_ERROR = PI_JOBS + "/jobs/errorondeadlock"; //$NON-NLS-1$
private static final String OPTION_DEBUG_BEGIN_END = PI_JOBS + "/jobs/beginend"; //$NON-NLS-1$
private static final String OPTION_DEBUG_YIELDING = PI_JOBS + "/jobs/yielding"; //$NON-NLS-1$
@@ -880,13 +887,16 @@
}
/* (non-Javadoc)
- * @see org.eclipse.core.runtime.jobs.Job#job(org.eclipse.core.runtime.jobs.Job)
+ * @see org.eclipse.core.runtime.jobs.Job#join(long, IProgressMonitor)
*/
- protected void join(InternalJob job) throws InterruptedException {
+ protected boolean join(InternalJob job, long timeout, IProgressMonitor monitor) throws InterruptedException {
+ Assert.isLegal(timeout >= 0, "timeout should not be negative"); //$NON-NLS-1$
+ long deadline = timeout == 0 ? 0 : System.currentTimeMillis() + timeout;
+
Job currentJob = currentJob();
if (currentJob != null) {
JobGroup jobGroup = currentJob.getJobGroup();
- if (jobGroup != null && jobGroup.getMaxThreads() != 0 && jobGroup == job.getJobGroup())
+ if (timeout == 0 && jobGroup != null && jobGroup.getMaxThreads() != 0 && jobGroup == job.getJobGroup())
throw new IllegalStateException("Joining on a job belonging to the same group is not allowed"); //$NON-NLS-1$
}
@@ -895,10 +905,10 @@
synchronized (lock) {
int state = job.getState();
if (state == Job.NONE)
- return;
+ return true;
//don't join a waiting or sleeping job when suspended (deadlock risk)
if (suspended && state != Job.RUNNING)
- return;
+ return true;
//it's an error for a job to join itself
if (state == Job.RUNNING && job.getThread() == Thread.currentThread())
throw new IllegalStateException("Job attempted to join itself"); //$NON-NLS-1$
@@ -911,17 +921,28 @@
}
};
job.addJobChangeListener(listener);
- //compute set of all jobs that must run before this one
- //add a listener that removes jobs from the blocking set when they finish
}
+
//wait until listener notifies this thread.
try {
boolean canBlock = lockManager.canBlock();
while (true) {
+ if (monitor != null && monitor.isCanceled())
+ throw new OperationCanceledException();
+ long remainingTime = deadline;
+ if (deadline != 0) {
+ remainingTime -= System.currentTimeMillis();
+ if (remainingTime <= 0) {
+ return false;
+ }
+ }
//notify hook to service pending syncExecs before falling asleep
lockManager.aboutToWait(job.getThread());
try {
- if (barrier.acquire(Long.MAX_VALUE))
+ // 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;
+ if (barrier.acquire(sleepTime))
break;
} catch (InterruptedException e) {
// if non-UI thread, re-throw the exception
@@ -934,6 +955,7 @@
lockManager.aboutToRelease();
job.removeJobChangeListener(listener);
}
+ return true;
}
/* (non-Javadoc)
diff --git a/bundles/org.eclipse.core.jobs/src/org/eclipse/core/runtime/jobs/Job.java b/bundles/org.eclipse.core.jobs/src/org/eclipse/core/runtime/jobs/Job.java
index c883860..d4206d0 100644
--- a/bundles/org.eclipse.core.jobs/src/org/eclipse/core/runtime/jobs/Job.java
+++ b/bundles/org.eclipse.core.jobs/src/org/eclipse/core/runtime/jobs/Job.java
@@ -7,7 +7,9 @@
*
* Contributors:
* IBM Corporation - initial API and implementation
- * Thirumala Reddy Mutchukota - Bug 432049, JobGroup API and implementation
+ * Thirumala Reddy Mutchukota (thirumala@google.com) -
+ * Bug 432049, JobGroup API and implementation
+ * Bug 105821, Support for Job#join with timeout and progress monitor
*******************************************************************************/
package org.eclipse.core.runtime.jobs;
@@ -436,11 +438,18 @@
* same group is allowed when the job group does not enforce throttling
* (JobGroup#getMaxThreads is zero).
* </p>
- *
+ * <p>
+ * Calling this method is equivalent to calling <code>join(0, null)</code> and
+ * it is recommended to use the other join method with timeout and progress monitor
+ * as that will provide more control over the join operation.
+ * </p>
+ *
* @exception InterruptedException if this thread is interrupted while waiting
- * @exception IllegalStateException when a job tries to join another job belonging
- * to the same job group and the group is configured with non zero maximum threads allowed.
+ * @exception IllegalStateException when a job tries to join on itself or join on
+ * another job belonging to the same job group and the group is configured with
+ * non zero maximum threads allowed.
* @see #setJobGroup(JobGroup)
+ * @see #join(long, IProgressMonitor)
* @see ILock
* @see IJobManager#suspend()
*/
@@ -450,6 +459,70 @@
}
/**
+ * Waits until either the job is finished or the given timeout has expired.
+ * This method will block the calling thread until the job has finished executing,
+ * or the given timeout is expired, or the given progress monitor is canceled by the user
+ * or the calling thread is interrupted. If the job has not been scheduled,
+ * this method returns immediately. A job must not be joined from within the scope of
+ * its run method.
+ * <p>
+ * If this method is called on a job that reschedules itself from within the
+ * <tt>run</tt> method, the join will return at the end of the first execution.
+ * In other words, join will return the first time this job exits the
+ * {@link #RUNNING} state, or as soon as this job enters the {@link #NONE} state.
+ * </p>
+ * <p>
+ * If this method is called while the job manager is suspended, this job
+ * will only be joined if it is already running; if this job is waiting or sleeping,
+ * this method returns immediately.
+ * </p>
+ * <p>
+ * Note that there is a deadlock risk when using join. If the calling thread owns
+ * a lock or object monitor that the joined thread is waiting for and the timeout
+ * is set zero (i.e no timeout), deadlock will occur.
+ * </p>
+ * <p>
+ * Joining on another job belonging to the same group is not allowed if the
+ * timeout is set to zero and the group enforces throttling due to the potential
+ * for deadlock. For example, when the maximum threads allowed is set to 1 and
+ * a currently running Job A issues a join with no timeout on another Job B
+ * belonging to its own job group, A waits indefinitely for its join to finish,
+ * but B never gets to run. To avoid that an IllegalStateException is thrown when
+ * a job tries to join (with no timeout) another job belonging to the same job group.
+ * Joining another job belonging to the same group is allowed when either the job group
+ * does not enforce throttling (JobGroup#getMaxThreads is zero) or a non zero timeout
+ * value is provided.
+ * </p>
+ * <p>
+ * Throws an <code>OperationCanceledException</code> when the given progress monitor
+ * is canceled. Canceling the monitor does not cancel the job and, if required,
+ * the job may be canceled explicitly using the {@link #cancel()} method.
+ * </p>
+ *
+ * @param timeoutMillis the maximum amount of time to wait for the join to complete,
+ * or <code>zero</code> for no timeout.
+ * @param monitor the progress monitor that can be used to cancel the join operation,
+ * or <code>null</code> if cancellation is not required. No progress is reported
+ * on this monitor.
+ * @return <code>true</code> when the job completes, or <code>false</code> when
+ * the operation is not completed within the given time.
+ * @exception InterruptedException if this thread is interrupted while waiting
+ * @exception IllegalStateException when a job tries to join on itself or join with
+ * no timeout on another job belonging to the same job group and the group is configured
+ * with non-zero maximum threads allowed.
+ * @exception OperationCanceledException if the progress monitor is canceled while waiting
+ * @see #setJobGroup(JobGroup)
+ * @see #cancel()
+ * @see ILock
+ * @see IJobManager#suspend()
+ * @since 3.7
+ */
+ @Override
+ public final boolean join(long timeoutMillis, IProgressMonitor monitor) throws InterruptedException, OperationCanceledException {
+ return super.join(timeoutMillis, monitor);
+ }
+
+ /**
* Removes a job listener from this job.
* Has no effect if an identical listener is not already registered.
*
diff --git a/tests/org.eclipse.core.tests.runtime/src/org/eclipse/core/tests/runtime/jobs/JobTest.java b/tests/org.eclipse.core.tests.runtime/src/org/eclipse/core/tests/runtime/jobs/JobTest.java
index 64fa307..57b4d35 100644
--- a/tests/org.eclipse.core.tests.runtime/src/org/eclipse/core/tests/runtime/jobs/JobTest.java
+++ b/tests/org.eclipse.core.tests.runtime/src/org/eclipse/core/tests/runtime/jobs/JobTest.java
@@ -10,7 +10,12 @@
* Stephan Wahlbrink - Test fix for bug 200997.
* Dmitry Karasik - Test cases for bug 255384
* Jan Koehnlein - Test case for bug 60964 (454698)
+<<<<<<< master
* Alexander Kurtakov <akurtako@redhat.com> - bug 458490
+=======
+ * Thirumala Reddy Mutchukota (thirumala@google.com) -
+ * Bug 105821, Support for Job#join with timeout and progress monitor
+>>>>>>> d688b58 Bug 105821: Support Job#join with timeout and progress monitor.
*******************************************************************************/
package org.eclipse.core.tests.runtime.jobs;
@@ -20,8 +25,7 @@
import org.eclipse.core.internal.jobs.Worker;
import org.eclipse.core.runtime.*;
import org.eclipse.core.runtime.jobs.*;
-import org.eclipse.core.tests.harness.TestBarrier;
-import org.eclipse.core.tests.harness.TestJob;
+import org.eclipse.core.tests.harness.*;
import org.junit.Assert;
/**
@@ -858,6 +862,113 @@
TestBarrier.waitForStatus(status, TestBarrier.STATUS_DONE);
}
+ public void testJoinWithTimeout() {
+ longJob.schedule();
+ final long timeout = 1000;
+ final long duration[] = {-1};
+ // Create a thread that will join the test job
+ final int[] status = new int[1];
+ status[0] = TestBarrier.STATUS_WAIT_FOR_START;
+ Thread t = new Thread(new Runnable() {
+ public void run() {
+ status[0] = TestBarrier.STATUS_START;
+ try {
+ long start = System.currentTimeMillis();
+ longJob.join(timeout, null);
+ duration[0] = System.currentTimeMillis() - start;
+ } catch (InterruptedException e) {
+ Assert.fail("0.88");
+ } catch (OperationCanceledException e) {
+ Assert.fail("0.99");
+ }
+ status[0] = TestBarrier.STATUS_DONE;
+ }
+ });
+ t.start();
+ TestBarrier.waitForStatus(status, TestBarrier.STATUS_START);
+ assertEquals("1.0", TestBarrier.STATUS_START, status[0]);
+ int i = 0;
+ for (; i < 11; i++) {
+ if (status[0] == TestBarrier.STATUS_DONE) {
+ // Verify that the join call is blocked for at least for the duration of given timeout
+ assertTrue("2.0 duration: " + duration + " timeout: " + timeout, duration[0] >= timeout);
+ break;
+ }
+ sleep(100);
+ }
+ // Verify that the join call is finished with in reasonable time of 1100 ms (given timeout + 100ms)
+ assertTrue("3.0", i < 11);
+ // Verify that the join call is still running
+ assertEquals("4.0", Job.RUNNING, longJob.getState());
+ // Finally cancel the job
+ longJob.cancel();
+ waitForCompletion(longJob);
+ }
+
+ public void testJoinWithProgressMonitor() {
+ shortJob.schedule(100000);
+ // Create a progress monitor for the join call
+ final FussyProgressMonitor monitor = new FussyProgressMonitor();
+ // Create a thread that will join the test job
+ final int[] status = new int[1];
+ status[0] = TestBarrier.STATUS_WAIT_FOR_START;
+ Thread t = new Thread(new Runnable() {
+ public void run() {
+ status[0] = TestBarrier.STATUS_START;
+ try {
+ shortJob.join(0, monitor);
+ } catch (InterruptedException e) {
+ Assert.fail("0.88");
+ } catch (OperationCanceledException e) {
+ Assert.fail("0.99");
+ }
+ status[0] = TestBarrier.STATUS_DONE;
+ }
+ });
+ t.start();
+ TestBarrier.waitForStatus(status, TestBarrier.STATUS_START);
+ assertEquals("1.0", TestBarrier.STATUS_START, status[0]);
+ // Wakeup the job to get the join call to complete
+ shortJob.wakeUp();
+ TestBarrier.waitForStatus(status, TestBarrier.STATUS_DONE);
+ monitor.sanityCheck();
+ }
+
+ public void testJoinWithCancelingMonitor() {
+ longJob.schedule();
+ // Create a progress monitor for the join call
+ final FussyProgressMonitor monitor = new FussyProgressMonitor();
+ // Create a thread that will join the test job
+ final int[] status = new int[1];
+ status[0] = TestBarrier.STATUS_WAIT_FOR_START;
+ Thread t = new Thread(new Runnable() {
+ public void run() {
+ status[0] = TestBarrier.STATUS_START;
+ try {
+ longJob.join(0, monitor);
+ } catch (InterruptedException e) {
+ Assert.fail("0.88");
+ } catch (OperationCanceledException e) {
+ // expected
+ }
+ status[0] = TestBarrier.STATUS_DONE;
+ }
+ });
+ t.start();
+ TestBarrier.waitForStatus(status, TestBarrier.STATUS_START);
+ assertEquals("1.0", TestBarrier.STATUS_START, status[0]);
+
+ // Cancel the monitor that is attached to the join call
+ monitor.setCanceled(true);
+ TestBarrier.waitForStatus(status, 0, TestBarrier.STATUS_DONE);
+ monitor.sanityCheck();
+ // Verify that the join call is still running
+ assertEquals("2.0", Job.RUNNING, longJob.getState());
+ // Finally cancel the job
+ longJob.cancel();
+ waitForCompletion(longJob);
+ }
+
public void testJoinInterruptNonUIThread() throws InterruptedException {
final Job job = new TestJob("job", 1000, 100);
Thread t = new Thread(new Runnable() {