diff --git a/core/src/main/java/hudson/model/AbstractProject.java b/core/src/main/java/hudson/model/AbstractProject.java
index 21bccd469c0a36b7f3c750a10e8be71b2fd176ee..3da512977977911e627f0b9a1a0202535bd38515 100644
--- a/core/src/main/java/hudson/model/AbstractProject.java
+++ b/core/src/main/java/hudson/model/AbstractProject.java
@@ -67,6 +67,7 @@ import hudson.scm.SCMS;
import hudson.search.SearchIndexBuilder;
import hudson.security.ACL;
import hudson.security.Permission;
+import hudson.slaves.Cloud;
import hudson.slaves.WorkspaceList;
import hudson.tasks.BuildStep;
import hudson.tasks.BuildStepDescriptor;
@@ -1375,7 +1376,6 @@ public abstract class AbstractProject
,R extends A
// At this point we start thinking about triggering a build just to get a workspace,
// because otherwise there's no way we can detect changes.
// However, first there are some conditions in which we do not want to do so.
-
// give time for slaves to come online if we are right after reconnection (JENKINS-8408)
long running = Jenkins.getInstance().getInjector().getInstance(Uptime.class).getUptime();
long remaining = TimeUnit2.MINUTES.toMillis(10)-running;
@@ -1385,6 +1385,14 @@ public abstract class AbstractProject
,R extends A
return NO_CHANGES;
}
+ // Do not trigger build, if no suitable slave is online
+ if (workspaceOfflineReason.equals(WorkspaceOfflineReason.all_suitable_nodes_are_offline)) {
+ // No suitable executor is online
+ listener.getLogger().print(Messages.AbstractProject_AwaitingWorkspaceToComeOnline(running/1000));
+ listener.getLogger().println( " (" + workspaceOfflineReason.name() + ")");
+ return NO_CHANGES;
+ }
+
Label label = getAssignedLabel();
if (label != null && label.isSelfLabel()) {
// if the build is fixed on a node, then attempting a build will do us
@@ -1409,12 +1417,11 @@ public abstract class AbstractProject
,R extends A
} else {
WorkspaceList l = b.getBuiltOn().toComputer().getWorkspaceList();
return pollWithWorkspace(listener, scm, b, ws, l);
-
}
+
} else {
// polling without workspace
LOGGER.fine("Polling SCM changes of " + getName());
-
if (pollingBaseline==null) // see NOTE-NO-BASELINE above
calcPollingBaseline(getLastBuild(),null,listener);
PollingResult r = scm.poll(this, null, null, listener, pollingBaseline);
@@ -1449,11 +1456,50 @@ public abstract class AbstractProject
,R extends A
enum WorkspaceOfflineReason {
nonexisting_workspace,
builton_node_gone,
- builton_node_no_executors
+ builton_node_no_executors,
+ all_suitable_nodes_are_offline,
+ use_ondemand_slave
+ }
+
+ /**
+ * Returns true if all suitable nodes for the job are offline.
+ *
+ */
+
+ private boolean isAllSuitableNodesOffline(R build) {
+ Label label = getAssignedLabel();
+ List allNodes = Jenkins.getInstance().getNodes();
+
+ if (allNodes.isEmpty() && !(label == Jenkins.getInstance().getSelfLabel())) {
+ // no master/slave. pointless to talk about nodes
+ label = null;
+ }
+
+ if (label != null) {
+ return label.isOffline();
+ } else {
+ if (canRoam) {
+ for (Node n : Jenkins.getInstance().getNodes()) {
+ Computer c = n.toComputer();
+ if (c != null && c.isOnline() && c.isAcceptingTasks()) {
+ // Some executor is ready and this job can run anywhere
+ return false;
+ }
+ }
+ }
+ }
+ return true;
}
private WorkspaceOfflineReason workspaceOffline(R build) throws IOException, InterruptedException {
FilePath ws = build.getWorkspace();
+ Label label = getAssignedLabel();
+
+ if (isAllSuitableNodesOffline(build)) {
+ Collection applicableClouds = label == null ? Jenkins.getInstance().clouds : label.getClouds();
+ return applicableClouds.isEmpty() ? WorkspaceOfflineReason.all_suitable_nodes_are_offline : WorkspaceOfflineReason.use_ondemand_slave;
+ }
+
if (ws==null || !ws.exists()) {
return WorkspaceOfflineReason.nonexisting_workspace;
}
diff --git a/test/src/main/java/org/jvnet/hudson/test/JenkinsRule.java b/test/src/main/java/org/jvnet/hudson/test/JenkinsRule.java
index d8cb0e006b309ee6cc1d343919c9d347ee13fe54..766ee61c2d3f840ac7d64f3f141dd113075f4b2a 100644
--- a/test/src/main/java/org/jvnet/hudson/test/JenkinsRule.java
+++ b/test/src/main/java/org/jvnet/hudson/test/JenkinsRule.java
@@ -876,7 +876,7 @@ public class JenkinsRule implements TestRule, MethodRule, RootAction {
public DumbSlave createSlave(String nodeName, String labels, EnvVars env) throws Exception {
synchronized (jenkins) {
DumbSlave slave = new DumbSlave(nodeName, "dummy",
- createTmpDir().getPath(), "1", Node.Mode.NORMAL, labels==null?"":labels, createComputerLauncher(env), RetentionStrategy.NOOP, Collections.EMPTY_LIST);
+ createTmpDir().getPath(), "1", Node.Mode.NORMAL, labels==null?"":labels, createComputerLauncher(env), RetentionStrategy.NOOP, Collections.EMPTY_LIST);
jenkins.addNode(slave);
return slave;
}
diff --git a/test/src/test/java/hudson/model/ProjectTest.java b/test/src/test/java/hudson/model/ProjectTest.java
index 48a7e9578536b0d00f739fdcb4bef92eac050bae..823321d777f21f01525c60b0d578955ef829be70 100644
--- a/test/src/test/java/hudson/model/ProjectTest.java
+++ b/test/src/test/java/hudson/model/ProjectTest.java
@@ -60,6 +60,12 @@ import java.io.File;
import hudson.FilePath;
import hudson.slaves.EnvironmentVariablesNodeProperty;
import hudson.EnvVars;
+import hudson.model.labels.LabelAtom;
+import hudson.slaves.Cloud;
+import hudson.slaves.DumbSlave;
+import hudson.slaves.DummyCloudImpl;
+import hudson.slaves.NodeProvisioner;
+import hudson.slaves.OfflineCause;
import hudson.tasks.Shell;
import org.jvnet.hudson.test.TestExtension;
import java.util.List;
@@ -74,12 +80,17 @@ import static org.junit.Assert.*;
import hudson.tasks.Fingerprinter;
import hudson.tasks.ArtifactArchiver;
import hudson.tasks.BuildTrigger;
+import hudson.util.OneShotEvent;
import java.util.Map;
+import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
import org.junit.Ignore;
+import org.jvnet.localizer.Localizable;
+import sun.awt.image.OffScreenImage;
/**
*
@@ -637,6 +648,110 @@ public class ProjectTest {
assertFalse("Project should be enabled.", project.isDisabled());
}
+ /**
+ * Job is un-restricted (no nabel), this is submitted to queue, which spawns an on demand slave
+ * @throws Exception
+ */
+ @Test
+ public void testJobSubmittedShouldSpawnCloud() throws Exception {
+ /**
+ * Setup a project with an SCM. Jenkins should have no executors in itself.
+ */
+ FreeStyleProject proj = j.createFreeStyleProject("JENKINS-21394-spawn");
+ RequiresWorkspaceSCM requiresWorkspaceScm = new RequiresWorkspaceSCM(true);
+ proj.setScm(requiresWorkspaceScm);
+ j.jenkins.setNumExecutors(0);
+ /*
+ * We have a cloud
+ */
+ DummyCloudImpl2 c2 = new DummyCloudImpl2(j, 0);
+ c2.label = new LabelAtom("test-cloud-label");
+ j.jenkins.clouds.add(c2);
+
+ SCMTrigger t = new SCMTrigger("@daily", true);
+ t.start(proj, true);
+ proj.addTrigger(t);
+ t.new Runner().run();
+
+ Thread.sleep(1000);
+ //Assert that the job IS submitted to Queue.
+ assertEquals(1, j.jenkins.getQueue().getItems().length);
+ }
+
+ /**
+ * Job is restricted, but label can not be provided by any cloud, only normal slaves. Then job will not submit, because no slave is available.
+ * @throws Exception
+ */
+ @Test
+ public void testUnrestrictedJobNoLabelByCloudNoQueue() throws Exception {
+ assertTrue(j.jenkins.clouds.isEmpty());
+ //Create slave. (Online)
+ Slave s1 = j.createOnlineSlave();
+
+ //Create a project, and bind the job to the created slave
+ FreeStyleProject proj = j.createFreeStyleProject("JENKINS-21394-noqueue");
+ proj.setAssignedLabel(s1.getSelfLabel());
+
+ //Add an SCM to the project. We require a workspace for the poll
+ RequiresWorkspaceSCM requiresWorkspaceScm = new RequiresWorkspaceSCM(true);
+ proj.setScm(requiresWorkspaceScm);
+
+ j.buildAndAssertSuccess(proj);
+
+ //Now create another slave. And restrict the job to that slave. The slave is offline, leaving the job with no assignable nodes.
+ //We tell our mock SCM to return that it has got changes. But since there are no slaves, we get the desired result.
+ Slave s2 = j.createSlave();
+ proj.setAssignedLabel(s2.getSelfLabel());
+ requiresWorkspaceScm.hasChange = true;
+
+ //Poll (We now should have NO online slaves, this should now return NO_CHANGES.
+ PollingResult pr = proj.poll(j.createTaskListener());
+ assertFalse(pr.hasChanges());
+
+ SCMTrigger t = new SCMTrigger("@daily", true);
+ t.start(proj, true);
+ proj.addTrigger(t);
+
+ t.new Runner().run();
+
+ /**
+ * Assert that the log contains the correct message.
+ */
+ HtmlPage log = j.createWebClient().getPage(proj, "scmPollLog");
+ String logastext = log.asText();
+ assertTrue(logastext.contains("(" + AbstractProject.WorkspaceOfflineReason.all_suitable_nodes_are_offline.name() + ")"));
+
+ }
+
+ /**
+ * Job is restricted. Label is on slave that can be started in cloud. Job is submitted to queue, which spawns an on demand slave.
+ * @throws Exception
+ */
+ @Test
+ public void testRestrictedLabelOnSlaveYesQueue() throws Exception {
+ FreeStyleProject proj = j.createFreeStyleProject("JENKINS-21394-yesqueue");
+ RequiresWorkspaceSCM requiresWorkspaceScm = new RequiresWorkspaceSCM(true);
+ proj.setScm(requiresWorkspaceScm);
+ j.jenkins.setNumExecutors(0);
+
+ /*
+ * We have a cloud
+ */
+ DummyCloudImpl2 c2 = new DummyCloudImpl2(j, 0);
+ c2.label = new LabelAtom("test-cloud-label");
+ j.jenkins.clouds.add(c2);
+ proj.setAssignedLabel(c2.label);
+
+ SCMTrigger t = new SCMTrigger("@daily", true);
+ t.start(proj, true);
+ proj.addTrigger(t);
+ t.new Runner().run();
+
+ Thread.sleep(1000);
+ //The job should be in queue
+ assertEquals(1, j.jenkins.getQueue().getItems().length);
+ }
+
public static class TransientAction extends InvisibleAction{
}
@@ -654,6 +769,36 @@ public class ProjectTest {
}
+ @TestExtension
+ public static class RequiresWorkspaceSCM extends NullSCM {
+
+ public boolean hasChange = false;
+
+ public RequiresWorkspaceSCM() { }
+
+ public RequiresWorkspaceSCM(boolean hasChange) {
+ this.hasChange = hasChange;
+ }
+
+ @Override
+ public boolean pollChanges(AbstractProject, ?> project, Launcher launcher, FilePath workspace, TaskListener listener) throws IOException, InterruptedException {
+ return true;
+ }
+
+ @Override
+ public boolean requiresWorkspaceForPolling(){
+ return true;
+ }
+
+ @Override
+ protected PollingResult compareRemoteRevisionWith(AbstractProject project, Launcher launcher, FilePath workspace, TaskListener listener, SCMRevisionState baseline) throws IOException, InterruptedException {
+ if(!hasChange) {
+ return PollingResult.NO_CHANGES;
+ }
+ return PollingResult.SIGNIFICANT;
+ }
+ }
+
@TestExtension
public static class AlwaysChangedSCM extends NullSCM{
@@ -661,6 +806,7 @@ public class ProjectTest {
public boolean pollChanges(AbstractProject, ?> project, Launcher launcher, FilePath workspace, TaskListener listener) throws IOException, InterruptedException {
return true;
}
+
@Override
public boolean requiresWorkspaceForPolling(){
return false;
@@ -748,4 +894,94 @@ public class ProjectTest {
public class ActionImpl extends InvisibleAction{
}
+
+ @TestExtension
+ public static class DummyCloudImpl2 extends Cloud {
+ private final transient JenkinsRule caller;
+
+ /**
+ * Configurable delay between the {@link Cloud#provision(Label,int)} and the actual launch of a slave,
+ * to emulate a real cloud that takes some time for provisioning a new system.
+ *
+ *
+ * Number of milliseconds.
+ */
+ private final int delay;
+
+ // stats counter to perform assertions later
+ public int numProvisioned;
+
+ /**
+ * Only reacts to provisioning for this label.
+ */
+ public Label label;
+
+ public DummyCloudImpl2() {
+ super("test");
+ this.delay = 0;
+ this.caller = null;
+ }
+
+ public DummyCloudImpl2(JenkinsRule caller, int delay) {
+ super("test");
+ this.caller = caller;
+ this.delay = delay;
+ }
+
+ @Override
+ public Collection provision(Label label, int excessWorkload) {
+ List r = new ArrayList();
+
+ //Always provision...even if there is no workload.
+ while(excessWorkload >= 0) {
+ System.out.println("Provisioning");
+ numProvisioned++;
+ Future f = Computer.threadPoolForRemoting.submit(new ProjectTest.DummyCloudImpl2.Launcher(delay));
+ r.add(new NodeProvisioner.PlannedNode(name+" #"+numProvisioned,f,1));
+ excessWorkload-=1;
+ }
+ return r;
+ }
+
+ @Override
+ public boolean canProvision(Label label) {
+ //This cloud can ALWAYS provision
+ return true;
+ /* return label==this.label; */
+ }
+
+ private final class Launcher implements Callable {
+ private final long time;
+ /**
+ * This is so that we can find out the status of Callable from the debugger.
+ */
+ private volatile Computer computer;
+
+ private Launcher(long time) {
+ this.time = time;
+ }
+
+ @Override
+ public Node call() throws Exception {
+ // simulate the delay in provisioning a new slave,
+ // since it's normally some async operation.
+ Thread.sleep(time);
+
+ System.out.println("launching slave");
+ DumbSlave slave = caller.createSlave(label);
+ computer = slave.toComputer();
+ computer.connect(false).get();
+ synchronized (ProjectTest.DummyCloudImpl2.this) {
+ System.out.println(computer.getName()+" launch"+(computer.isOnline()?"ed successfully":" failed"));
+ System.out.println(computer.getLog());
+ }
+ return slave;
+ }
+ }
+
+ @Override
+ public Descriptor getDescriptor() {
+ throw new UnsupportedOperationException();
+ }
+ }
}