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() {