Skip to content

Commit c7ca8d9

Browse files
authored
Improve reliablity of finding 'after' nodes. (#364)
1 parent ecb8c58 commit c7ca8d9

File tree

4 files changed

+165
-97
lines changed

4 files changed

+165
-97
lines changed

src/main/java/io/jenkins/plugins/pipelinegraphview/treescanner/NodeRelationship.java

Lines changed: 0 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,10 @@
55
import io.jenkins.plugins.pipelinegraphview.utils.BlueRun;
66
import io.jenkins.plugins.pipelinegraphview.utils.NodeRunStatus;
77
import io.jenkins.plugins.pipelinegraphview.utils.PipelineNodeUtil;
8-
import org.jenkinsci.plugins.workflow.actions.ErrorAction;
9-
import org.jenkinsci.plugins.workflow.actions.WarningAction;
10-
import org.jenkinsci.plugins.workflow.flow.FlowExecution;
118
import org.jenkinsci.plugins.workflow.graph.FlowNode;
129
import org.jenkinsci.plugins.workflow.job.WorkflowRun;
13-
import org.jenkinsci.plugins.workflow.pipelinegraphanalysis.GenericStatus;
1410
import org.jenkinsci.plugins.workflow.pipelinegraphanalysis.StatusAndTiming;
1511
import org.jenkinsci.plugins.workflow.pipelinegraphanalysis.TimingInfo;
16-
import org.jenkinsci.plugins.workflow.steps.FlowInterruptedException;
1712
import org.jenkinsci.plugins.workflow.support.actions.PauseAction;
1813
import org.slf4j.Logger;
1914
import org.slf4j.LoggerFactory;
@@ -117,60 +112,11 @@ public FlowNode getEnd() {
117112
} else if (PipelineNodeUtil.isPaused(this.end)) {
118113
return new NodeRunStatus(BlueRun.BlueRunResult.UNKNOWN, BlueRun.BlueRunState.PAUSED);
119114
}
120-
// StatusAndTiming.computeChunkStatus2 assumes that a Stage is running if there
121-
// is no after node. There are instances where this can happen (might be a bug
122-
// in this code). To work around this we catch this case and explicitly generate
123-
// the status ourselves.
124-
if (this.after == null && !isRunning(run)) {
125-
return new NodeRunStatus(getFinishNodeStatus());
126-
}
127115
dump(
128116
"Calculating Chunk Status start: %s, end: %s after: %s",
129117
this.start.getId(), this.end.getId(), (this.after != null) ? this.after.getId() : "null");
130118
// Catch-all if none of the above are applicable.
131119
return new NodeRunStatus(
132120
StatusAndTiming.computeChunkStatus2(run, this.before, this.start, this.end, this.after));
133121
}
134-
135-
/*
136-
* Determine if the current block is still executing.
137-
* Note: This doesn't seem efficient, but I couldn't see another way.
138-
*/
139-
private boolean isRunning(WorkflowRun run) {
140-
FlowExecution exec = run.getExecution();
141-
if (exec != null) {
142-
for (FlowNode head : exec.getCurrentHeads()) {
143-
if (head.getAllEnclosingIds().contains(this.start.getId())) {
144-
return true;
145-
}
146-
}
147-
}
148-
return false;
149-
}
150-
151-
/*
152-
* Generate status for finished node.
153-
* Source:
154-
* https://github.com/jenkinsci/pipeline-graph-analysis-plugin/blob/master/src/
155-
* main/java/org/jenkinsci/plugins/workflow/pipelinegraphanalysis/
156-
* StatusAndTiming.java#L295
157-
*/
158-
private GenericStatus getFinishNodeStatus() {
159-
ErrorAction err = this.end.getError();
160-
if (err != null) {
161-
Throwable rootCause = err.getError();
162-
if (rootCause instanceof FlowInterruptedException) {
163-
return GenericStatus.fromResult(((FlowInterruptedException) rootCause).getResult());
164-
} else {
165-
return GenericStatus.FAILURE;
166-
}
167-
}
168-
WarningAction warning = StatusAndTiming.findWorstWarningBetween(start, end);
169-
if (warning != null) {
170-
return GenericStatus.fromResult(warning.getResult());
171-
}
172-
173-
// Previous chunk before end. If flow continued beyond this, it didn't fail.
174-
return GenericStatus.SUCCESS;
175-
}
176122
}

