Skip to content

Commit b1778a9

Browse files
committed
Call StepExecution.onResume in parallel to the extent possible
1 parent 8fba087 commit b1778a9

1 file changed

Lines changed: 103 additions & 23 deletions

File tree

src/main/java/org/jenkinsci/plugins/workflow/flow/FlowExecutionList.java

Lines changed: 103 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,20 @@
2323
import java.io.File;
2424
import java.io.IOException;
2525
import java.util.ArrayList;
26+
import java.util.Collection;
27+
import java.util.HashMap;
28+
import java.util.HashSet;
2629
import java.util.Iterator;
2730
import java.util.List;
31+
import java.util.Map;
32+
import java.util.Set;
2833
import java.util.concurrent.CancellationException;
2934
import java.util.concurrent.RejectedExecutionException;
3035
import java.util.concurrent.TimeUnit;
3136
import java.util.logging.Level;
3237
import java.util.logging.Logger;
38+
import org.jenkinsci.plugins.workflow.graph.FlowNode;
39+
import org.jenkinsci.plugins.workflow.graphanalysis.LinearBlockHoppingScanner;
3340

3441
import org.kohsuke.accmod.Restricted;
3542
import org.kohsuke.accmod.restrictions.Beta;
@@ -259,29 +266,19 @@ public void onResumed(@NonNull FlowExecution e) {
259266
Futures.addCallback(e.getCurrentExecutions(false), new FutureCallback<List<StepExecution>>() {
260267
@Override
261268
public void onSuccess(@NonNull List<StepExecution> result) {
262-
try {
263-
if (e.isComplete()) {
264-
// WorkflowRun.onLoad will not fireResumed if the execution was already complete when loaded,
265-
// and CpsFlowExecution should not then complete until afterStepExecutionsResumed, so this is defensive.
266-
return;
267-
}
268-
FlowExecutionList list = FlowExecutionList.get();
269-
FlowExecutionOwner owner = e.getOwner();
270-
if (!list.runningTasks.contains(owner)) {
271-
LOGGER.log(Level.WARNING, "Resuming {0}, which is missing from FlowExecutionList ({1}), so registering it now.", new Object[] {owner, list.runningTasks.getView()});
272-
list.register(owner);
273-
}
274-
LOGGER.log(Level.FINE, "Will resume {0}", result);
275-
for (StepExecution se : result) {
276-
try {
277-
se.onResume();
278-
} catch (Throwable x) {
279-
se.getContext().onFailure(x);
280-
}
281-
}
282-
} finally {
283-
e.afterStepExecutionsResumed();
269+
if (e.isComplete()) {
270+
// WorkflowRun.onLoad will not fireResumed if the execution was already complete when loaded,
271+
// and CpsFlowExecution should not then complete until afterStepExecutionsResumed, so this is defensive.
272+
return;
284273
}
274+
FlowExecutionList list = FlowExecutionList.get();
275+
FlowExecutionOwner owner = e.getOwner();
276+
if (!list.runningTasks.contains(owner)) {
277+
LOGGER.log(Level.WARNING, "Resuming {0}, which is missing from FlowExecutionList ({1}), so registering it now.", new Object[] {owner, list.runningTasks.getView()});
278+
list.register(owner);
279+
}
280+
LOGGER.log(Level.FINE, "Will resume {0}", result);
281+
new ParallelResumer(result, e::afterStepExecutionsResumed).run();
285282
}
286283

287284
@Override
@@ -294,7 +291,90 @@ public void onFailure(@NonNull Throwable t) {
294291
e.afterStepExecutionsResumed();
295292
}
296293

297-
}, Timer.get()); // We always hold RunMap and WorkflowRun locks here, so we resume steps on a different thread to avoid potential deadlocks. See JENKINS-67351.
294+
}, MoreExecutors.directExecutor());
298295
}
299296
}
297+
298+
/** Calls {@link StepExecution#onResume} for each step in a running build.
299+
* Does so in parallel, but always completing enclosing blocks before the enclosed step.
300+
* A simplified version of https://stackoverflow.com/a/67449067/12916, since this should be a tree not a general DAG.
301+
*/
302+
private static final class ParallelResumer {
303+
304+
private final Runnable onCompletion;
305+
/** Step nodes mapped to the step execution. Entries removed when they are ready to be resumed. */
306+
private final Map<FlowNode, StepExecution> nodes = new HashMap<>();
307+
/** Step nodes currently being resumed. Removed after resumption completes. */
308+
private final Set<FlowNode> processing = new HashSet<>();
309+
/** Step nodes mapped to the nearest enclosing step node (no entry if at root). */
310+
private final Map<FlowNode, FlowNode> enclosing = new HashMap<>();
311+
312+
ParallelResumer(Collection<StepExecution> executions, Runnable onCompletion) {
313+
this.onCompletion = onCompletion;
314+
// First look up positions in the flow graph, so that we can compute dependencies:
315+
for (StepExecution se : executions) {
316+
try {
317+
FlowNode n = se.getContext().get(FlowNode.class);
318+
if (n != null) {
319+
nodes.put(n, se);
320+
} else {
321+
LOGGER.warning(() -> "Could not find FlowNode for " + se + " so it will not be resumed");
322+
}
323+
} catch (IOException | InterruptedException x) {
324+
LOGGER.log(Level.WARNING, "Could not look up FlowNode for " + se + " so it will not be resumed", x);
325+
}
326+
}
327+
for (FlowNode n : nodes.keySet()) {
328+
LinearBlockHoppingScanner scanner = new LinearBlockHoppingScanner();
329+
scanner.setup(n);
330+
for (FlowNode parent : scanner) {
331+
if (parent != n && nodes.containsKey(parent)) {
332+
enclosing.put(n, parent);
333+
break;
334+
}
335+
}
336+
}
337+
}
338+
339+
synchronized void run() {
340+
LOGGER.fine(() -> "Checking status with nodes=" + nodes + " enclosing=" + enclosing + " processing=" + processing);
341+
if (nodes.isEmpty()) {
342+
if (processing.isEmpty()) {
343+
LOGGER.fine("Done");
344+
onCompletion.run();
345+
}
346+
return;
347+
}
348+
Map<FlowNode, StepExecution> ready = new HashMap<>();
349+
for (Map.Entry<FlowNode, StepExecution> entry : nodes.entrySet()) {
350+
FlowNode n = entry.getKey();
351+
FlowNode parent = enclosing.get(n);
352+
if (parent == null || !nodes.containsKey(parent)) {
353+
ready.put(n, entry.getValue());
354+
}
355+
}
356+
LOGGER.fine(() -> "Ready to resume: " + ready);
357+
nodes.keySet().removeAll(ready.keySet());
358+
for (Map.Entry<FlowNode, StepExecution> entry : ready.entrySet()) {
359+
FlowNode n = entry.getKey();
360+
StepExecution exec = entry.getValue();
361+
processing.add(n);
362+
Timer.get().submit(() -> {
363+
LOGGER.fine(() -> "About to resume " + n + " ~ " + exec);
364+
try {
365+
exec.onResume();
366+
} catch (Throwable x) {
367+
exec.getContext().onFailure(x);
368+
}
369+
LOGGER.fine(() -> "Finished resuming " + n + " ~ " + exec);
370+
synchronized (ParallelResumer.this) {
371+
processing.remove(n);
372+
run();
373+
}
374+
});
375+
}
376+
}
377+
378+
}
379+
300380
}

0 commit comments

Comments
 (0)