Skip to content

Commit

Permalink
Add PauseAction when throttling pipelines
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
nickb-carallon committed Mar 1, 2024
1 parent 85cb312 commit 44adeb5
Show file tree
Hide file tree
Showing 4 changed files with 71 additions and 12 deletions.
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,12 @@ THE SOFTWARE.
<artifactId>matrix-auth</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.pipeline-stage-view</groupId>
<artifactId>pipeline-stage-view</artifactId>
<version>2.13</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
<artifactId>workflow-basic-steps</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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);

Check warning on line 64 in src/main/java/hudson/plugins/throttleconcurrents/ThrottleQueueTaskDispatcher.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 64 is not covered by tests
}
}
updatePauseAction(task, cause);
return cause;
}

private CauseOfBlockage canTakeImpl(Node node, Task task) {
Expand Down Expand Up @@ -218,14 +222,17 @@ private boolean shouldBeThrottled(@NonNull Task task, @CheckForNull ThrottleJobP
}

private CauseOfBlockage canRun(Task task, ThrottleJobProperty tjp, List<String> 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);

Check warning on line 231 in src/main/java/hudson/plugins/throttleconcurrents/ThrottleQueueTaskDispatcher.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 231 is not covered by tests
}
}
updatePauseAction(task, cause);
return cause;
}

private CauseOfBlockage canRunImpl(Task task, ThrottleJobProperty tjp, List<String> pipelineCategories) {
Expand Down Expand Up @@ -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) {

Check warning on line 710 in src/main/java/hudson/plugins/throttleconcurrents/ThrottleQueueTaskDispatcher.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 710 is only partially covered, one branch is missing
return;

Check warning on line 711 in src/main/java/hudson/plugins/throttleconcurrents/ThrottleQueueTaskDispatcher.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 711 is not covered by tests
}

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

Check warning on line 725 in src/main/java/hudson/plugins/throttleconcurrents/ThrottleQueueTaskDispatcher.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 723-725 are not covered by tests
});
}
}
}

private static final Logger LOGGER = Logger.getLogger(ThrottleQueueTaskDispatcher.class.getName());
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -96,6 +98,12 @@ static Set<String> 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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()) {
Expand Down Expand Up @@ -460,6 +470,7 @@ private String getThrottleScript(String jobName, List<String> categories, String
return "throttle(["
+ StringUtils.join(quoted, ", ")
+ "]) {\n"
+ "stage('wait') {\n"
+ " echo 'hi there'\n"
+ " node('"
+ label
Expand All @@ -468,6 +479,7 @@ private String getThrottleScript(String jobName, List<String> categories, String
+ jobName
+ "-job'\n"
+ " }\n"
+ "}\n"
+ "}\n";
}

Expand Down

0 comments on commit 44adeb5

Please sign in to comment.