Skip to content

Commit 9ca388a

Browse files
authored
Merge pull request #124 from stuartrowe/JENKINS-71961
[JENKINS-71961] Fix waitForBuild Step does not abort downstream build when upstream job aborted
2 parents faba22c + b08e7fc commit 9ca388a

File tree

4 files changed

+236
-20
lines changed

4 files changed

+236
-20
lines changed

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
package org.jenkinsci.plugins.workflow.support.steps.build;
22

3-
import edu.umd.cs.findbugs.annotations.NonNull;
3+
import hudson.AbortException;
44
import hudson.Extension;
55
import hudson.console.ModelHyperlinkNote;
66
import hudson.model.Result;
77
import hudson.model.Run;
88
import hudson.model.TaskListener;
99
import hudson.model.listeners.RunListener;
10+
import jenkins.util.Timer;
11+
1012
import java.util.logging.Level;
1113
import java.util.logging.Logger;
1214
import org.jenkinsci.plugins.workflow.actions.WarningAction;
@@ -20,7 +22,7 @@ public class WaitForBuildListener extends RunListener<Run<?,?>> {
2022
private static final Logger LOGGER = Logger.getLogger(WaitForBuildListener.class.getName());
2123

2224
@Override
23-
public void onCompleted(Run<?,?> run, @NonNull TaskListener listener) {
25+
public void onFinalized(Run<?,?> run) {
2426
for (WaitForBuildAction action : run.getActions(WaitForBuildAction.class)) {
2527
StepContext context = action.context;
2628
LOGGER.log(Level.FINE, "completing {0} for {1}", new Object[] {run, context});
@@ -46,4 +48,11 @@ public void onCompleted(Run<?,?> run, @NonNull TaskListener listener) {
4648
}
4749
run.removeActions(WaitForBuildAction.class);
4850
}
51+
52+
@Override
53+
public void onDeleted(final Run<?,?> run) {
54+
for (WaitForBuildAction action : run.getActions(WaitForBuildAction.class)) {
55+
Timer.get().submit(() -> action.context.onFailure(new AbortException(run.getFullDisplayName() + " was deleted")));
56+
}
57+
}
4958
}

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ public class WaitForBuildStep extends Step {
2424

2525
private final String runId;
2626
private boolean propagate = false;
27+
private boolean propagateAbort = false;
2728

2829
@DataBoundConstructor
2930
public WaitForBuildStep(String runId) {
@@ -42,6 +43,14 @@ public boolean isPropagate() {
4243
this.propagate = propagate;
4344
}
4445

46+
public boolean isPropagateAbort() {
47+
return propagateAbort;
48+
}
49+
50+
@DataBoundSetter public void setPropagateAbort(boolean propagateAbort) {
51+
this.propagateAbort = propagateAbort;
52+
}
53+
4554
@Override
4655
public StepExecution start(StepContext context) throws Exception {
4756
return new WaitForBuildStepExecution(this, context);

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

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,27 @@
33
import edu.umd.cs.findbugs.annotations.NonNull;
44
import hudson.AbortException;
55
import hudson.console.ModelHyperlinkNote;
6+
import hudson.model.Computer;
7+
import hudson.model.Executor;
8+
import hudson.model.Queue;
69
import hudson.model.Result;
710
import hudson.model.Run;
811
import hudson.model.TaskListener;
12+
import jenkins.model.Jenkins;
913
import org.jenkinsci.plugins.workflow.steps.AbstractStepExecutionImpl;
1014
import org.jenkinsci.plugins.workflow.steps.FlowInterruptedException;
1115
import org.jenkinsci.plugins.workflow.steps.StepContext;
1216

17+
import java.io.IOException;
18+
import java.util.logging.Level;
19+
import java.util.logging.Logger;
20+
1321
public class WaitForBuildStepExecution extends AbstractStepExecutionImpl {
1422

1523
private static final long serialVersionUID = 1L;
1624

25+
private static final Logger LOGGER = Logger.getLogger(WaitForBuildStepExecution.class.getName());
26+
1727
private final transient WaitForBuildStep step;
1828

1929
public WaitForBuildStepExecution(WaitForBuildStep step, @NonNull StepContext context) {
@@ -55,4 +65,51 @@ public boolean start() throws Exception {
5565
}
5666
}
5767

68+
@Override
69+
public void stop(@NonNull Throwable cause) throws Exception {
70+
StepContext context = getContext();
71+
Jenkins jenkins = Jenkins.getInstanceOrNull();
72+
if (jenkins == null) {
73+
context.onFailure(cause);
74+
return;
75+
}
76+
77+
boolean interrupted = false;
78+
79+
if (step.isPropagateAbort()) {
80+
// if there's any in-progress build already, abort that.
81+
// when the build is actually aborted, WaitForBuildListener will take notice and report the failure,
82+
// so this method shouldn't call getContext().onFailure()
83+
for (Computer c : jenkins.getComputers()) {
84+
for (Executor e : c.getAllExecutors()) {
85+
interrupted |= maybeInterrupt(e, cause, context);
86+
}
87+
}
88+
}
89+
90+
if(!interrupted) {
91+
super.stop(cause);
92+
}
93+
}
94+
95+
private static boolean maybeInterrupt(Executor e, Throwable cause, StepContext context) throws IOException, InterruptedException {
96+
boolean interrupted = false;
97+
Queue.Executable exec = e.getCurrentExecutable();
98+
if (exec instanceof Run) {
99+
Run<?, ?> downstream = (Run<?, ?>) exec;
100+
for(WaitForBuildAction waitForBuildAction : downstream.getActions(WaitForBuildAction.class)) {
101+
if (waitForBuildAction.context.equals(context)) {
102+
e.interrupt(Result.ABORTED, new BuildTriggerCancelledCause(cause));
103+
try {
104+
downstream.save();
105+
} catch (IOException x) {
106+
LOGGER.log(Level.WARNING, "failed to save interrupt cause on " + exec, x);
107+
}
108+
interrupted = true;
109+
}
110+
}
111+
}
112+
return interrupted;
113+
}
114+
58115
}

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

Lines changed: 159 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import hudson.model.FreeStyleProject;
44
import hudson.model.Result;
55
import hudson.model.Run;
6+
67
import org.jenkinsci.plugins.workflow.actions.WarningAction;
78
import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition;
89
import org.jenkinsci.plugins.workflow.cps.nodes.StepAtomNode;
@@ -11,6 +12,7 @@
1112
import org.jenkinsci.plugins.workflow.graph.FlowNode;
1213
import org.jenkinsci.plugins.workflow.job.WorkflowJob;
1314
import org.jenkinsci.plugins.workflow.job.WorkflowRun;
15+
import org.jenkinsci.plugins.workflow.test.steps.SemaphoreStep;
1416
import org.junit.ClassRule;
1517
import org.junit.Rule;
1618
import org.junit.Test;
@@ -31,25 +33,60 @@ public class WaitForBuildStepTest {
3133

3234
@Test public void waitForBuild() throws Exception {
3335
Result dsResult = Result.FAILURE;
34-
createWaitingDownStreamJob(dsResult);
36+
WorkflowJob ds = createWaitingDownStreamJob("wait", dsResult);
3537
WorkflowJob us = j.jenkins.createProject(WorkflowJob.class, "us");
3638
us.setDefinition(new CpsFlowDefinition(
3739
"def ds = build job: 'ds', waitForStart: true\n" +
40+
"semaphore 'scheduled'\n" +
3841
"def dsRunId = \"${ds.getFullProjectName()}#${ds.getNumber()}\"\n" +
3942
"def completeDs = waitForBuild runId: dsRunId\n" +
4043
"echo \"'ds' completed with status ${completeDs.getResult()}\"", true));
41-
j.assertLogContains("'ds' completed with status " + dsResult.toString(), j.buildAndAssertSuccess(us));
44+
45+
// schedule upstream
46+
WorkflowRun usRun = us.scheduleBuild2(0).waitForStart();
47+
48+
// wait for ds to be scheduled
49+
SemaphoreStep.waitForStart("scheduled/1", usRun);
50+
SemaphoreStep.success("scheduled/1", true);
51+
52+
// signal the downstream run to complete after it has been waited on
53+
WorkflowRun dsRun = ds.getBuildByNumber(1);
54+
SemaphoreStep.waitForStart("wait/1", dsRun);
55+
waitForWaitForBuildAction(dsRun);
56+
SemaphoreStep.success("wait/1", true);
57+
58+
// assert upstream build status
59+
WorkflowRun completedUsRun = j.waitForCompletion(usRun);
60+
j.assertBuildStatusSuccess(completedUsRun);
61+
j.assertLogContains("'ds' completed with status " + dsResult.toString(), completedUsRun);
4262
}
4363

4464
@Test public void waitForBuildPropagte() throws Exception {
4565
Result dsResult = Result.FAILURE;
46-
createWaitingDownStreamJob(dsResult);
66+
WorkflowJob ds = createWaitingDownStreamJob("wait", dsResult);
4767
WorkflowJob us = j.jenkins.createProject(WorkflowJob.class, "us");
4868
us.setDefinition(new CpsFlowDefinition(
4969
"def ds = build job: 'ds', waitForStart: true\n" +
70+
"semaphore 'scheduled'\n" +
5071
"def dsRunId = \"${ds.getFullProjectName()}#${ds.getNumber()}\"\n" +
5172
"waitForBuild runId: dsRunId, propagate: true", true));
52-
j.assertLogContains("completed with status " + dsResult.toString(), j.buildAndAssertStatus(dsResult, us));
73+
74+
// schedule upstream
75+
WorkflowRun usRun = us.scheduleBuild2(0).waitForStart();
76+
77+
// wait for ds to be scheduled
78+
SemaphoreStep.waitForStart("scheduled/1", usRun);
79+
SemaphoreStep.success("scheduled/1", true);
80+
81+
// signal the downstream run to complete after it has been waited on
82+
WorkflowRun dsRun = ds.getBuildByNumber(1);
83+
waitForWaitForBuildAction(dsRun);
84+
SemaphoreStep.success("wait/1", true);
85+
86+
// assert upstream build status
87+
WorkflowRun completedUsRun = j.waitForCompletion(usRun);
88+
j.assertBuildStatus(dsResult, completedUsRun);
89+
j.assertLogContains("completed with status " + dsResult.toString(), completedUsRun);
5390
}
5491

5592
@SuppressWarnings("rawtypes")
@@ -82,24 +119,127 @@ public class WaitForBuildStepTest {
82119
@Issue("JENKINS-70983")
83120
@Test public void waitForUnstableBuildWithWarningAction() throws Exception {
84121
Result dsResult = Result.UNSTABLE;
85-
createWaitingDownStreamJob(dsResult);
122+
WorkflowJob ds = createWaitingDownStreamJob("wait", dsResult);
86123
WorkflowJob us = j.jenkins.createProject(WorkflowJob.class, "us");
87124
us.setDefinition(new CpsFlowDefinition(
88125
"def ds = build job: 'ds', waitForStart: true\n" +
126+
"semaphore 'scheduled'\n" +
89127
"def dsRunId = \"${ds.getFullProjectName()}#${ds.getNumber()}\"\n" +
90128
"try {\n" +
91129
" waitForBuild runId: dsRunId, propagate: true\n" +
92130
"} finally {\n" +
93131
" echo \"'ds' completed with status ${ds.getResult()}\"\n" +
94132
"}", true));
95-
j.assertLogContains("'ds' completed with status " + dsResult.toString(), j.buildAndAssertStatus(dsResult, us));
96-
WorkflowRun lastUpstreamRun = us.getLastBuild();
97-
FlowNode buildTriggerNode = findFirstNodeWithDescriptor(lastUpstreamRun.getExecution(), WaitForBuildStep.DescriptorImpl.class);
133+
134+
// schedule upstream
135+
WorkflowRun usRun = us.scheduleBuild2(0).waitForStart();
136+
137+
// wait for ds to be scheduled
138+
SemaphoreStep.waitForStart("scheduled/1", usRun);
139+
SemaphoreStep.success("scheduled/1", true);
140+
141+
// signal the downstream run to complete after it has been waited on
142+
WorkflowRun dsRun = ds.getBuildByNumber(1);
143+
waitForWaitForBuildAction(dsRun);
144+
SemaphoreStep.success("wait/1", true);
145+
146+
// assert upstream build status
147+
WorkflowRun completedUsRun = j.waitForCompletion(usRun);
148+
j.assertBuildStatus(dsResult, completedUsRun);
149+
j.assertLogContains("'ds' completed with status " + dsResult.toString(), completedUsRun);
150+
151+
FlowNode buildTriggerNode = findFirstNodeWithDescriptor(completedUsRun.getExecution(), WaitForBuildStep.DescriptorImpl.class);
98152
WarningAction action = buildTriggerNode.getAction(WarningAction.class);
99153
assertNotNull(action);
100154
assertEquals(action.getResult(), Result.UNSTABLE);
101155
}
102156

157+
@Issue("JENKINS-71961")
158+
@Test public void abortBuild() throws Exception {
159+
WorkflowJob ds = createWaitingDownStreamJob("wait", Result.SUCCESS);
160+
WorkflowJob us = j.jenkins.createProject(WorkflowJob.class, "us");
161+
us.setDefinition(new CpsFlowDefinition(
162+
"def ds = build job: 'ds', waitForStart: true\n" +
163+
"semaphore 'scheduled'\n" +
164+
"def dsRunId = \"${ds.getFullProjectName()}#${ds.getNumber()}\"\n" +
165+
"def completeDs = waitForBuild runId: dsRunId, propagate: true\n" +
166+
"echo \"'ds' completed with status ${completeDs.getResult()}\"", true));
167+
168+
// schedule upstream
169+
WorkflowRun usRun = us.scheduleBuild2(0).waitForStart();
170+
171+
// wait for ds to be scheduled
172+
SemaphoreStep.waitForStart("scheduled/1", usRun);
173+
SemaphoreStep.success("scheduled/1", true);
174+
175+
WorkflowRun dsRun = ds.getBuildByNumber(1);
176+
waitForWaitForBuildAction(dsRun);
177+
178+
// Abort the downstream build
179+
dsRun.getExecutor().interrupt();
180+
181+
j.assertBuildStatus(Result.ABORTED, j.waitForCompletion(dsRun));
182+
j.assertBuildStatus(Result.ABORTED, j.waitForCompletion(usRun));
183+
}
184+
185+
@Issue("JENKINS-71961")
186+
@Test public void interruptFlowPropagateAbort() throws Exception {
187+
WorkflowJob ds = createWaitingDownStreamJob("wait", Result.SUCCESS);
188+
WorkflowJob us = j.jenkins.createProject(WorkflowJob.class, "us");
189+
us.setDefinition(new CpsFlowDefinition(
190+
"def ds = build job: 'ds', waitForStart: true\n" +
191+
"semaphore 'scheduled'\n" +
192+
"def dsRunId = \"${ds.getFullProjectName()}#${ds.getNumber()}\"\n" +
193+
"def completeDs = waitForBuild runId: dsRunId, propagate: true, propagateAbort: true\n" +
194+
"echo \"'ds' completed with status ${completeDs.getResult()}\"", true));
195+
196+
// schedule upstream
197+
WorkflowRun usRun = us.scheduleBuild2(0).waitForStart();
198+
199+
// wait for ds to be scheduled
200+
SemaphoreStep.waitForStart("scheduled/1", usRun);
201+
SemaphoreStep.success("scheduled/1", true);
202+
203+
WorkflowRun dsRun = ds.getBuildByNumber(1);
204+
waitForWaitForBuildAction(dsRun);
205+
206+
// Abort the upstream build
207+
usRun.doStop();
208+
209+
j.assertBuildStatus(Result.ABORTED, j.waitForCompletion(dsRun));
210+
j.assertBuildStatus(Result.ABORTED, j.waitForCompletion(usRun));
211+
}
212+
213+
@Issue("JENKINS-71961")
214+
@Test public void interruptFlowNoPropagateAbort() throws Exception {
215+
WorkflowJob ds = createWaitingDownStreamJob("wait", Result.SUCCESS);
216+
WorkflowJob us = j.jenkins.createProject(WorkflowJob.class, "us");
217+
us.setDefinition(new CpsFlowDefinition(
218+
"def ds = build job: 'ds', waitForStart: true\n" +
219+
"semaphore 'scheduled'\n" +
220+
"def dsRunId = \"${ds.getFullProjectName()}#${ds.getNumber()}\"\n" +
221+
"def completeDs = waitForBuild runId: dsRunId, propagate: true, propagateAbort: false\n" +
222+
"echo \"'ds' completed with status ${completeDs.getResult()}\"", true));
223+
224+
// schedule upstream
225+
WorkflowRun usRun = us.scheduleBuild2(0).waitForStart();
226+
227+
// wait for ds to be scheduled
228+
SemaphoreStep.waitForStart("scheduled/1", usRun);
229+
SemaphoreStep.success("scheduled/1", true);
230+
231+
WorkflowRun dsRun = ds.getBuildByNumber(1);
232+
waitForWaitForBuildAction(dsRun);
233+
234+
// Abort the upstream build
235+
usRun.doStop();
236+
j.assertBuildStatus(Result.ABORTED, j.waitForCompletion(usRun));
237+
238+
// Allow the downstream to complete
239+
SemaphoreStep.success("wait/1", true);
240+
j.assertBuildStatus(Result.SUCCESS, j.waitForCompletion(dsRun));
241+
}
242+
103243
private static FlowNode findFirstNodeWithDescriptor(FlowExecution execution, Class<WaitForBuildStep.DescriptorImpl> cls) {
104244
for (FlowNode node : new FlowGraphWalker(execution)) {
105245
if (node instanceof StepAtomNode) {
@@ -112,22 +252,23 @@ private static FlowNode findFirstNodeWithDescriptor(FlowExecution execution, Cla
112252
return null;
113253
}
114254

115-
private WorkflowJob createWaitingDownStreamJob(Result result) throws Exception {
255+
private WorkflowJob createWaitingDownStreamJob(String semaphoreName, Result result) throws Exception {
116256
WorkflowJob ds = j.jenkins.createProject(WorkflowJob.class, "ds");
117257
ds.setDefinition(new CpsFlowDefinition(
118-
"import org.jenkinsci.plugins.workflow.support.steps.build.WaitForBuildAction\n" +
119-
"@NonCPS\n" +
120-
"boolean hasWaitForBuildAction() {\n" +
121-
" return currentBuild.getRawBuild().getAction(WaitForBuildAction.class) != null\n" +
122-
"}\n" +
123-
"while(!hasWaitForBuildAction()) {\n" +
124-
" sleep(time: 100, unit: 'MILLISECONDS')\n" +
125-
"}\n" +
258+
"semaphore('" + semaphoreName + "')\n" +
126259
"catchError(buildResult: '" + result.toString() + "') {\n" +
127260
" error('')\n" +
128261
"}", false));
129262
return ds;
130-
263+
}
264+
265+
private void waitForWaitForBuildAction(WorkflowRun r) throws Exception {
266+
while(true) {
267+
if (r.getAction(WaitForBuildAction.class) != null) {
268+
break;
269+
}
270+
Thread.sleep(10);
271+
}
131272
}
132273

133274
}

0 commit comments

Comments
 (0)