Skip to content

Commit b7e492a

Browse files
dwnusbaumjglick
andauthored
Notify GraphListener extensions prior to listeners added via FlowExecution.addListener (#1052)
* Run local listeners after listeners from extensions * Notify listeners in order according to GraphListener.ordinal * Link to upstream PR * Update baseline and BOM as needed for workflow-api update * Simplify changes by handling TimingAction for FlowStartNode directly in FlowHead * Remove SNAPSHOT dependency on workflow-job, which was only used for testing * Remove unused imports * Adjust new import ordering to minimize diff * Remove another unused import and improve import ordering * Simplify based on review feedback Co-authored-by: Jesse Glick <[email protected]> --------- Co-authored-by: Jesse Glick <[email protected]>
1 parent 7eaf89c commit b7e492a

File tree

4 files changed

+67
-8
lines changed

4 files changed

+67
-8
lines changed

plugin/src/main/java/org/jenkinsci/plugins/workflow/cps/CpsFlowExecution.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,9 @@ public class CpsFlowExecution extends FlowExecution implements BlockableResume {
323323
}
324324
}
325325

326+
/**
327+
* Holds listeners added via {@link #addListener} in the reverse order that they were added.
328+
*/
326329
private transient List<GraphListener> listeners;
327330

328331
/**
@@ -1229,7 +1232,7 @@ public void addListener(GraphListener listener) {
12291232
if (listeners == null) {
12301233
listeners = new CopyOnWriteArrayList<>();
12311234
}
1232-
listeners.add(listener);
1235+
listeners.add(0, listener);
12331236
}
12341237

12351238
@Override public void removeListener(GraphListener listener) {
@@ -1527,12 +1530,13 @@ private static void cleanUpClassHelperCache(@NonNull Class<?> clazz) throws Exce
15271530
}
15281531

15291532
List<GraphListener> getListenersToRun() {
1530-
List<GraphListener> l = new ArrayList<>();
1531-
1533+
// Listeners from extensions always come first, ordered by `Extension.ordinal`. Listeners added via
1534+
// `addListener` are then notified in the reverse order that they were added (see `addListener`) so that the
1535+
// `build-finalizing WorkflowRun$GraphL` always runs last.
1536+
List<GraphListener> l = new ArrayList<>(ExtensionList.lookup(GraphListener.class));
15321537
if (listeners != null) {
15331538
l.addAll(listeners);
15341539
}
1535-
l.addAll(ExtensionList.lookup(GraphListener.class));
15361540

15371541
return l;
15381542
}

plugin/src/main/java/org/jenkinsci/plugins/workflow/cps/FlowHead.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ void newStartNode(FlowStartNode n) throws IOException {
111111
}
112112
execution.flowStartNodeActions.clear();
113113
} // may be unset from loadProgramFailed
114+
n.addAction(new TimingAction());
114115
synchronized (execution) {
115116
this.head = execution.startNodes.push(n);
116117
}

plugin/src/test/java/org/jenkinsci/plugins/workflow/GraphListenerTest.java

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,26 @@
11
package org.jenkinsci.plugins.workflow;
22

3+
import static org.awaitility.Awaitility.await;
4+
import static org.hamcrest.Matchers.is;
5+
6+
import hudson.ExtensionList;
37
import hudson.model.Run;
48
import org.jenkinsci.plugins.workflow.cps.CpsFlowDefinition;
59
import org.jenkinsci.plugins.workflow.cps.CpsFlowExecution;
610
import org.jenkinsci.plugins.workflow.flow.GraphListener;
11+
import org.jenkinsci.plugins.workflow.graph.FlowEndNode;
712
import org.jenkinsci.plugins.workflow.graph.FlowNode;
813
import org.jenkinsci.plugins.workflow.job.WorkflowJob;
14+
import org.jenkinsci.plugins.workflow.job.WorkflowRun;
915
import org.junit.Assert;
10-
import org.junit.ClassRule;
1116
import org.junit.Rule;
1217
import org.junit.Test;
18+
import org.junit.rules.ErrorCollector;
1319
import org.jvnet.hudson.test.JenkinsRule;
1420
import org.jvnet.hudson.test.LoggerRule;
1521
import org.jvnet.hudson.test.TestExtension;
1622

23+
import java.io.IOException;
1724
import java.io.Serializable;
1825
import java.util.List;
1926
import java.util.Random;
@@ -22,12 +29,15 @@
2229

2330
public class GraphListenerTest
2431
{
25-
@ClassRule
26-
public static JenkinsRule r = new JenkinsRule();
32+
@Rule
33+
public JenkinsRule r = new JenkinsRule();
2734

2835
@Rule
2936
public LoggerRule logging = new LoggerRule();
3037

38+
@Rule
39+
public ErrorCollector errors = new ErrorCollector();
40+
3141
private static final String LOG_MESSAGE = "some problem here";
3242

3343
@Issue("JENKINS-54890")
@@ -47,7 +57,7 @@ public void listener()
4757
Assert.assertTrue( "cannot find listener exception message", found > 0 );
4858
}
4959

50-
@TestExtension
60+
@TestExtension("listener")
5161
public static class TestGraphListener
5262
implements GraphListener, Serializable
5363
{
@@ -61,4 +71,33 @@ public void onNewHead( FlowNode flowNode )
6171
throw new NullPointerException( LOG_MESSAGE );
6272
}
6373
}
74+
75+
@Test
76+
public void listenersRunBeforeBuildCompletion() throws Exception {
77+
var listener = ExtensionList.lookupSingleton(CheckBuildCompletionListener.class);
78+
listener.errors = errors;
79+
var p = r.createProject(WorkflowJob.class);
80+
p.setDefinition(new CpsFlowDefinition("echo 'test'", true));
81+
var b = r.buildAndAssertSuccess(p);
82+
await().until(() -> listener.done);
83+
}
84+
85+
@TestExtension("listenersRunBeforeBuildCompletion")
86+
public static class CheckBuildCompletionListener implements GraphListener {
87+
private ErrorCollector errors;
88+
private boolean done;
89+
90+
@Override
91+
public void onNewHead(FlowNode node) {
92+
if (node instanceof FlowEndNode) {
93+
try {
94+
var b = (WorkflowRun) node.getExecution().getOwner().getExecutable();
95+
errors.checkThat("Listeners should always run before build completion", b.isLogUpdated(), is(true));
96+
} catch (IOException e) {
97+
errors.addError(e);
98+
}
99+
done = true;
100+
}
101+
}
102+
}
64103
}

plugin/src/test/java/org/jenkinsci/plugins/workflow/cps/CpsFlowExecutionTest.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191
import org.htmlunit.WebRequest;
9292
import org.jenkinsci.plugins.scriptsecurity.sandbox.RejectedAccessException;
9393
import org.jenkinsci.plugins.scriptsecurity.sandbox.whitelists.Whitelisted;
94+
import org.jenkinsci.plugins.workflow.actions.TimingAction;
9495
import org.jenkinsci.plugins.workflow.cps.CpsFlowExecution.TimingFlowNodeStorage;
9596
import org.jenkinsci.plugins.workflow.cps.GroovySourceFileAllowlist.DefaultAllowlist;
9697
import org.jenkinsci.plugins.workflow.flow.FlowExecution;
@@ -1007,4 +1008,18 @@ public boolean takesImplicitBlockArgument() {
10071008
}
10081009
}
10091010

1011+
@Test public void timingActionAlwaysAdded() throws Throwable {
1012+
sessions.then(r -> {
1013+
WorkflowJob p = r.createProject(WorkflowJob.class, "p");
1014+
p.setDefinition(new CpsFlowDefinition("parallel(one: { stage('1') { echo '1' } }, two: { echo '2' })", true));
1015+
WorkflowRun b = r.buildAndAssertSuccess(p);
1016+
var nodesWithoutTiming = new DepthFirstScanner()
1017+
.allNodes(b.getExecution())
1018+
.stream()
1019+
.filter(n -> n.getPersistentAction(TimingAction.class) == null)
1020+
.toList();
1021+
assertThat(nodesWithoutTiming, empty());
1022+
});
1023+
}
1024+
10101025
}

0 commit comments

Comments
 (0)