src/main/java/io/jenkins/plugins/pipelinegraphview/treescanner/NodeRelationshipFinder.java

Lines changed: 74 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@
99
import java.util.Collections;
1010
import java.util.LinkedHashMap;
1111
import java.util.List;
12+
import java.util.Map;
1213
import org.jenkinsci.plugins.workflow.cps.nodes.StepAtomNode;
1314
import org.jenkinsci.plugins.workflow.graph.BlockEndNode;
15+
import org.jenkinsci.plugins.workflow.graph.BlockStartNode;
1416
import org.jenkinsci.plugins.workflow.graph.FlowNode;
1517
import org.slf4j.Logger;
1618
import org.slf4j.LoggerFactory;
@@ -20,16 +22,24 @@ public class NodeRelationshipFinder {
2022
private boolean isDebugEnabled = logger.isDebugEnabled();
2123

2224
private LinkedHashMap<String, FlowNode> endNodes = new LinkedHashMap<>();
23-
private ArrayDeque<FlowNode> pendingEndNodes = new ArrayDeque<>();
24-
private ArrayDeque<FlowNode> pendingStartNodes = new ArrayDeque<>();
2525

26-
// Somewhere to temporarily store the parallel branches information whilst we
27-
// are handing a parallel block.
28-
// The StatusAndTiming API requires us to the structure of the parallel block to
29-
// get the status of a branch.
26+
/* Stack of stacks to store the last seen node for each nested block we have gone into.
27+
* Used to assign the after node for relationships.
28+
* This can be a different type, depending on situation:
29+
* - For the last Atom node in a block, will be the BlockEndNode (or null if the step is running).
30+
* - For nodes that have later siblings, this will be the previous sibling we found.
31+
* - This might be a AtomNode or a BlockStartNode (when a step is followed by a StepBlock).
32+
*/
33+
private ArrayDeque<ArrayDeque<FlowNode>> lastSeenNodes = new ArrayDeque<>();
34+
private Map<String, ArrayDeque<FlowNode>> seenChildNodes = new LinkedHashMap<>();
35+
36+
/* Somewhere to temporarily store the parallel branches information whilst we
37+
* are handing a parallel block.
38+
* The StatusAndTiming API requires us to the structure of the parallel block to
39+
* get the status of a branch.
40+
*/
3041
private ArrayDeque<NodeRelationship> pendingBranchRelationships = new ArrayDeque<>();
3142

32-
private NodeRelationship subsequentStepRelationship = null;
3343
private LinkedHashMap<String, NodeRelationship> relationships = new LinkedHashMap<>();
3444

3545
// Print debug message if 'isDebugEnabled' is true.
@@ -57,6 +67,9 @@ public LinkedHashMap<String, NodeRelationship> getNodeRelationships(
5767
dump("Sorted Ids: %s", String.join(", ", sortedIds));
5868
for (String id : sortedIds) {
5969
getRelationshipForNode(nodeMap.get(id));
70+
// Add this node to the parents's stack as the last of it's child nodes that
71+
// we have seen.
72+
addSeenNodes(nodeMap.get(id));
6073
}
6174
return relationships;
6275
}
@@ -73,8 +86,6 @@ private void getRelationshipForNode(@NonNull FlowNode node) {
7386
}
7487

7588
private void handleBlockStart(@NonNull FlowNode node) {
76-
// Reset step relationship - shouldn't carry over between blocks.
77-
subsequentStepRelationship = null;
7889
// Assign end node to start node.
7990
if (FlowNodeWrapper.isStart(node)) {
8091
addBlockRelationship(node);
@@ -83,32 +94,68 @@ private void handleBlockStart(@NonNull FlowNode node) {
8394
}
8495
}
8596

86-
private void addStepRelationship(@NonNull StepAtomNode step) {
87-
dump("Generating relationship for step %s", step.getId());
97+
private void addSeenNodes(FlowNode node) {
98+
if (!seenChildNodes.keySet().contains(node.getEnclosingId())) {
99+
seenChildNodes.put(node.getEnclosingId(), new ArrayDeque<FlowNode>());
100+
}
101+
dump("Adding %s to seenChildNodes %s", node.getId(), node.getEnclosingId());
102+
seenChildNodes.get(node.getEnclosingId()).push(node);
103+
}
104+
105+
@CheckForNull
106+
private FlowNode getAfterNode(FlowNode node) {
88107
FlowNode after = null;
89-
// If we are followed by a step, that will be our after node, otherwise use the
90-
// end node of the block.
91-
if (subsequentStepRelationship != null) {
92-
after = subsequentStepRelationship.getStart();
93-
dump("Getting after node (%s) from subsequentStepRelationship", after.getId());
108+
// The after node is the last child of the enclosing node, except for the last node in
109+
// a block, then it's the last node in the enclosing nodes list (likely, this blocks end node).
110+
FlowNode parentStartNode = getFirstEnclosingNode(node);
111+
ArrayDeque<FlowNode> laterSiblings = getProcessedChildren(parentStartNode);
112+
if (parentStartNode != null && laterSiblings.isEmpty()) {
113+
// If there are no later siblings, get the parents later sibling.
114+
ArrayDeque<FlowNode> parentsLaterSiblings = getProcessedChildren(getFirstEnclosingNode(parentStartNode));
115+
after = parentsLaterSiblings.isEmpty() ? null : parentsLaterSiblings.peek();
116+
dump(parentsLaterSiblings.toString());
94117
} else {
95-
after = pendingEndNodes.peek();
96-
dump(
97-
"Getting after node (%s) from endNodes stack (size: %s)",
98-
(after != null) ? after.getId() : "null", endNodes.size());
118+
dump(laterSiblings.toString());
119+
after = laterSiblings.peek();
99120
}
121+
return after;
122+
}
123+
124+
@CheckForNull
125+
private BlockStartNode getFirstEnclosingNode(FlowNode node) {
126+
return node.getEnclosingBlocks().isEmpty()
127+
? null
128+
: node.getEnclosingBlocks().get(0);
129+
}
130+
131+
private ArrayDeque<FlowNode> getProcessedChildren(@CheckForNull FlowNode node) {
132+
if (node != null && seenChildNodes.keySet().contains(node.getId())) {
133+
return seenChildNodes.get(node.getId());
134+
}
135+
return new ArrayDeque<FlowNode>();
136+
}
137+
138+
private void addStepRelationship(@NonNull StepAtomNode step) {
139+
dump("Generating relationship for step %s", step.getId());
140+
// FlowNode after = subsequentNode;
141+
FlowNode after = getAfterNode(step);
142+
dump(
143+
"Adding step for %s(%s),%s(%s)",
144+
step.getId(),
145+
step.getClass().getName(),
146+
after == null ? "null" : after.getId(),
147+
after == null ? "null" : after.getClass().getName());
100148
NodeRelationship nodeRelationship = new NodeRelationship(step, step, after);
101149
relationships.put(step.getId(), nodeRelationship);
102-
subsequentStepRelationship = nodeRelationship;
103150
}
104151

105152
private void handleBlockEnd(@NonNull BlockEndNode<?> endNode) {
106153
// Blindly push a new start pending reliable way to check for parallel node.
107154
FlowNode startNode = endNode.getStartNode();
108155
endNodes.put(startNode.getId(), endNode);
109-
pendingEndNodes.push(endNode);
110-
dump("Adding %s to pendingEndNodes", endNode.getId());
111-
pendingStartNodes.push(startNode);
156+
// Create new stack for this block, add the end node and push it to stack of stacks.
157+
ArrayDeque<FlowNode> nodeBlockStack = new ArrayDeque<>();
158+
lastSeenNodes.push(nodeBlockStack);
112159
}
113160

114161
private void addBlockRelationship(@NonNull FlowNode node) {
@@ -131,15 +178,11 @@ private void addBlockRelationship(@NonNull FlowNode node) {
131178
if (endNode != node) {
132179
relationships.put(endNode.getId(), blockRelationship);
133180
}
134-
// Remove end node from stack (if there is one).
135-
if (!pendingEndNodes.isEmpty()) {
136-
pendingEndNodes.pop();
137-
}
138181
}
139182
}
140183

141184
private void addParallelBranchRelationship(@NonNull FlowNode node, @NonNull FlowNode endNode) {
142-
FlowNode after = getBlockAfterNode();
185+
FlowNode after = getAfterNode(node);
143186
// Store a parallel branch relationship - these will be used to build up the
144187
// parent parallel block relationship.
145188
// Once generated, that relationship will be superseded this one.
@@ -156,7 +199,7 @@ private void addParallelBranchRelationship(@NonNull FlowNode node, @NonNull Flow
156199
}
157200

158201
private NodeRelationship addParallelRelationship(@NonNull FlowNode node, @NonNull FlowNode endNode) {
159-
FlowNode after = getBlockAfterNode();
202+
FlowNode after = getAfterNode(node);
160203
dump(
161204
"Generating relationship for parallel Block %s (with after %s)",
162205
node.getId(), (after != null) ? after.getId() : "null");
@@ -176,20 +219,8 @@ private NodeRelationship addParallelRelationship(@NonNull FlowNode node, @NonNul
176219
return parallelRelationship;
177220
}
178221

179-
@CheckForNull
180-
private FlowNode getBlockAfterNode() {
181-
FlowNode after = null;
182-
if (!pendingEndNodes.isEmpty()) {
183-
after = pendingEndNodes.pop();
184-
}
185-
dump(
186-
"Getting after node (%s) from pendingEndNodes stack (size: %s)",
187-
(after != null) ? after.getId() : "null", pendingEndNodes.size());
188-
return after;
189-
}
190-
191222
private NodeRelationship addStageRelationship(@NonNull FlowNode node, @NonNull FlowNode endNode) {
192-
FlowNode after = getBlockAfterNode();
223+
FlowNode after = getAfterNode(node);
193224
dump(
194225
"Generating relationship for Block %s{%s}->%s{%s} (with after %s{%s})",
195226
node.getId(),

src/test/java/io/jenkins/plugins/pipelinegraphview/utils/PipelineStepApiTest.java

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111

1212
import hudson.model.Result;
1313
import hudson.model.queue.QueueTaskFuture;
14+
import io.jenkins.plugins.pipelinegraphview.treescanner.NodeRelationship;
1415
import io.jenkins.plugins.pipelinegraphview.treescanner.NodeRelationshipFinder;
16+
import io.jenkins.plugins.pipelinegraphview.treescanner.ParallelBlockRelationship;
1517
import io.jenkins.plugins.pipelinegraphview.treescanner.PipelineNodeGraphAdapter;
1618
import io.jenkins.plugins.pipelinegraphview.treescanner.PipelineNodeTreeScanner;
1719
import java.util.Arrays;
@@ -22,6 +24,7 @@
2224
import java.util.logging.Level;
2325
import java.util.logging.Logger;
2426
import org.jenkinsci.plugins.workflow.graph.FlowNode;
27+
import org.jenkinsci.plugins.workflow.job.WorkflowJob;
2528
import org.jenkinsci.plugins.workflow.job.WorkflowRun;
2629
import org.junit.Before;
2730
import org.junit.Rule;
@@ -46,6 +49,8 @@ public void enabledDebugLogs() {
4649
l.record(PipelineNodeTreeScanner.class, Level.FINEST);
4750
l.record(PipelineNodeGraphAdapter.class, Level.FINEST);
4851
l.record(NodeRelationshipFinder.class, Level.FINEST);
52+
l.record(NodeRelationship.class, Level.FINEST);
53+
l.record(ParallelBlockRelationship.class, Level.FINEST);
4954
}
5055

5156
@Test
@@ -483,4 +488,75 @@ public void stepsGetValidTimings() throws Exception {
483488
TestUtils.assertTimesInRange(n, checks.get(n.getName()));
484489
}
485490
}
491+
492+
@Issue("GH#362")
493+
@Test
494+
public void gh362_pausedParallelBranches() throws Exception {
495+
WorkflowJob job = TestUtils.createJob(
496+
j, "gh233_multipleRunningParallelBranches", "gh233_multipleRunningParallelBranches.jenkinsfile");
497+
498+
QueueTaskFuture<WorkflowRun> futureRun = job.scheduleBuild2(0);
499+
WorkflowRun run = futureRun.waitForStart();
500+
j.waitForMessage("Starting sleep A...", run);
501+
j.waitForMessage("Starting sleep B...", run);
502+
// Sleep to allow Pipeline to reach sleep.
503+
Thread.sleep(500L);
504+
List<PipelineStep> steps = new PipelineStepApi(run).getAllSteps().getSteps();
505+
String stepsString = TestUtils.collectStepsAsString(steps, (PipelineStep s) -> TestUtils.nodeNameAndStatus(s));
506+
507+
LOGGER.log(Level.INFO, stepsString);
508+
// Wait for Pipeline to end (terminating it means end nodes might not be
509+
// created).
510+
j.waitForCompletion(run);
511+
List<PipelineStep> finishedSteps =
512+
new PipelineStepApi(run).getAllSteps().getSteps();
513+
String stepsStringFinished =
514+
TestUtils.collectStepsAsString(finishedSteps, (PipelineStep s) -> TestUtils.nodeNameAndStatus(s));
515+
LOGGER.log(Level.INFO, stepsStringFinished);
516+
517+
assertThat(
518+
stepsString,
519+
equalTo("Starting sleep A...{success},1{running},Starting sleep B...{success},1{running}"));
520+
assertThat(
521+
stepsStringFinished,
522+
equalTo("Starting sleep A...{success},1{success},Starting sleep B...{success},1{success}"));
523+
}
524+
525+
@Issue("GH#362")
526+
@Test
527+
public void gh362_stepsBeforeStepBlockGetValidStatus() throws Exception {
528+
WorkflowJob job = TestUtils.createJob(j, "stepBlockInSteps", "stepBlockInSteps.jenkinsfile");
529+
530+
QueueTaskFuture<WorkflowRun> futureRun = job.scheduleBuild2(0);
531+
WorkflowRun run = futureRun.waitForStart();
532+
j.waitForMessage("Hello World", run);
533+
// Sleep to allow Pipeline to reach sleep.
534+
Thread.sleep(500L);
535+
List<PipelineStep> steps = new PipelineStepApi(run).getAllSteps().getSteps();
536+
String stepsString = TestUtils.collectStepsAsString(steps, (PipelineStep s) -> TestUtils.nodeNameAndStatus(s));
537+
538+
LOGGER.log(Level.INFO, stepsString);
539+
// Wait for Pipeline to end (terminating it means end nodes might not be
540+
// created).
541+
j.waitForCompletion(run);
542+
543+
List<PipelineStep> finishedSteps =
544+
new PipelineStepApi(run).getAllSteps().getSteps();
545+
String stepsStringFinished =
546+
TestUtils.collectStepsAsString(finishedSteps, (PipelineStep s) -> TestUtils.nodeNameAndStatus(s));
547+
LOGGER.log(Level.INFO, stepsStringFinished);
548+
549+
assertThat(stepsString, equalTo("Hello World{success},1{running}"));
550+
assertThat(stepsStringFinished, equalTo("Hello World{success},1{success},Goodbye World{success}"));
551+
552+
Map<String, List<Long>> checks = new LinkedHashMap<>();
553+
// Give large ranges - we are testing that the values are feasible, not that they are precise.
554+
checks.put("Hello World", Arrays.asList(1000L, 0L, 0L, 5000L, 500L, 500L));
555+
checks.put("1", Arrays.asList(1000L, 0L, 1000L, 5000L, 1500L, 1500L));
556+
checks.put("Goodbye World", Arrays.asList(0L, 0L, 0L, 5000L, 500L, 500L));
557+
for (AbstractPipelineNode n : finishedSteps) {
558+
assertThat(checks, hasEntry(is(n.getName()), notNullValue()));
559+
TestUtils.assertTimesInRange(n, checks.get(n.getName()));
560+
}
561+
}
486562
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
pipeline {
2+
agent any
3+
4+
stages {
5+
stage('Hello') {
6+
steps {
7+
echo 'Hello World'
8+
retry(1) {
9+
sleep(1)
10+
}
11+
echo 'Goodbye World'
12+
}
13+
}
14+
}
15+
}

0 commit comments

Comments
 (0)