package hudson.plugins.throttleconcurrents; import hudson.Launcher; import hudson.model.AbstractBuild; import hudson.model.BuildListener; import hudson.model.Computer; import hudson.model.Executor; import hudson.model.FreeStyleBuild; import hudson.model.FreeStyleProject; import hudson.model.Label; import hudson.model.Node; import hudson.model.Queue; import hudson.model.Result; import hudson.model.queue.QueueTaskFuture; import hudson.plugins.throttleconcurrents.pipeline.ThrottleStep; import hudson.slaves.DumbSlave; import hudson.slaves.NodeProperty; import hudson.slaves.RetentionStrategy; import org.apache.commons.lang.StringUtils; import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition; import org.jenkinsci.plugins.workflow.cps.SnippetizerTester; import org.jenkinsci.plugins.workflow.job.WorkflowJob; import org.jenkinsci.plugins.workflow.job.WorkflowRun; import org.jenkinsci.plugins.workflow.support.steps.ExecutorStepExecution; import org.jenkinsci.plugins.workflow.test.steps.SemaphoreStep; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.junit.runners.model.Statement; import org.jvnet.hudson.test.BuildWatcher; import org.jvnet.hudson.test.RestartableJenkinsRule; import org.jvnet.hudson.test.TestBuilder; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.concurrent.Semaphore; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; public class ThrottleStepTest { private static final String ONE_PER_NODE = "one_per_node"; private static final String OTHER_ONE_PER_NODE = "other_one_per_node"; private static final String TWO_TOTAL = "two_total"; @Rule public RestartableJenkinsRule story = new RestartableJenkinsRule(); @ClassRule public static BuildWatcher buildWatcher = new BuildWatcher(); @Rule public TemporaryFolder firstAgentTmp = new TemporaryFolder(); @Rule public TemporaryFolder secondAgentTmp = new TemporaryFolder(); public void setupAgentsAndCategories() throws Exception { DumbSlave firstAgent = new DumbSlave("first-agent", "dummy agent", firstAgentTmp.getRoot().getAbsolutePath(), "4", Node.Mode.NORMAL, "on-agent", story.j.createComputerLauncher(null), RetentionStrategy.NOOP, Collections.<NodeProperty<?>>emptyList()); DumbSlave secondAgent = new DumbSlave("second-agent", "dummy agent", secondAgentTmp.getRoot().getAbsolutePath(), "4", Node.Mode.NORMAL, "on-agent", story.j.createComputerLauncher(null), RetentionStrategy.NOOP, Collections.<NodeProperty<?>>emptyList()); story.j.jenkins.addNode(firstAgent); story.j.jenkins.addNode(secondAgent); ThrottleJobProperty.ThrottleCategory firstCat = new ThrottleJobProperty.ThrottleCategory(ONE_PER_NODE, 1, 0, null); ThrottleJobProperty.ThrottleCategory secondCat = new ThrottleJobProperty.ThrottleCategory(TWO_TOTAL, 0, 2, null); ThrottleJobProperty.ThrottleCategory thirdCat = new ThrottleJobProperty.ThrottleCategory(OTHER_ONE_PER_NODE, 1, 0, null); ThrottleJobProperty.DescriptorImpl descriptor = story.j.jenkins.getDescriptorByType(ThrottleJobProperty.DescriptorImpl.class); assertNotNull(descriptor); descriptor.setCategories(Arrays.asList(firstCat, secondCat, thirdCat)); } @Test public void onePerNode() throws Exception { story.addStep(new Statement() { @Override public void evaluate() throws Throwable { setupAgentsAndCategories(); WorkflowJob firstJob = story.j.jenkins.createProject(WorkflowJob.class, "first-job"); firstJob.setDefinition(getJobFlow("first", ONE_PER_NODE, "first-agent")); WorkflowRun firstJobFirstRun = firstJob.scheduleBuild2(0).waitForStart(); SemaphoreStep.waitForStart("wait-first-job/1", firstJobFirstRun); WorkflowJob secondJob = story.j.jenkins.createProject(WorkflowJob.class, "second-job"); secondJob.setDefinition(getJobFlow("second", ONE_PER_NODE, "first-agent")); WorkflowRun secondJobFirstRun = secondJob.scheduleBuild2(0).waitForStart(); story.j.waitForMessage("Still waiting to schedule task", secondJobFirstRun); assertFalse(story.j.jenkins.getQueue().isEmpty()); Node n = story.j.jenkins.getNode("first-agent"); assertNotNull(n); assertEquals(1, n.toComputer().countBusy()); hasPlaceholderTaskForRun(n, firstJobFirstRun); SemaphoreStep.success("wait-first-job/1", null); story.j.assertBuildStatusSuccess(story.j.waitForCompletion(firstJobFirstRun)); SemaphoreStep.waitForStart("wait-second-job/1", secondJobFirstRun); assertTrue(story.j.jenkins.getQueue().isEmpty()); assertEquals(1, n.toComputer().countBusy()); hasPlaceholderTaskForRun(n, secondJobFirstRun); SemaphoreStep.success("wait-second-job/1", null); story.j.assertBuildStatusSuccess(story.j.waitForCompletion(secondJobFirstRun)); } }); } @Test public void duplicateCategories() throws Exception { story.addStep(new Statement() { @Override public void evaluate() throws Throwable { setupAgentsAndCategories(); WorkflowJob j = story.j.jenkins.createProject(WorkflowJob.class, "first-job"); j.setDefinition(new CpsFlowDefinition("throttle(['" + ONE_PER_NODE + "', '" + ONE_PER_NODE +"']) { echo 'Hello' }", false)); WorkflowRun b = j.scheduleBuild2(0).waitForStart(); story.j.assertBuildStatusSuccess(story.j.waitForCompletion(b)); story.j.assertLogContains("One or more duplicate categories (" + ONE_PER_NODE + ") specified. Duplicates will be ignored.", b); story.j.assertLogContains("Hello", b); } }); } @Test public void undefinedCategories() throws Exception { story.addStep(new Statement() { @Override public void evaluate() throws Throwable { WorkflowJob j = story.j.jenkins.createProject(WorkflowJob.class, "first-job"); j.setDefinition(new CpsFlowDefinition("throttle(['undefined', 'also-undefined']) { echo 'Hello' }", false)); WorkflowRun b = j.scheduleBuild2(0).waitForStart(); story.j.assertBuildStatus(Result.FAILURE, story.j.waitForCompletion(b)); story.j.assertLogContains("One or more specified categories do not exist: undefined, also-undefined", b); story.j.assertLogNotContains("Hello", b); } }); } @Test public void multipleCategories() throws Exception { story.addStep(new Statement() { @Override public void evaluate() throws Throwable { setupAgentsAndCategories(); WorkflowJob firstJob = story.j.jenkins.createProject(WorkflowJob.class, "first-job"); firstJob.setDefinition(getJobFlow("first", ONE_PER_NODE, "first-agent")); WorkflowRun firstJobFirstRun = firstJob.scheduleBuild2(0).waitForStart(); SemaphoreStep.waitForStart("wait-first-job/1", firstJobFirstRun); WorkflowJob secondJob = story.j.jenkins.createProject(WorkflowJob.class, "second-job"); secondJob.setDefinition(getJobFlow("second", OTHER_ONE_PER_NODE, "second-agent")); WorkflowRun secondJobFirstRun = secondJob.scheduleBuild2(0).waitForStart(); SemaphoreStep.waitForStart("wait-second-job/1", secondJobFirstRun); WorkflowJob thirdJob = story.j.jenkins.createProject(WorkflowJob.class, "third-job"); thirdJob.setDefinition(getJobFlow("third", Arrays.asList(ONE_PER_NODE, OTHER_ONE_PER_NODE), "on-agent")); WorkflowRun thirdJobFirstRun = thirdJob.scheduleBuild2(0).waitForStart(); story.j.waitForMessage("Still waiting to schedule task", thirdJobFirstRun); assertFalse(story.j.jenkins.getQueue().isEmpty()); Node n = story.j.jenkins.getNode("first-agent"); assertNotNull(n); assertEquals(1, n.toComputer().countBusy()); hasPlaceholderTaskForRun(n, firstJobFirstRun); Node n2 = story.j.jenkins.getNode("second-agent"); assertNotNull(n2); assertEquals(1, n2.toComputer().countBusy()); hasPlaceholderTaskForRun(n2, secondJobFirstRun); SemaphoreStep.success("wait-first-job/1", null); story.j.assertBuildStatusSuccess(story.j.waitForCompletion(firstJobFirstRun)); SemaphoreStep.waitForStart("wait-third-job/1", thirdJobFirstRun); assertTrue(story.j.jenkins.getQueue().isEmpty()); assertEquals(1, n.toComputer().countBusy()); hasPlaceholderTaskForRun(n, thirdJobFirstRun); SemaphoreStep.success("wait-second-job/1", null); story.j.assertBuildStatusSuccess(story.j.waitForCompletion(secondJobFirstRun)); SemaphoreStep.success("wait-third-job/1", null); story.j.assertBuildStatusSuccess(story.j.waitForCompletion(thirdJobFirstRun)); } }); } @Test public void onePerNodeParallel() throws Exception { story.addStep(new Statement() { @Override public void evaluate() throws Throwable { setupAgentsAndCategories(); WorkflowJob firstJob = story.j.jenkins.createProject(WorkflowJob.class, "first-job"); firstJob.setDefinition(new CpsFlowDefinition("parallel(\n" + " a: { " + getThrottleScript("first-branch-a", ONE_PER_NODE, "on-agent") + " },\n" + " b: { " + getThrottleScript("first-branch-b", ONE_PER_NODE, "on-agent") + " },\n" + " c: { " + getThrottleScript("first-branch-c", ONE_PER_NODE, "on-agent") + " }\n" + ")\n", false)); WorkflowRun run1 = firstJob.scheduleBuild2(0).waitForStart(); SemaphoreStep.waitForStart("wait-first-branch-a-job/1", run1); SemaphoreStep.waitForStart("wait-first-branch-b-job/1", run1); WorkflowJob secondJob = story.j.jenkins.createProject(WorkflowJob.class, "second-job"); secondJob.setDefinition(new CpsFlowDefinition("parallel(\n" + " a: { " + getThrottleScript("second-branch-a", ONE_PER_NODE, "on-agent") + " },\n" + " b: { " + getThrottleScript("second-branch-b", ONE_PER_NODE, "on-agent") + " },\n" + " c: { " + getThrottleScript("second-branch-c", ONE_PER_NODE, "on-agent") + " }\n" + ")\n", false)); WorkflowRun run2 = secondJob.scheduleBuild2(0).waitForStart(); Computer first = story.j.jenkins.getNode("first-agent").toComputer(); Computer second = story.j.jenkins.getNode("second-agent").toComputer(); assertEquals(1, first.countBusy()); assertEquals(1, second.countBusy()); story.j.waitForMessage("Still waiting to schedule task", run1); story.j.waitForMessage("Still waiting to schedule task", run2); SemaphoreStep.success("wait-first-branch-a-job/1", null); SemaphoreStep.waitForStart("wait-first-branch-c-job/1", run1); assertEquals(1, first.countBusy()); assertEquals(1, second.countBusy()); SemaphoreStep.success("wait-first-branch-b-job/1", null); SemaphoreStep.waitForStart("wait-second-branch-a-job/1", run1); assertEquals(1, first.countBusy()); assertEquals(1, second.countBusy()); SemaphoreStep.success("wait-first-branch-c-job/1", null); SemaphoreStep.waitForStart("wait-second-branch-b-job/1", run1); assertEquals(1, first.countBusy()); assertEquals(1, second.countBusy()); SemaphoreStep.success("wait-second-branch-a-job/1", null); SemaphoreStep.waitForStart("wait-second-branch-c-job/1", run1); assertEquals(1, first.countBusy()); assertEquals(1, second.countBusy()); SemaphoreStep.success("wait-second-branch-b-job/1", null); SemaphoreStep.success("wait-second-branch-c-job/1", null); story.j.assertBuildStatusSuccess(story.j.waitForCompletion(run1)); story.j.assertBuildStatusSuccess(story.j.waitForCompletion(run2)); } }); } @Test public void twoTotal() throws Exception { story.addStep(new Statement() { @Override public void evaluate() throws Throwable { setupAgentsAndCategories(); WorkflowJob firstJob = story.j.jenkins.createProject(WorkflowJob.class, "first-job"); firstJob.setDefinition(getJobFlow("first", TWO_TOTAL, "first-agent")); WorkflowRun firstJobFirstRun = firstJob.scheduleBuild2(0).waitForStart(); SemaphoreStep.waitForStart("wait-first-job/1", firstJobFirstRun); WorkflowJob secondJob = story.j.jenkins.createProject(WorkflowJob.class, "second-job"); secondJob.setDefinition(getJobFlow("second", TWO_TOTAL, "second-agent")); WorkflowRun secondJobFirstRun = secondJob.scheduleBuild2(0).waitForStart(); SemaphoreStep.waitForStart("wait-second-job/1", secondJobFirstRun); WorkflowJob thirdJob = story.j.jenkins.createProject(WorkflowJob.class, "third-job"); thirdJob.setDefinition(getJobFlow("third", TWO_TOTAL, "on-agent")); WorkflowRun thirdJobFirstRun = thirdJob.scheduleBuild2(0).waitForStart(); story.j.waitForMessage("Still waiting to schedule task", thirdJobFirstRun); assertFalse(story.j.jenkins.getQueue().isEmpty()); Node n = story.j.jenkins.getNode("first-agent"); assertNotNull(n); assertEquals(1, n.toComputer().countBusy()); hasPlaceholderTaskForRun(n, firstJobFirstRun); Node n2 = story.j.jenkins.getNode("second-agent"); assertNotNull(n2); assertEquals(1, n2.toComputer().countBusy()); hasPlaceholderTaskForRun(n2, secondJobFirstRun); SemaphoreStep.success("wait-first-job/1", null); story.j.assertBuildStatusSuccess(story.j.waitForCompletion(firstJobFirstRun)); SemaphoreStep.waitForStart("wait-third-job/1", thirdJobFirstRun); assertTrue(story.j.jenkins.getQueue().isEmpty()); assertEquals(1, n.toComputer().countBusy()); hasPlaceholderTaskForRun(n, thirdJobFirstRun); SemaphoreStep.success("wait-second-job/1", null); story.j.assertBuildStatusSuccess(story.j.waitForCompletion(secondJobFirstRun)); SemaphoreStep.success("wait-third-job/1", null); story.j.assertBuildStatusSuccess(story.j.waitForCompletion(thirdJobFirstRun)); } }); } @Test public void interopWithFreestyle() throws Exception { final Semaphore semaphore = new Semaphore(1); story.addStep(new Statement() { @Override public void evaluate() throws Throwable { setupAgentsAndCategories(); WorkflowJob firstJob = story.j.jenkins.createProject(WorkflowJob.class, "first-job"); firstJob.setDefinition(getJobFlow("first", ONE_PER_NODE, "first-agent")); WorkflowRun firstJobFirstRun = firstJob.scheduleBuild2(0).waitForStart(); SemaphoreStep.waitForStart("wait-first-job/1", firstJobFirstRun); FreeStyleProject freeStyleProject = story.j.createFreeStyleProject("f"); freeStyleProject.addProperty(new ThrottleJobProperty( null, // maxConcurrentPerNode null, // maxConcurrentTotal Arrays.asList(ONE_PER_NODE), // categories true, // throttleEnabled "category", // throttleOption false, null, ThrottleMatrixProjectOptions.DEFAULT )); freeStyleProject.setAssignedLabel(Label.get("first-agent")); freeStyleProject.getBuildersList().add(new TestBuilder() { @Override public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener) throws InterruptedException, IOException { semaphore.acquire(); return true; } }); semaphore.acquire(); QueueTaskFuture<FreeStyleBuild> futureBuild = freeStyleProject.scheduleBuild2(0); assertFalse(story.j.jenkins.getQueue().isEmpty()); assertEquals(1, story.j.jenkins.getQueue().getItems().length); Queue.Item i = story.j.jenkins.getQueue().getItems()[0]; assertTrue(i.task instanceof FreeStyleProject); Node n = story.j.jenkins.getNode("first-agent"); assertNotNull(n); assertEquals(1, n.toComputer().countBusy()); hasPlaceholderTaskForRun(n, firstJobFirstRun); SemaphoreStep.success("wait-first-job/1", null); story.j.assertBuildStatusSuccess(story.j.waitForCompletion(firstJobFirstRun)); FreeStyleBuild freeStyleBuild = futureBuild.waitForStart(); assertEquals(1, n.toComputer().countBusy()); for (Executor e : n.toComputer().getExecutors()) { if (e.isBusy()) { assertEquals(freeStyleBuild, e.getCurrentExecutable()); } } WorkflowJob secondJob = story.j.jenkins.createProject(WorkflowJob.class, "second-job"); secondJob.setDefinition(getJobFlow("second", ONE_PER_NODE, "first-agent")); WorkflowRun secondJobFirstRun = secondJob.scheduleBuild2(0).waitForStart(); story.j.waitForMessage("Still waiting to schedule task", secondJobFirstRun); assertFalse(story.j.jenkins.getQueue().isEmpty()); assertEquals(1, n.toComputer().countBusy()); for (Executor e : n.toComputer().getExecutors()) { if (e.isBusy()) { assertEquals(freeStyleBuild, e.getCurrentExecutable()); } } semaphore.release(); story.j.assertBuildStatusSuccess(story.j.waitForCompletion(freeStyleBuild)); SemaphoreStep.waitForStart("wait-second-job/1", secondJobFirstRun); assertTrue(story.j.jenkins.getQueue().isEmpty()); assertEquals(1, n.toComputer().countBusy()); hasPlaceholderTaskForRun(n, secondJobFirstRun); SemaphoreStep.success("wait-second-job/1", null); story.j.assertBuildStatusSuccess(story.j.waitForCompletion(secondJobFirstRun)); } }); } private CpsFlowDefinition getJobFlow(String jobName, String category, String label) { return getJobFlow(jobName, Collections.singletonList(category), label); } private CpsFlowDefinition getJobFlow(String jobName, List<String> categories, String label) { // This should be sandbox:true, but when I do that, I get org.jenkinsci.plugins.scriptsecurity.sandbox.RejectedAccessException: Scripts not permitted to use method groovy.lang.GroovyObject invokeMethod java.lang.String java.lang.Object // And I cannot figure out why. So for now... return new CpsFlowDefinition(getThrottleScript(jobName, categories, label), false); } private String getThrottleScript(String jobName, String category, String label) { return getThrottleScript(jobName, Collections.singletonList(category), label); } private String getThrottleScript(String jobName, List<String> categories, String label) { List<String> quoted = new ArrayList<>(); for (String c : categories) { quoted.add("'" + c + "'"); } return "throttle([" + StringUtils.join(quoted, ", ") + "]) {\n" + " echo 'hi there'\n" + " node('" + label + "') {\n" + " semaphore 'wait-" + jobName + "-job'\n" + " }\n" + "}\n"; } @Test public void snippetizer() throws Exception { story.addStep(new Statement() { @Override public void evaluate() throws Throwable { setupAgentsAndCategories(); SnippetizerTester st = new SnippetizerTester(story.j); st.assertRoundTrip(new ThrottleStep(Collections.singletonList(ONE_PER_NODE)), "throttle(['" + ONE_PER_NODE + "']) {\n // some block\n}"); } }); } private void hasPlaceholderTaskForRun(Node n, WorkflowRun r) throws Exception { for (Executor exec : n.toComputer().getExecutors()) { if (exec.getCurrentExecutable() != null) { assertTrue(exec.getCurrentExecutable().getParent() instanceof ExecutorStepExecution.PlaceholderTask); ExecutorStepExecution.PlaceholderTask task = (ExecutorStepExecution.PlaceholderTask)exec.getCurrentExecutable().getParent(); assertEquals(r, task.run()); } } } }