Skip to content

Commit 390f33a

Browse files
authored
Only process 1 graph request at a time for a given run (#887)
The initial computationally expensive part of creating the graph is creating the PipelineNodeTreeScanner and as such I have created a way of ensuring that all incoming requests use the same parent PipelineNodeGraphAdapter. This required some additional synchronisation code to ensure that concurrent modifications aren't made to the underlying data. --------- Signed-off-by: Lewis Birks <[email protected]>
1 parent 55503fe commit 390f33a

File tree

5 files changed

+148
-112
lines changed

5 files changed

+148
-112
lines changed

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

Lines changed: 97 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -24,72 +24,81 @@
2424
public class PipelineNodeGraphAdapter implements PipelineGraphBuilderApi, PipelineStepBuilderApi {
2525

2626
private static final Logger logger = LoggerFactory.getLogger(PipelineNodeGraphAdapter.class);
27-
private boolean isDebugEnabled = logger.isDebugEnabled();
28-
private PipelineNodeTreeScanner treeScanner;
29-
private List<FlowNodeWrapper> pipelineNodesList;
30-
private Map<String, List<FlowNodeWrapper>> stepsMap;
31-
private Map<String, String> nodesToRemap;
27+
private final boolean isDebugEnabled = logger.isDebugEnabled();
28+
private final PipelineNodeTreeScanner treeScanner;
29+
private volatile List<FlowNodeWrapper> pipelineNodesList;
30+
private volatile Map<String, List<FlowNodeWrapper>> stepsMap;
31+
private volatile Map<String, String> nodesToRemap;
3232

3333
public PipelineNodeGraphAdapter(WorkflowRun run) {
3434
treeScanner = new PipelineNodeTreeScanner(run);
3535
}
3636

37+
private final Object pipelineLock = new Object();
38+
private final Object stepLock = new Object();
39+
private final Object remapLock = new Object();
40+
3741
private Map<String, String> getNodesToRemap(List<FlowNodeWrapper> pipelineNodesList) {
3842
if (this.nodesToRemap != null) {
3943
return this.nodesToRemap;
4044
}
41-
// Get a map of nodes to remap. The first id is the node to map from, the second
42-
// is the node to
43-
// map to.
44-
// Most of the logic here is to recreate old behavior - it might not be to
45-
// everyone's liking.
46-
this.nodesToRemap = new HashMap<String, String>();
47-
for (int i = pipelineNodesList.size() - 1; i >= 0; i--) {
48-
FlowNodeWrapper node = pipelineNodesList.get(i);
49-
for (FlowNodeWrapper parent : node.getParents()) {
50-
// Parallel Start Nodes that have a Stage with the same name as a parent will be
51-
// mapped to that
52-
// parent stage
53-
// id.
54-
if (node.getType() == FlowNodeWrapper.NodeType.PARALLEL_BLOCK
55-
&& parent.getType() == FlowNodeWrapper.NodeType.STAGE) {
56-
if (isDebugEnabled) {
57-
logger.debug(
58-
"getNodesToRemap => Found Parallel block {id: {}, name: {}, type: {}} that has a Stage {id: {}, name: {}, type: {}} as a parent. Adding to remap list.",
59-
node.getId(),
60-
node.getDisplayName(),
61-
node.getType(),
62-
parent.getId(),
63-
parent.getDisplayName(),
64-
parent.getType());
45+
synchronized (remapLock) {
46+
if (this.nodesToRemap != null) {
47+
return this.nodesToRemap;
48+
}
49+
// Get a map of nodes to remap. The first id is the node to map from, the second
50+
// is the node to
51+
// map to.
52+
// Most of the logic here is to recreate old behavior - it might not be to
53+
// everyone's liking.
54+
Map<String, String> nodesToRemap = new HashMap<>();
55+
for (int i = pipelineNodesList.size() - 1; i >= 0; i--) {
56+
FlowNodeWrapper node = pipelineNodesList.get(i);
57+
for (FlowNodeWrapper parent : node.getParents()) {
58+
// Parallel Start Nodes that have a Stage with the same name as a parent will be
59+
// mapped to that
60+
// parent stage
61+
// id.
62+
if (node.getType() == FlowNodeWrapper.NodeType.PARALLEL_BLOCK
63+
&& parent.getType() == FlowNodeWrapper.NodeType.STAGE) {
64+
if (isDebugEnabled) {
65+
logger.debug(
66+
"getNodesToRemap => Found Parallel block {id: {}, name: {}, type: {}} that has a Stage {id: {}, name: {}, type: {}} as a parent. Adding to remap list.",
67+
node.getId(),
68+
node.getDisplayName(),
69+
node.getType(),
70+
parent.getId(),
71+
parent.getDisplayName(),
72+
parent.getType());
73+
}
74+
nodesToRemap.put(node.getId(), parent.getId());
75+
// Skip other checks.
76+
continue;
6577
}
66-
this.nodesToRemap.put(node.getId(), parent.getId());
67-
// Skip other checks.
68-
continue;
69-
}
70-
// If the node has a parent which is a parallel branch, with the same name and
71-
// has only one child (this node)
72-
// then remap child nodes to that parent. This removes some superfluous stages
73-
// in parallel branches.
74-
if (parent.getType() == FlowNodeWrapper.NodeType.PARALLEL
75-
&& node.getDisplayName().equals(parent.getDisplayName())) {
76-
if (isDebugEnabled) {
77-
logger.debug(
78-
"getNodesToRemap => Found Stage {id: {}, name: {}, type: {}} that is an only child and has a parent with the same name {id: {}, name: {}, type: {}}. Adding to remap list.",
79-
node.getId(),
80-
node.getDisplayName(),
81-
node.getType(),
82-
parent.getId(),
83-
parent.getDisplayName(),
84-
parent.getType());
78+
// If the node has a parent which is a parallel branch, with the same name and
79+
// has only one child (this node)
80+
// then remap child nodes to that parent. This removes some superfluous stages
81+
// in parallel branches.
82+
if (parent.getType() == FlowNodeWrapper.NodeType.PARALLEL
83+
&& node.getDisplayName().equals(parent.getDisplayName())) {
84+
if (isDebugEnabled) {
85+
logger.debug(
86+
"getNodesToRemap => Found Stage {id: {}, name: {}, type: {}} that is an only child and has a parent with the same name {id: {}, name: {}, type: {}}. Adding to remap list.",
87+
node.getId(),
88+
node.getDisplayName(),
89+
node.getType(),
90+
parent.getId(),
91+
parent.getDisplayName(),
92+
parent.getType());
93+
}
94+
nodesToRemap.put(node.getId(), parent.getId());
8595
}
86-
this.nodesToRemap.put(node.getId(), parent.getId());
87-
continue;
8896
}
8997
}
90-
}
91-
for (String nodeId : this.nodesToRemap.keySet()) {
92-
this.nodesToRemap.put(nodeId, getFinalParent(nodeId, this.nodesToRemap));
98+
for (String nodeId : nodesToRemap.keySet()) {
99+
nodesToRemap.put(nodeId, getFinalParent(nodeId, nodesToRemap));
100+
}
101+
this.nodesToRemap = nodesToRemap;
93102
}
94103
return this.nodesToRemap;
95104
}
@@ -117,9 +126,7 @@ private void dumpNodeGraphviz(
117126
nodes.addAll(stepsList);
118127
}
119128
}
120-
if (isDebugEnabled) {
121-
logger.debug(FlowNodeWrapper.getNodeGraphviz(nodes));
122-
}
129+
logger.debug(FlowNodeWrapper.getNodeGraphviz(nodes));
123130
}
124131
}
125132

@@ -134,26 +141,25 @@ private void remapStageParentage() {
134141
return;
135142
}
136143
Map<String, FlowNodeWrapper> pipelineNodeMap = treeScanner.getPipelineNodeMap();
137-
138-
this.pipelineNodesList = new ArrayList<FlowNodeWrapper>(pipelineNodeMap.values());
139-
Collections.sort(this.pipelineNodesList, new FlowNodeWrapper.NodeComparator());
144+
List<FlowNodeWrapper> pipelineNodes = new ArrayList<>(pipelineNodeMap.values());
145+
pipelineNodes.sort(new FlowNodeWrapper.NodeComparator());
140146
// Remove children whose parents were skipped.
141-
Map<String, String> nodesToRemap = getNodesToRemap(this.pipelineNodesList);
147+
Map<String, String> nodesToRemap = getNodesToRemap(pipelineNodes);
142148
if (isDebugEnabled) {
143149
logger.debug(
144150
"remapStageParentage => nodesToRemap: {}",
145151
nodesToRemap.entrySet().stream()
146152
.map(entrySet -> entrySet.getKey() + ":" + entrySet.getValue())
147153
.collect(Collectors.joining(",", "[", "]")));
148154
}
149-
dumpNodeGraphviz(this.pipelineNodesList);
155+
dumpNodeGraphviz(pipelineNodes);
150156
// Find all nodes that have a parent to remap (see 'getNodesToRemap') and change
151157
// their parentage
152158
// to the designated parent.
153-
for (int i = this.pipelineNodesList.size() - 1; i >= 0; i--) {
154-
FlowNodeWrapper node = this.pipelineNodesList.get(i);
159+
for (int i = pipelineNodes.size() - 1; i >= 0; i--) {
160+
FlowNodeWrapper node = pipelineNodes.get(i);
155161
List<String> parentIds =
156-
node.getParents().stream().map(FlowNodeWrapper::getId).collect(Collectors.toList());
162+
node.getParents().stream().map(FlowNodeWrapper::getId).toList();
157163
for (String parentId : parentIds) {
158164
if (nodesToRemap.containsKey(parentId)) {
159165
FlowNodeWrapper parent = pipelineNodeMap.get(parentId);
@@ -193,7 +199,7 @@ private void remapStageParentage() {
193199
}
194200
}
195201
// Remove remapped nodes from the tree
196-
this.pipelineNodesList = this.pipelineNodesList.stream()
202+
this.pipelineNodesList = pipelineNodes.stream()
197203
.
198204
// Filter out obsolete Parallel block nodes - ones whose children were remapped
199205
// to a
@@ -212,54 +218,57 @@ private void remapStepParentage() {
212218
if (this.stepsMap != null) {
213219
return;
214220
}
215-
this.stepsMap = treeScanner.getAllSteps();
216-
dumpNodeGraphviz(getPipelineNodes(), this.stepsMap);
217-
Map<String, String> nodesToRemap = getNodesToRemap(getPipelineNodes());
221+
Map<String, List<FlowNodeWrapper>> stepsMap = treeScanner.getAllSteps();
222+
List<FlowNodeWrapper> pipelineNodes = getPipelineNodes();
223+
dumpNodeGraphviz(pipelineNodes, stepsMap);
224+
Map<String, String> nodesToRemap = getNodesToRemap(pipelineNodes);
218225
for (Map.Entry<String, String> remapEntry : nodesToRemap.entrySet()) {
219226
String originalParentId = remapEntry.getKey();
220-
if (this.stepsMap.containsKey(originalParentId)) {
227+
if (stepsMap.containsKey(originalParentId)) {
221228
String remappedParentId = remapEntry.getValue();
222229
if (isDebugEnabled) {
223230
logger.debug(
224231
"remapStepParentage => Remapping {} steps from stage {} to {}.",
225-
this.stepsMap.get(originalParentId).size(),
232+
stepsMap.get(originalParentId).size(),
226233
originalParentId,
227234
remappedParentId);
228235
}
229236
List<FlowNodeWrapper> remappedParentStepsList =
230-
this.stepsMap.getOrDefault(remappedParentId, new ArrayList<>());
231-
remappedParentStepsList.addAll(this.stepsMap.get(originalParentId));
237+
stepsMap.getOrDefault(remappedParentId, new ArrayList<>());
238+
remappedParentStepsList.addAll(stepsMap.get(originalParentId));
232239
// Ensure steps are sorted correctly.
233-
Collections.sort(remappedParentStepsList, new FlowNodeWrapper.NodeComparator());
234-
this.stepsMap.put(remappedParentId, remappedParentStepsList);
235-
this.stepsMap.remove(originalParentId);
240+
remappedParentStepsList.sort(new FlowNodeWrapper.NodeComparator());
241+
stepsMap.put(remappedParentId, remappedParentStepsList);
242+
stepsMap.remove(originalParentId);
236243
}
237244
}
238-
dumpNodeGraphviz(getPipelineNodes(), this.stepsMap);
245+
this.stepsMap = stepsMap;
246+
dumpNodeGraphviz(pipelineNodes, this.stepsMap);
239247
}
240248

241249
public List<FlowNodeWrapper> getPipelineNodes() {
242250
if (this.pipelineNodesList == null) {
243-
remapStageParentage();
251+
synchronized (pipelineLock) {
252+
if (this.pipelineNodesList == null) {
253+
remapStageParentage();
254+
}
255+
}
244256
}
245-
return this.pipelineNodesList;
257+
return Collections.unmodifiableList(this.pipelineNodesList);
246258
}
247259

248260
public Map<String, List<FlowNodeWrapper>> getAllSteps() {
249261
if (this.stepsMap == null) {
250-
remapStepParentage();
262+
synchronized (stepLock) {
263+
if (this.stepsMap == null) {
264+
remapStepParentage();
265+
}
266+
}
251267
}
252-
return this.stepsMap;
268+
return Collections.unmodifiableMap(this.stepsMap);
253269
}
254270

255271
public List<FlowNodeWrapper> getStageSteps(String startNodeId) {
256-
return getAllSteps().getOrDefault(startNodeId, new ArrayList<FlowNodeWrapper>());
257-
}
258-
259-
public Map<String, List<FlowNodeWrapper>> getStep() {
260-
if (this.stepsMap == null) {
261-
remapStepParentage();
262-
}
263-
return this.stepsMap;
272+
return Collections.unmodifiableList(getAllSteps().getOrDefault(startNodeId, new ArrayList<>()));
264273
}
265274
}

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

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,6 @@ public class PipelineNodeTreeScanner {
4040
private final WorkflowRun run;
4141
private final FlowExecution execution;
4242

43-
/**
44-
* Point in time snapshot of all the active heads.
45-
*/
46-
private List<FlowNode> heads;
47-
4843
// Maps a node ID to a given node wrapper. Stores Stages and parallel blocks -
4944
// not steps.
5045
private Map<String, FlowNodeWrapper> stageNodeMap = new LinkedHashMap<>();
@@ -55,7 +50,7 @@ public class PipelineNodeTreeScanner {
5550
private final boolean declarative;
5651

5752
private static final Logger logger = LoggerFactory.getLogger(PipelineNodeTreeScanner.class);
58-
private boolean isDebugEnabled = logger.isDebugEnabled();
53+
private final boolean isDebugEnabled = logger.isDebugEnabled();
5954

6055
public PipelineNodeTreeScanner(@NonNull WorkflowRun run) {
6156
this.run = run;
@@ -101,7 +96,7 @@ public void build() {
10196
* Gets all the nodes that are reachable in the graph.
10297
*/
10398
private List<FlowNode> getAllNodes() {
104-
heads = execution.getCurrentHeads();
99+
List<FlowNode> heads = execution.getCurrentHeads();
105100
final DepthFirstScanner scanner = new DepthFirstScanner();
106101
scanner.setup(heads);
107102

@@ -115,14 +110,11 @@ private List<FlowNode> getAllNodes() {
115110

116111
@NonNull
117112
public List<FlowNodeWrapper> getStageSteps(String startNodeId) {
118-
List<FlowNodeWrapper> stageSteps = new ArrayList<>();
119113
FlowNodeWrapper wrappedStage = stageNodeMap.get(startNodeId);
120-
for (FlowNodeWrapper wrappedStep : stepNodeMap.values()) {
121-
if (wrappedStep.getParents().contains(wrappedStage)) {
122-
stageSteps.add(wrappedStep);
123-
}
124-
}
125-
stageSteps.sort(new FlowNodeWrapper.NodeComparator());
114+
List<FlowNodeWrapper> stageSteps = stepNodeMap.values().stream()
115+
.filter(wrappedStep -> wrappedStep.getParents().contains(wrappedStage))
116+
.sorted(new FlowNodeWrapper.NodeComparator())
117+
.collect(Collectors.toCollection(ArrayList::new));
126118
if (isDebugEnabled) {
127119
logger.debug("Returning {} steps for node '{}'", stageSteps.size(), startNodeId);
128120
}
@@ -150,7 +142,6 @@ public Map<String, FlowNodeWrapper> getPipelineNodeMap() {
150142
return this.stageNodeMap;
151143
}
152144

153-
@NonNull
154145
public boolean isDeclarative() {
155146
return this.declarative;
156147
}
@@ -163,7 +154,7 @@ private static class GraphBuilder {
163154
@NonNull
164155
private final FlowExecution execution;
165156

166-
private Map<String, FlowNodeWrapper> wrappedNodeMap = new LinkedHashMap<>();
157+
private final Map<String, FlowNodeWrapper> wrappedNodeMap = new LinkedHashMap<>();
167158
// These two are populated when required using by filtering unwanted nodes from
168159
// 'wrappedNodeMap' into a new map.
169160
private Map<String, FlowNodeWrapper> wrappedStepMap;
@@ -174,7 +165,7 @@ private static class GraphBuilder {
174165

175166
private final Logger logger = LoggerFactory.getLogger(GraphBuilder.class);
176167
private final InputAction inputAction;
177-
private boolean isDebugEnabled = logger.isDebugEnabled();
168+
private final boolean isDebugEnabled = logger.isDebugEnabled();
178169

179170
/*
180171
* Builds a graph representing this Execution. Stages an steps aer represented
@@ -490,8 +481,8 @@ private void assignParent(@NonNull FlowNodeWrapper wrappedNode, @CheckForNull Fl
490481
private @NonNull FlowNodeWrapper wrapNode(@NonNull FlowNode node, @NonNull NodeRelationship relationship) {
491482
TimingInfo timing = null;
492483
NodeRunStatus status = null;
493-
if (relationship instanceof ParallelBlockRelationship && PipelineNodeUtil.isParallelBranch(node)) {
494-
ParallelBlockRelationship parallelRelationship = (ParallelBlockRelationship) relationship;
484+
if (relationship instanceof ParallelBlockRelationship parallelRelationship
485+
&& PipelineNodeUtil.isParallelBranch(node)) {
495486
timing = parallelRelationship.getBranchTimingInfo(this.run, (BlockStartNode) node);
496487
status = parallelRelationship.getBranchStatus(this.run, (BlockStartNode) node);
497488
} else {
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package io.jenkins.plugins.pipelinegraphview.utils;
2+
3+
import io.jenkins.plugins.pipelinegraphview.treescanner.PipelineNodeGraphAdapter;
4+
import java.util.Map;
5+
import java.util.concurrent.CancellationException;
6+
import java.util.concurrent.CompletableFuture;
7+
import java.util.concurrent.CompletionException;
8+
import java.util.concurrent.ConcurrentHashMap;
9+
import org.jenkinsci.plugins.workflow.job.WorkflowRun;
10+
import org.slf4j.Logger;
11+
import org.slf4j.LoggerFactory;
12+
13+
public class CachedPipelineNodeGraphAdaptor {
14+
15+
public static final CachedPipelineNodeGraphAdaptor instance = new CachedPipelineNodeGraphAdaptor();
16+
private static final Logger log = LoggerFactory.getLogger(CachedPipelineNodeGraphAdaptor.class);
17+
18+
private final Map<String, CompletableFuture<PipelineNodeGraphAdapter>> tasks = new ConcurrentHashMap<>();
19+
20+
private CachedPipelineNodeGraphAdaptor() {}
21+
22+
public PipelineNodeGraphAdapter getFor(WorkflowRun run) {
23+
String key = run.getExternalizableId();
24+
25+
CompletableFuture<PipelineNodeGraphAdapter> task = tasks.computeIfAbsent(key, (ignored) -> {
26+
log.debug("Creating new PipelineNodeGraphAdapter for run: {}", key);
27+
return CompletableFuture.supplyAsync(() -> new PipelineNodeGraphAdapter(run));
28+
});
29+
30+
try {
31+
return task.join();
32+
} catch (CancellationException | CompletionException e) {
33+
throw new RuntimeException("Failure computing graph for " + key, e);
34+
} finally {
35+
tasks.remove(key);
36+
}
37+
}
38+
}

0 commit comments

Comments
 (0)