Skip to content

Commit be8849e

Browse files
authored
Store DownstreamBuildAction on Run instead of FlowNode for consistent persistence even on completed builds (#128)
* Make DownstreamBuildAction immutable to fix persistence across restarts in some cases * Store DownstreamBuildAction on Run instead of FlowNode for consistent persistence even on completed builds * Refactor DownstreamBuildAction to hold multiple downstream builds internally * Change DownstreamBuildAction.downstreamBuilds from a Map to a List
1 parent 8c88916 commit be8849e

File tree

6 files changed

+168
-87
lines changed

6 files changed

+168
-87
lines changed

pom.xml

+1-7
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
<dependency>
4848
<groupId>io.jenkins.tools.bom</groupId>
4949
<artifactId>bom-2.387.x</artifactId>
50-
<version>2143.ve4c3c9ec790a</version>
50+
<version>2543.vfb_1a_5fb_9496d</version>
5151
<scope>import</scope>
5252
<type>pom</type>
5353
</dependency>
@@ -90,16 +90,12 @@
9090
<groupId>org.jenkins-ci.plugins.workflow</groupId>
9191
<artifactId>workflow-cps</artifactId>
9292
<scope>test</scope>
93-
<!-- TODO: Remove version declaration when workflow-cps plugin 3691.v28b_14c465a_b_b_ or newer is in plugin BOM -->
94-
<version>3691.v28b_14c465a_b_b_</version>
9593
</dependency>
9694
<dependency>
9795
<groupId>org.jenkins-ci.plugins.workflow</groupId>
9896
<artifactId>workflow-cps</artifactId>
9997
<classifier>tests</classifier>
10098
<scope>test</scope>
101-
<!-- TODO: Remove version declaration when workflow-cps plugin 3691.v28b_14c465a_b_b_ or newer is in plugin BOM -->
102-
<version>3691.v28b_14c465a_b_b_</version>
10399
</dependency>
104100
<dependency>
105101
<groupId>org.jenkins-ci.plugins.workflow</groupId>
@@ -151,8 +147,6 @@
151147
<groupId>org.jenkins-ci.plugins</groupId>
152148
<artifactId>git</artifactId>
153149
<scope>test</scope>
154-
<!-- TODO: Remove version declaration when git plugin 5.1.0 or newer is in plugin BOM -->
155-
<version>5.1.0</version>
156150
</dependency>
157151
<dependency>
158152
<groupId>org.awaitility</groupId>

src/main/java/org/jenkinsci/plugins/workflow/support/steps/build/BuildTriggerListener.java

+5-22
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
import java.util.logging.Logger;
1414
import jenkins.util.Timer;
1515
import org.jenkinsci.plugins.workflow.actions.WarningAction;
16-
import org.jenkinsci.plugins.workflow.flow.FlowExecution;
1716
import org.jenkinsci.plugins.workflow.flow.FlowExecutionOwner;
1817
import org.jenkinsci.plugins.workflow.graph.FlowNode;
1918
import org.jenkinsci.plugins.workflow.steps.FlowInterruptedException;
@@ -88,32 +87,16 @@ public void onDeleted(final Run<?,?> run) {
8887
}
8988
}
9089

