Bug 528248 - Project#build() greedily uses root scheduling rule

Introduce "relaxed" rule, like on workspace.build() for notifications,
and use project-specific build rule for project build.

Change-Id: Idcf46d3663fa355dce66e3ef618f3e4e72000c3b
Signed-off-by: Mickael Istria <mistria@redhat.com>
diff --git a/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/Project.java b/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/Project.java
index a0727b9..8a2a6bf 100644
--- a/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/Project.java
+++ b/bundles/org.eclipse.core.resources/src/org/eclipse/core/internal/resources/Project.java
@@ -26,6 +26,7 @@
 import org.eclipse.core.runtime.*;
 import org.eclipse.core.runtime.content.IContentTypeMatcher;
 import org.eclipse.core.runtime.jobs.ISchedulingRule;
+import org.eclipse.core.runtime.jobs.Job;
 import org.eclipse.osgi.util.NLS;
 
 public class Project extends Container implements IProject {
@@ -545,31 +546,35 @@
 			@Override
 			public void run(IProgressMonitor innerMonitor) throws CoreException {
 				innerMonitor = Policy.monitorFor(innerMonitor);
-				final ISchedulingRule rule = workspace.getRoot();
+				final ISchedulingRule projectBuildRule = workspace.getBuildManager().getRule(config, trigger, builderName, args);
+				final boolean relaxed = Job.getJobManager().currentRule() == null && projectBuildRule != null && projectBuildRule.contains(workspace.getRoot());
+
+				// PRE + POST_BUILD, and the build itself are allowed to modify resources, so require the current thread's scheduling rule
+				// to either contain the WR or be null. Therefore, if not null, ensure it contains the WR rule...
+				final ISchedulingRule notificationsRule = relaxed ? null : workspace.getRuleFactory().buildRule();
 				try {
 					innerMonitor.beginTask("", Policy.totalWork); //$NON-NLS-1$
 					try {
-						workspace.prepareOperation(rule, innerMonitor);
+						workspace.prepareOperation(notificationsRule, innerMonitor);
 						if (!shouldBuild())
 							return;
 						workspace.beginOperation(true);
 						workspace.aboutToBuild(Project.this, trigger);
 					} finally {
-						workspace.endOperation(rule, false);
+						workspace.endOperation(notificationsRule, false);
 					}
-					final ISchedulingRule buildRule = workspace.getBuildManager().getRule(config, trigger, builderName, args);
 					try {
 						IStatus result;
-						workspace.prepareOperation(buildRule, innerMonitor);
+						workspace.prepareOperation(projectBuildRule, innerMonitor);
 						//don't open the tree eagerly because it will be wasted if no build occurs
 						workspace.beginOperation(false);
 						result = workspace.getBuildManager().build(config, trigger, builderName, args, Policy.subMonitorFor(innerMonitor, Policy.opWork));
 						if (!result.isOK())
 							throw new ResourceException(result);
 					} finally {
-						workspace.endOperation(buildRule, false);
+						workspace.endOperation(projectBuildRule, false);
 						try {
-							workspace.prepareOperation(rule, innerMonitor);
+							workspace.prepareOperation(notificationsRule, innerMonitor);
 							//don't open the tree eagerly because it will be wasted if no change occurs
 							workspace.beginOperation(false);
 							workspace.broadcastBuildEvent(Project.this, IResourceChangeEvent.POST_BUILD, trigger);
@@ -577,7 +582,7 @@
 							if (workspace.getElementTree().isImmutable())
 								workspace.newWorkingTree();
 						} finally {
-							workspace.endOperation(rule, false);
+							workspace.endOperation(notificationsRule, false);
 						}
 					}
 				} finally {
diff --git a/bundles/org.eclipse.core.resources/src/org/eclipse/core/resources/IncrementalProjectBuilder.java b/bundles/org.eclipse.core.resources/src/org/eclipse/core/resources/IncrementalProjectBuilder.java
index ed5654d..d65ab73 100644
--- a/bundles/org.eclipse.core.resources/src/org/eclipse/core/resources/IncrementalProjectBuilder.java
+++ b/bundles/org.eclipse.core.resources/src/org/eclipse/core/resources/IncrementalProjectBuilder.java
@@ -410,9 +410,10 @@
 	 * <strong>Notes:</strong>
 	 * <ul>
 	 * <li>
-	 * If the builder rule is non-<code>null</code> it must be "contained" in the workspace root rule.
-	 * I.e. {@link ISchedulingRule#contains(ISchedulingRule)} must return
-	 * <code>true</code> when invoked on the workspace root with the builder rule.
+	 * The rule may be <i>relaxed</i> and in some cases let the builder be scheduled in
+	 * parallel of any other operation using a rule based on {@link IResource}). To
+	 * implement such <i>relaxed</i> rule for the builder, simply make the rule return
+	 * <code>true</code> for <code>rule.contains(workspaceRoot)</code>
 	 * </li>
 	 * <li>
 	 * The rule returned here may have no effect if the build is invoked within the
@@ -424,6 +425,7 @@
 	 * The delta returned by {@link #getDelta(IProject)} for any project
 	 * outside the scope of the builder's rule may not contain changes that occurred
 	 * concurrently with the build.
+	 * </li>
 	 * </ul>
 	 * </p>
 	 * <p>
diff --git a/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/internal/builders/ParallelBuildChainTest.java b/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/internal/builders/ParallelBuildChainTest.java
index 35463cc..ae1e3e2 100644
--- a/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/internal/builders/ParallelBuildChainTest.java
+++ b/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/internal/builders/ParallelBuildChainTest.java
@@ -88,8 +88,8 @@
 
 	@Test
 	public void testIndividualProjectBuildsInParallelNoConflict() throws CoreException, OperationCanceledException, InterruptedException {
-		int projectsCount = getWorkspace().getRoot().getProjects().length;
-		JobGroup group = new JobGroup("Build Group", 5, projectsCount);
+		long duration = System.currentTimeMillis();
+		JobGroup group = new JobGroup("Build Group", 5, getWorkspace().getRoot().getProjects().length);
 		for (IProject project : getWorkspace().getRoot().getProjects()) {
 			Job job = new Job("Building " + project) {
 				@Override
@@ -106,13 +106,37 @@
 			job.schedule();
 		}
 		Assert.assertTrue("Timeout, most likely a deadlock", group.join(5000, getMonitor()));
+		duration = System.currentTimeMillis() - duration;
 		assertEquals(getWorkspace().getRoot().getProjects().length, TimerBuilder.getTotalBuilds());
+		assertTrue(TimerBuilder.getMaxSimultaneousBuilds() >= 3);
+		assertTrue(duration < 3000);
 	}
 
 	@Test
 	public void testIndividualProjectBuildsInParallelProjectScheduling() throws CoreException, OperationCanceledException, InterruptedException {
-		setTimerBuilderSchedulingRuleForAllProjects(RuleType.CURRENT_PROJECT, getMonitor());
-		testIndividualProjectBuildsInParallelNoConflict();
+		setTimerBuilderSchedulingRuleForAllProjects(RuleType.CURRENT_PROJECT_RELAXED, getMonitor());
+		long duration = System.currentTimeMillis();
+		JobGroup group = new JobGroup("Build Group", 5, getWorkspace().getRoot().getProjects().length);
+		for (IProject project : getWorkspace().getRoot().getProjects()) {
+			Job job = new Job("Building " + project) {
+				@Override
+				public IStatus run(IProgressMonitor monitor) {
+					try {
+						project.build(IncrementalProjectBuilder.FULL_BUILD, monitor);
+						return Status.OK_STATUS;
+					} catch (CoreException e) {
+						return new Status(IStatus.ERROR, "org.eclipse.core.tests.resources", e.getMessage(), e);
+					}
+				}
+			};
+			job.setJobGroup(group);
+			job.schedule();
+		}
+		Assert.assertTrue("Timeout, most likely a deadlock", group.join(5000, getMonitor()));
+		duration = System.currentTimeMillis() - duration;
+		assertEquals(getWorkspace().getRoot().getProjects().length, TimerBuilder.getTotalBuilds());
+		assertTrue(TimerBuilder.getMaxSimultaneousBuilds() >= 3);
+		assertTrue(duration < 3000);
 	}
 
 }
diff --git a/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/internal/builders/TimerBuilder.java b/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/internal/builders/TimerBuilder.java
index 8b23c87..7ae0b2c 100644
--- a/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/internal/builders/TimerBuilder.java
+++ b/tests/org.eclipse.core.tests.resources/src/org/eclipse/core/tests/internal/builders/TimerBuilder.java
@@ -11,8 +11,7 @@
 package org.eclipse.core.tests.internal.builders;
 
 import java.util.Map;
-import org.eclipse.core.resources.IProject;
-import org.eclipse.core.resources.IncrementalProjectBuilder;
+import org.eclipse.core.resources.*;
 import org.eclipse.core.runtime.CoreException;
 import org.eclipse.core.runtime.IProgressMonitor;
 import org.eclipse.core.runtime.jobs.ISchedulingRule;
@@ -30,20 +29,33 @@
 	private static int maxSimultaneousBuilds = 0;
 
 	public static enum RuleType {
-		NO_CONFLICT, CURRENT_PROJECT, WORKSPACE_ROOT
+		NO_CONFLICT, CURRENT_PROJECT, WORKSPACE_ROOT, NO_CONFLICT_RELAXED, CURRENT_PROJECT_RELAXED;
 	}
 
-	private final ISchedulingRule noConflictRule = new ISchedulingRule() {
-			@Override
-			public boolean isConflicting(ISchedulingRule rule) {
-				return this == rule;
-			}
+	final ISchedulingRule noConflictRule = new ISchedulingRule() {
+		@Override
+		public boolean isConflicting(ISchedulingRule rule) {
+			return this == rule;
+		}
 
-			@Override
-			public boolean contains(ISchedulingRule rule) {
-				return this == rule;
-			}
-		};
+		@Override
+		public boolean contains(ISchedulingRule rule) {
+			return this == rule;
+		}
+	};
+
+	final ISchedulingRule relaxedProjetRule = new ISchedulingRule() {
+
+		@Override
+		public boolean isConflicting(ISchedulingRule rule) {
+			return this == rule;
+		}
+
+		@Override
+		public boolean contains(ISchedulingRule rule) {
+			return this == rule || ResourcesPlugin.getWorkspace().getRoot().contains(rule);
+		}
+	};
 
 	@Override
 	protected IProject[] build(int kind, Map<String, String> args, IProgressMonitor monitor) throws CoreException {
@@ -76,6 +88,8 @@
 					return getProject();
 				case WORKSPACE_ROOT :
 					return getProject().getWorkspace().getRoot();
+				case CURRENT_PROJECT_RELAXED :
+					return relaxedProjetRule;
 			}
 		}
 		return noConflictRule;