From 44adeb54df884757dd579efae976840a411ae863 Mon Sep 17 00:00:00 2001 From: Nick Bulleid Date: Fri, 1 Mar 2024 09:48:32 +0000 Subject: [PATCH] Add PauseAction when throttling pipelines The PauseAction is used by pipeline visualisers (such as pipeline-stage-view) to indicate when a stage has been paused, and for how long is was staged for. This can improve the display of stages timings, as the paused time is removed from the overall stage time. The PauseAction only applies to stages, so only applies when the throttle step is used in conjunction with a pipeline stage. --- pom.xml | 6 ++ .../ThrottleQueueTaskDispatcher.java | 57 +++++++++++++++---- .../plugins/throttleconcurrents/TestUtil.java | 8 +++ .../throttleconcurrents/ThrottleStepTest.java | 12 ++++ 4 files changed, 71 insertions(+), 12 deletions(-) diff --git a/pom.xml b/pom.xml index 98f06a00..0e1f99c9 100644 --- a/pom.xml +++ b/pom.xml @@ -151,6 +151,12 @@ THE SOFTWARE. matrix-auth test + + org.jenkins-ci.plugins.pipeline-stage-view + pipeline-stage-view + 2.13 + test + org.jenkins-ci.plugins.workflow workflow-basic-steps diff --git a/src/main/java/hudson/plugins/throttleconcurrents/ThrottleQueueTaskDispatcher.java b/src/main/java/hudson/plugins/throttleconcurrents/ThrottleQueueTaskDispatcher.java index 4da52f63..597ccfc8 100644 --- a/src/main/java/hudson/plugins/throttleconcurrents/ThrottleQueueTaskDispatcher.java +++ b/src/main/java/hudson/plugins/throttleconcurrents/ThrottleQueueTaskDispatcher.java @@ -41,6 +41,7 @@ import org.jenkinsci.plugins.workflow.graph.StepNode; import org.jenkinsci.plugins.workflow.graphanalysis.LinearBlockHoppingScanner; import org.jenkinsci.plugins.workflow.steps.StepDescriptor; +import org.jenkinsci.plugins.workflow.support.actions.PauseAction; import org.jenkinsci.plugins.workflow.support.concurrent.Timeout; import org.jenkinsci.plugins.workflow.support.steps.ExecutorStepExecution.PlaceholderTask; @@ -54,14 +55,17 @@ public class ThrottleQueueTaskDispatcher extends QueueTaskDispatcher { @Deprecated @Override public @CheckForNull CauseOfBlockage canTake(Node node, Task task) { + CauseOfBlockage cause = null; if (Jenkins.getAuthentication().equals(ACL.SYSTEM)) { - return canTakeImpl(node, task); - } - - // Throttle-concurrent-builds requires READ permissions for all projects. - try (ACLContext ctx = ACL.as(ACL.SYSTEM)) { - return canTakeImpl(node, task); + cause = canTakeImpl(node, task); + } else { + // Throttle-concurrent-builds requires READ permissions for all projects. + try (ACLContext ctx = ACL.as(ACL.SYSTEM)) { + cause = canTakeImpl(node, task); + } } + updatePauseAction(task, cause); + return cause; } private CauseOfBlockage canTakeImpl(Node node, Task task) { @@ -218,14 +222,17 @@ private boolean shouldBeThrottled(@NonNull Task task, @CheckForNull ThrottleJobP } private CauseOfBlockage canRun(Task task, ThrottleJobProperty tjp, List pipelineCategories) { + CauseOfBlockage cause = null; if (Jenkins.getAuthentication().equals(ACL.SYSTEM)) { - return canRunImpl(task, tjp, pipelineCategories); - } - - // Throttle-concurrent-builds requires READ permissions for all projects. - try (ACLContext ctx = ACL.as(ACL.SYSTEM)) { - return canRunImpl(task, tjp, pipelineCategories); + cause = canRunImpl(task, tjp, pipelineCategories); + } else { + // Throttle-concurrent-builds requires READ permissions for all projects. + try (ACLContext ctx = ACL.as(ACL.SYSTEM)) { + cause = canRunImpl(task, tjp, pipelineCategories); + } } + updatePauseAction(task, cause); + return cause; } private CauseOfBlockage canRunImpl(Task task, ThrottleJobProperty tjp, List pipelineCategories) { @@ -695,5 +702,31 @@ private int getMaxConcurrentPerNodeBasedOnMatchingLabels( return maxConcurrentPerNodeLabeledIfMatch; } + private void updatePauseAction(Task task, CauseOfBlockage cause) { + if (task instanceof PlaceholderTask) { + PlaceholderTask placeholderTask = (PlaceholderTask) task; + try { + FlowNode flowNode = placeholderTask.getNode(); + if (flowNode == null) { + return; + } + + if (cause != null) { + if (PauseAction.getCurrentPause(flowNode) == null) { + flowNode.addAction(new PauseAction(cause.getShortDescription())); + } + } else { + if (PauseAction.getCurrentPause(flowNode) != null) { + PauseAction.endCurrentPause(flowNode); + } + } + } catch (IOException | InterruptedException e) { + LOGGER.log(Level.WARNING, "Error setting pause action on pipeline {0}: {1}", new Object[] { + task.getDisplayName(), e + }); + } + } + } + private static final Logger LOGGER = Logger.getLogger(ThrottleQueueTaskDispatcher.class.getName()); } diff --git a/src/test/java/hudson/plugins/throttleconcurrents/TestUtil.java b/src/test/java/hudson/plugins/throttleconcurrents/TestUtil.java index 37929815..fd95de0e 100644 --- a/src/test/java/hudson/plugins/throttleconcurrents/TestUtil.java +++ b/src/test/java/hudson/plugins/throttleconcurrents/TestUtil.java @@ -8,6 +8,7 @@ import hudson.model.Computer; import hudson.model.Executor; import hudson.model.Node; +import hudson.model.Queue; import hudson.model.queue.CauseOfBlockage; import hudson.slaves.DumbSlave; import hudson.slaves.RetentionStrategy; @@ -17,6 +18,7 @@ import java.util.Set; import jenkins.model.queue.CompositeCauseOfBlockage; import org.jenkinsci.plugins.workflow.job.WorkflowRun; +import org.jenkinsci.plugins.workflow.support.actions.PauseAction; import org.jenkinsci.plugins.workflow.support.steps.ExecutorStepExecution; import org.junit.rules.TemporaryFolder; import org.jvnet.hudson.test.JenkinsRule; @@ -96,6 +98,12 @@ static Set getBlockageReasons(CauseOfBlockage cob) { } } + static void hasPauseActionForItem(Queue.Item item) throws Exception { + assertTrue(item.task instanceof ExecutorStepExecution.PlaceholderTask); + ExecutorStepExecution.PlaceholderTask task = (ExecutorStepExecution.PlaceholderTask) item.task; + assertNotNull(task.getNode().getAction(PauseAction.class)); + } + static void hasPlaceholderTaskForRun(Node n, WorkflowRun r) throws Exception { for (Executor exec : n.toComputer().getExecutors()) { if (exec.getCurrentExecutable() != null) { diff --git a/src/test/java/hudson/plugins/throttleconcurrents/ThrottleStepTest.java b/src/test/java/hudson/plugins/throttleconcurrents/ThrottleStepTest.java index 07cec6d8..0a8c3b3e 100644 --- a/src/test/java/hudson/plugins/throttleconcurrents/ThrottleStepTest.java +++ b/src/test/java/hudson/plugins/throttleconcurrents/ThrottleStepTest.java @@ -95,6 +95,7 @@ public void onePerNode() throws Exception { blockageReasons, hasItem(Messages._ThrottleQueueTaskDispatcher_MaxCapacityOnNode(1) .toString())); + TestUtil.hasPauseActionForItem(queuedItem); assertEquals(1, agent.toComputer().countBusy()); TestUtil.hasPlaceholderTaskForRun(agent, firstJobFirstRun); @@ -176,6 +177,8 @@ public void multipleCategories() throws Exception { WorkflowRun thirdJobFirstRun = thirdJob.scheduleBuild2(0).waitForStart(); j.waitForMessage("Still waiting to schedule task", thirdJobFirstRun); assertFalse(j.jenkins.getQueue().isEmpty()); + Queue.Item queuedItem = j.jenkins.getQueue().getItems()[0]; + TestUtil.hasPauseActionForItem(queuedItem); assertEquals(1, firstAgent.toComputer().countBusy()); TestUtil.hasPlaceholderTaskForRun(firstAgent, firstJobFirstRun); @@ -245,6 +248,9 @@ public void onePerNodeParallel() throws Exception { j.waitForMessage("Still waiting to schedule task", run1); j.waitForMessage("Still waiting to schedule task", run2); + Queue.Item queuedItem = j.jenkins.getQueue().getItems()[0]; + TestUtil.hasPauseActionForItem(queuedItem); + SemaphoreStep.success("wait-first-branch-a-job/1", null); SemaphoreStep.waitForStart("wait-first-branch-c-job/1", run1); assertEquals(1, firstAgent.toComputer().countBusy()); @@ -293,6 +299,8 @@ public void twoTotal() throws Exception { j.waitForMessage("Still waiting to schedule task", thirdJobFirstRun); j.jenkins.getQueue().maintain(); assertFalse(j.jenkins.getQueue().isEmpty()); + Queue.Item queuedItem = j.jenkins.getQueue().getItems()[0]; + TestUtil.hasPauseActionForItem(queuedItem); assertEquals(1, firstAgent.toComputer().countBusy()); TestUtil.hasPlaceholderTaskForRun(firstAgent, firstJobFirstRun); @@ -377,6 +385,8 @@ public boolean perform(AbstractBuild build, Launcher launcher, BuildListen WorkflowRun secondJobFirstRun = secondJob.scheduleBuild2(0).waitForStart(); j.waitForMessage("Still waiting to schedule task", secondJobFirstRun); assertFalse(j.jenkins.getQueue().isEmpty()); + Queue.Item queuedItem = j.jenkins.getQueue().getItems()[0]; + TestUtil.hasPauseActionForItem(queuedItem); assertEquals(1, agent.toComputer().countBusy()); for (Executor e : agent.toComputer().getExecutors()) { @@ -460,6 +470,7 @@ private String getThrottleScript(String jobName, List categories, String return "throttle([" + StringUtils.join(quoted, ", ") + "]) {\n" + + "stage('wait') {\n" + " echo 'hi there'\n" + " node('" + label @@ -468,6 +479,7 @@ private String getThrottleScript(String jobName, List categories, String + jobName + "-job'\n" + " }\n" + + "}\n" + "}\n"; }