91-
private void updateDownstreamBuildAction(Run<?, ?> run) {
92-
for (Cause cause : run.getCauses()) {
90+
private void updateDownstreamBuildAction(Run<?, ?> downstream) {
91+
for (Cause cause : downstream.getCauses()) {
9392
if (cause instanceof BuildUpstreamCause) {
9493
BuildUpstreamCause buildUpstreamCause = (BuildUpstreamCause) cause;
9594
Run<?, ?> upstream = buildUpstreamCause.getUpstreamRun();
9695
if (upstream instanceof FlowExecutionOwner.Executable) {
97-
FlowExecutionOwner owner = ((FlowExecutionOwner.Executable) upstream).asFlowExecutionOwner();
98-
if (owner == null) {
99-
LOGGER.log(Level.FINE, () -> "Unable to update DownstreamBuildAction for " + upstream + " node " + buildUpstreamCause.getNodeId());
100-
continue;
101-
}
96+
String flowNodeId = buildUpstreamCause.getNodeId();
97+
DownstreamBuildAction.getOrCreate(upstream, flowNodeId, downstream.getParent()).setBuild(downstream);
10298
try {
103-
FlowExecution execution = owner.get();
104-
FlowNode node = execution.getNode(buildUpstreamCause.getNodeId());
105-
if (node == null) {
106-
LOGGER.log(Level.FINE, () -> "Unable to update DownstreamBuildAction for " + upstream + " node " + buildUpstreamCause.getNodeId());
107-
continue;
108-
}
109-
DownstreamBuildAction downstreamAction = node.getPersistentAction(DownstreamBuildAction.class);
110-
if (downstreamAction == null) {
111-
// Should only happen for builds already in the queue when this plugin is updated to include DownstreamBuildAction.
112-
downstreamAction = new DownstreamBuildAction(run.getParent());
113-
node.addAction(downstreamAction);
114-
}
115-
downstreamAction.setBuild(run);
116-
run.save();
99+
upstream.save();
117100
} catch (IOException e) {
118101
LOGGER.log(Level.FINE, e, () -> "Unable to update DownstreamBuildAction for " + upstream + " node " + buildUpstreamCause.getNodeId());
119102
}

src/main/java/org/jenkinsci/plugins/workflow/support/steps/build/BuildTriggerStepExecution.java

+6-4
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ public BuildTriggerStepExecution(BuildTriggerStep step, @NonNull StepContext con
6666
@Override
6767
public boolean start() throws Exception {
6868
String job = step.getJob();
69-
Item item = Jenkins.get().getItem(job, getContext().get(Run.class).getParent(), Item.class);
69+
Run<?, ?> upstream = getContext().get(Run.class);
70+
Item item = Jenkins.get().getItem(job, upstream.getParent(), Item.class);
7071
if (item == null) {
7172
throw new AbortException("No item named " + job + " found");
7273
}
@@ -75,12 +76,13 @@ public boolean start() throws Exception {
7576
// TODO find some way of allowing ComputedFolders to hook into the listener code
7677
throw new AbortException("Waiting for non-job items is not supported");
7778
}
79+
7880
FlowNode node = getContext().get(FlowNode.class);
79-
node.addAction(new DownstreamBuildAction(item));
81+
DownstreamBuildAction.getOrCreate(upstream, node.getId(), item);
8082

8183
List<Action> actions = new ArrayList<>();
82-
actions.add(new CauseAction(new BuildUpstreamCause(getContext().get(FlowNode.class), getContext().get(Run.class))));
83-
actions.add(new BuildUpstreamNodeAction(node, getContext().get(Run.class)));
84+
actions.add(new CauseAction(new BuildUpstreamCause(getContext().get(FlowNode.class), upstream)));
85+
actions.add(new BuildUpstreamNodeAction(node, upstream));
8486

8587
if (item instanceof ParameterizedJobMixIn.ParameterizedJob) {
8688
final ParameterizedJobMixIn.ParameterizedJob project = (ParameterizedJobMixIn.ParameterizedJob) item;

src/main/java/org/jenkinsci/plugins/workflow/support/steps/build/DownstreamBuildAction.java

+68-28
Original file line numberDiff line numberDiff line change
@@ -6,47 +6,87 @@
66
import hudson.model.Item;
77
import hudson.model.ItemGroup;
88
import hudson.model.Run;
9-
import org.jenkinsci.plugins.workflow.actions.PersistentAction;
9+
import java.util.ArrayList;
10+
import java.util.Collections;
11+
import java.util.List;
12+
import org.jenkinsci.plugins.workflow.graph.FlowNode;
1013
import org.springframework.security.access.AccessDeniedException;
1114

1215
/**
13-
* Tracks the downstream build triggered by the {@code build} step.
16+
* Tracks downstream builds triggered by the {@code build} step, as well as the {@link FlowNode#getId} of the step.
1417
*
1518
* @see BuildUpstreamCause
1619
*/
17-
public final class DownstreamBuildAction extends InvisibleAction implements PersistentAction {
18-
private final String jobFullName;
19-
private Integer buildNumber;
20+
public final class DownstreamBuildAction extends InvisibleAction {
21+
private final List<DownstreamBuild> downstreamBuilds = new ArrayList<>();
2022

21-
DownstreamBuildAction(Item job) {
22-
this.jobFullName = job.getFullName();
23+
public static @NonNull DownstreamBuild getOrCreate(@NonNull Run<?, ?> run, @NonNull String flowNodeId, @NonNull Item job) {
24+
DownstreamBuildAction downstreamBuildAction;
25+
synchronized (DownstreamBuildAction.class) {
26+
downstreamBuildAction = run.getAction(DownstreamBuildAction.class);
27+
if (downstreamBuildAction == null) {
28+
downstreamBuildAction = new DownstreamBuildAction();
29+
run.addAction(downstreamBuildAction);
30+
}
31+
}
32+
return downstreamBuildAction.getOrAddDownstreamBuild(flowNodeId, job);
2333
}
2434

25-
public @NonNull String getJobFullName() {
26-
return jobFullName;
35+
public synchronized @NonNull List<DownstreamBuild> getDownstreamBuilds() {
36+
return Collections.unmodifiableList(new ArrayList<>(downstreamBuilds));
2737
}
2838

29-
/**
30-
* Get the build number of the downstream build, or {@code null} if the downstream build has not yet started or the queue item was cancelled.
31-
*/
32-
public @CheckForNull Integer getBuildNumber() {
33-
return buildNumber;
39+
private synchronized @NonNull DownstreamBuild getOrAddDownstreamBuild(@NonNull String flowNodeId, @NonNull Item job) {
40+
for (DownstreamBuild build : downstreamBuilds) {
41+
if (build.getFlowNodeId().equals(flowNodeId)) {
42+
return build;
43+
}
44+
}
45+
var build = new DownstreamBuild(flowNodeId, job);
46+
downstreamBuilds.add(build);
47+
return build;
3448
}
3549

36-
/**
37-
* Load the downstream build, if it has started and still exists.
38-
* <p>Loading builds indiscriminately will affect controller performance, so use this carefully. If you only need
39-
* to know whether the build started at one point, use {@link #getBuildNumber}.
40-
* @throws AccessDeniedException as per {@link ItemGroup#getItem}
41-
*/
42-
public @CheckForNull Run<?, ?> getBuild() throws AccessDeniedException {
43-
if (buildNumber == null) {
44-
return null;
45-
}
46-
return Run.fromExternalizableId(jobFullName + '#' + buildNumber);
47-
}
50+
public static final class DownstreamBuild {
51+
private final String flowNodeId;
52+
private final String jobFullName;
53+
private Integer buildNumber;
54+
55+
DownstreamBuild(String flowNodeId, @NonNull Item job) {
56+
this.flowNodeId = flowNodeId;
57+
this.jobFullName = job.getFullName();
58+
}
59+
60+
public @NonNull String getFlowNodeId() {
61+
return flowNodeId;
62+
}
4863

49-
void setBuild(Run<?, ?> build) {
50-
this.buildNumber = build.getNumber();
64+
public @NonNull String getJobFullName() {
65+
return jobFullName;
66+
}
67+
68+
/**
69+
* Get the build number of the downstream build, or {@code null} if the downstream build has not yet started or the queue item was cancelled.
70+
*/
71+
public @CheckForNull Integer getBuildNumber() {
72+
return buildNumber;
73+
}
74+
75+
/**
76+
* Load the downstream build, if it has started and still exists.
77+
* <p>Loading builds indiscriminately will affect controller performance, so use this carefully. If you only need
78+
* to know whether the build started at one point, use {@link #getBuildNumber}.
79+
* @throws AccessDeniedException as per {@link ItemGroup#getItem}
80+
*/
81+
public @CheckForNull Run<?, ?> getBuild() throws AccessDeniedException {
82+
if (buildNumber == null) {
83+
return null;
84+
}
85+
return Run.fromExternalizableId(jobFullName + '#' + buildNumber);
86+
}
87+
88+
void setBuild(@NonNull Run<?, ?> run) {
89+
this.buildNumber = run.getNumber();
90+
}
5191
}
5292
}

src/test/java/org/jenkinsci/plugins/workflow/support/steps/build/BuildTriggerStepRestartTest.java

+87-2
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,37 @@
22

33
import hudson.model.FreeStyleBuild;
44
import hudson.model.FreeStyleProject;
5+
import hudson.model.Label;
56
import hudson.model.Queue;
67
import hudson.model.Result;
8+
import hudson.model.Run;
79
import java.util.Arrays;
10+
import java.util.List;
11+
import java.util.Objects;
12+
import java.util.concurrent.TimeUnit;
813
import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition;
914
import org.jenkinsci.plugins.workflow.cps.CpsFlowExecution;
1015
import org.jenkinsci.plugins.workflow.job.WorkflowJob;
1116
import org.jenkinsci.plugins.workflow.job.WorkflowRun;
12-
import org.junit.Assert;
17+
import org.jenkinsci.plugins.workflow.support.steps.build.DownstreamBuildAction.DownstreamBuild;
1318
import org.junit.ClassRule;
1419
import org.junit.Rule;
1520
import org.junit.Test;
1621
import org.jvnet.hudson.test.BuildWatcher;
1722
import org.jvnet.hudson.test.JenkinsRule;
1823
import org.jvnet.hudson.test.JenkinsSessionRule;
24+
import static org.awaitility.Awaitility.await;
25+
import static org.hamcrest.MatcherAssert.assertThat;
26+
import static org.hamcrest.Matchers.equalTo;
27+
import static org.hamcrest.Matchers.notNullValue;
28+
import static org.hamcrest.Matchers.nullValue;
29+
import static org.junit.Assert.assertEquals;
30+
import static org.junit.Assert.assertNotNull;
1931

2032
/**
2133
* @author Vivek Pandey
2234
*/
23-
public class BuildTriggerStepRestartTest extends Assert {
35+
public class BuildTriggerStepRestartTest {
2436

2537
@ClassRule public static BuildWatcher buildWatcher = new BuildWatcher();
2638
@Rule public JenkinsSessionRule sessions = new JenkinsSessionRule();
@@ -64,6 +76,79 @@ public void restartBetweenJobs() throws Throwable {
6476
sessions.then(j -> j.jenkins.getItemByFullName("test1", FreeStyleProject.class).getBuildByNumber(1).delete());
6577
}
6678

79+
@Test
80+
public void downstreamBuildActionUpstreamCompletesBeforeDownstreamStarts() throws Throwable {
81+
sessions.then(j -> {
82+
FreeStyleProject downstream = j.createFreeStyleProject("downstream");
83+
downstream.setAssignedLabel(Label.parseExpression("agent"));
84+
WorkflowJob upstream = j.jenkins.createProject(WorkflowJob.class, "upstream");
85+
upstream.setDefinition(new CpsFlowDefinition("build job: 'downstream', wait: false", true));
86+
WorkflowRun upstreamRun = j.buildAndAssertSuccess(upstream);
87+
// Check action while the build is still in the queue.
88+
String buildStepId = BuildTriggerStepTest.findFirstNodeWithDescriptor(upstreamRun.getExecution(), BuildTriggerStep.DescriptorImpl.class).getId();
89+
var downstreamBuildAction = upstreamRun.getAction(DownstreamBuildAction.class);
90+
var downstreamBuild = downstreamBuildAction.getDownstreamBuilds().get(0);
91+
assertThat(downstreamBuild.getFlowNodeId(), equalTo(buildStepId));
92+
assertThat(downstreamBuild.getJobFullName(), equalTo(downstream.getFullName()));
93+
assertThat(downstreamBuild.getBuildNumber(), nullValue());
94+
assertThat(downstreamBuild.getBuild(), nullValue());
95+
// Check again once the build has started.
96+
j.createOnlineSlave(Label.parseExpression("agent"));
97+
await().atMost(10, TimeUnit.SECONDS).until(() -> downstreamBuild.getBuildNumber(), notNullValue());
98+
Run<?, ?> downstreamRun = downstream.getLastBuild();
99+
assertThat(downstreamBuild.getJobFullName(), equalTo(downstream.getFullName()));
100+
assertThat(downstreamBuild.getBuildNumber(), equalTo(downstreamRun.getNumber()));
101+
assertThat(downstreamBuild.getBuild(), equalTo(downstreamRun));
102+
j.waitForCompletion(downstreamRun);
103+
});
104+
sessions.then(j -> {
105+
FreeStyleProject downstream = j.jenkins.getItemByFullName("downstream", FreeStyleProject.class);
106+
WorkflowJob upstream = j.jenkins.getItemByFullName("upstream", WorkflowJob.class);
107+
WorkflowRun upstreamRun = upstream.getLastBuild();
108+
String buildStepId = BuildTriggerStepTest.findFirstNodeWithDescriptor(upstreamRun.getExecution(), BuildTriggerStep.DescriptorImpl.class).getId();
109+
var downstreamBuildAction = upstreamRun.getAction(DownstreamBuildAction.class);
110+
var downstreamBuild = downstreamBuildAction.getDownstreamBuilds().get(0);
111+
assertThat(downstreamBuild.getFlowNodeId(), equalTo(buildStepId));
112+
assertThat(downstreamBuild.getJobFullName(), equalTo(downstream.getFullName()));
113+
assertThat(downstreamBuild.getBuildNumber(), equalTo(downstream.getLastBuild().getNumber()));
114+
assertThat(downstreamBuild.getBuild(), equalTo(downstream.getLastBuild()));
115+
});
116+
}
117+
118+
@Test
119+
public void downstreamBuildActionMultipleBuilds() throws Throwable {
120+
sessions.then(j -> {
121+
FreeStyleProject downstream = j.createFreeStyleProject("downstream");
122+
WorkflowJob upstream = j.jenkins.createProject(WorkflowJob.class, "upstream");
123+
upstream.setDefinition(new CpsFlowDefinition("build 'downstream'; build 'downstream'", true));
124+
WorkflowRun upstreamRun = j.buildAndAssertSuccess(upstream);
125+
List<DownstreamBuild> downstreamBuilds = upstreamRun.getAction(DownstreamBuildAction.class).getDownstreamBuilds();
126+
await().atMost(10, TimeUnit.SECONDS).until(
127+
() -> downstreamBuilds.stream().map(DownstreamBuild::getBuildNumber).filter(Objects::nonNull).count(),
128+
equalTo(2L));
129+
for (Run<?, ?> downstreamRun : downstream.getBuilds()) {
130+
String nodeId = downstreamRun.getCause(BuildUpstreamCause.class).getNodeId();
131+
var downstreamBuild = downstreamBuilds.stream().filter(db -> db.getFlowNodeId().equals(nodeId)).findFirst().get();
132+
assertThat(downstreamBuild.getJobFullName(), equalTo(downstream.getFullName()));
133+
assertThat(downstreamBuild.getBuildNumber(), equalTo(downstreamRun.getNumber()));
134+
assertThat(downstreamBuild.getBuild(), equalTo(downstreamRun));
135+
}
136+
});
137+
sessions.then(j -> {
138+
FreeStyleProject downstream = j.jenkins.getItemByFullName("downstream", FreeStyleProject.class);
139+
WorkflowJob upstream = j.jenkins.getItemByFullName("upstream", WorkflowJob.class);
140+
WorkflowRun upstreamRun = upstream.getLastBuild();
141+
List<DownstreamBuild> downstreamBuilds = upstreamRun.getAction(DownstreamBuildAction.class).getDownstreamBuilds();
142+
for (Run<?, ?> downstreamRun : downstream.getBuilds()) {
143+
String nodeId = downstreamRun.getCause(BuildUpstreamCause.class).getNodeId();
144+
var downstreamBuild = downstreamBuilds.stream().filter(db -> db.getFlowNodeId().equals(nodeId)).findFirst().get();
145+
assertThat(downstreamBuild.getJobFullName(), equalTo(downstream.getFullName()));
146+
assertThat(downstreamBuild.getBuildNumber(), equalTo(downstreamRun.getNumber()));
147+
assertThat(downstreamBuild.getBuild(), equalTo(downstreamRun));
148+
}
149+
});
150+
}
151+
67152
private static void assertFreeStyleProjectsInQueue(int count, JenkinsRule j) {
68153
Queue.Item[] items = j.jenkins.getQueue().getItems();
69154
int actual = 0;

0 commit comments

Comments
 (0)