Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Revert "[JENKINS-37573] / [JENKINS-45553] Provide a fast view of bloc…
…k structures in the flow graph"
  • Loading branch information
svanoort committed Sep 22, 2017
1 parent 4ba6b42 commit 88ffdfc
Show file tree
Hide file tree
Showing 10 changed files with 132 additions and 830 deletions.
2 changes: 1 addition & 1 deletion pom.xml
Expand Up @@ -28,7 +28,7 @@
<parent>
<groupId>org.jenkins-ci.plugins</groupId>
<artifactId>plugin</artifactId>
<version>2.35</version>
<version>2.31</version>
<relativePath />
</parent>
<groupId>org.jenkins-ci.plugins.workflow</groupId>
Expand Down
Expand Up @@ -29,13 +29,9 @@
import hudson.model.Executor;
import jenkins.model.CauseOfInterruption;
import org.jenkinsci.plugins.workflow.actions.ErrorAction;
import org.jenkinsci.plugins.workflow.graph.BlockEndNode;
import org.jenkinsci.plugins.workflow.graph.BlockStartNode;
import org.jenkinsci.plugins.workflow.graph.FlowActionStorage;
import org.jenkinsci.plugins.workflow.graph.FlowEndNode;
import org.jenkinsci.plugins.workflow.graph.FlowNode;
import org.jenkinsci.plugins.workflow.graph.GraphLookupView;
import org.jenkinsci.plugins.workflow.graph.StandardGraphLookupView;
import org.jenkinsci.plugins.workflow.steps.StepContext;
import hudson.model.Result;
import hudson.security.ACL;
Expand All @@ -49,8 +45,6 @@
import jenkins.model.queue.AsynchronousExecution;
import org.acegisecurity.Authentication;
import org.jenkinsci.plugins.workflow.steps.FlowInterruptedException;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;

/**
* State of a currently executing workflow.
Expand All @@ -69,19 +63,7 @@
* @author Kohsuke Kawaguchi
* @author Jesse Glick
*/
public abstract class FlowExecution implements FlowActionStorage, GraphLookupView { // Implements GraphLookupView because FlowNode lives in another package

protected transient GraphLookupView internalGraphLookup = null;

/** Eventually this may be overridden if the FlowExecution has a better source of structural information, such as the {@link FlowNode} storage. */
protected synchronized GraphLookupView getInternalGraphLookup() {
if (internalGraphLookup == null) {
StandardGraphLookupView lookupView = new StandardGraphLookupView();
this.internalGraphLookup = lookupView;
this.addListener(lookupView);
}
return internalGraphLookup;
}
public abstract class FlowExecution implements FlowActionStorage {

/**
* Called after {@link FlowDefinition#create(FlowExecutionOwner, List)} to
Expand Down Expand Up @@ -238,63 +220,4 @@ public boolean blocksRestart() {
*/
public abstract @Nonnull Authentication getAuthentication();


/** @see GraphLookupView#isActive(FlowNode)
* @throws IllegalArgumentException If the input {@link FlowNode} does not belong to this execution
*/
@Restricted(NoExternalUse.class) // Only public because graph, flow, and graphanalysis are separate packages
public boolean isActive(@Nonnull FlowNode node) {
if (!this.equals(node.getExecution())) {
throw new IllegalArgumentException("Can't look up info for a FlowNode that doesn't belong to this execution!");
}
return getInternalGraphLookup().isActive(node);
}

/** @see GraphLookupView#getEndNode(BlockStartNode)
* @throws IllegalArgumentException If the input {@link FlowNode} does not belong to this execution
*/
@CheckForNull
@Restricted(NoExternalUse.class) // Only public because graph, flow, and graphanalysis are separate packages
public BlockEndNode getEndNode(@Nonnull BlockStartNode startNode) {
if (!this.equals(startNode.getExecution())) {
throw new IllegalArgumentException("Can't look up info for a FlowNode that doesn't belong to this execution!");
}
return getInternalGraphLookup().getEndNode(startNode);
}

/** @see GraphLookupView#findEnclosingBlockStart(FlowNode)
* @throws IllegalArgumentException If the input {@link FlowNode} does not belong to this execution
*/
@CheckForNull
@Restricted(NoExternalUse.class) // Only public because graph, flow, and graphanalysis are separate packages
public BlockStartNode findEnclosingBlockStart(@Nonnull FlowNode node) {
if (!this.equals(node.getExecution())) {
throw new IllegalArgumentException("Can't look up info for a FlowNode that doesn't belong to this execution!");
}
return getInternalGraphLookup().findEnclosingBlockStart(node);
}

/** @see GraphLookupView#findAllEnclosingBlockStarts(FlowNode)
* @throws IllegalArgumentException If the input {@link FlowNode} does not belong to this execution
*/
@Nonnull
@Restricted(NoExternalUse.class) // Only public because graph, flow, and graphanalysis are separate packages
public List<BlockStartNode> findAllEnclosingBlockStarts(@Nonnull FlowNode node) {
if (!this.equals(node.getExecution())) {
throw new IllegalArgumentException("Can't look up info for a FlowNode that doesn't belong to this execution!");
}
return getInternalGraphLookup().findAllEnclosingBlockStarts(node);
}

/** @see GraphLookupView#iterateEnclosingBlocks(FlowNode)
* @throws IllegalArgumentException If the input {@link FlowNode} does not belong to this execution
*/
@Nonnull
@Restricted(NoExternalUse.class)
public Iterable<BlockStartNode> iterateEnclosingBlocks(@Nonnull FlowNode node) {
if (!this.equals(node.getExecution())) {
throw new IllegalArgumentException("Can't look up info for a FlowNode that doesn't belong to this execution!");
}
return getInternalGraphLookup().iterateEnclosingBlocks(node);
}
}
Expand Up @@ -26,7 +26,6 @@

import org.jenkinsci.plugins.workflow.flow.FlowExecution;

import javax.annotation.CheckForNull;
import java.util.List;

/**
Expand All @@ -46,9 +45,4 @@ protected BlockStartNode(FlowExecution exec, String id, List<FlowNode> parents)
super(exec, id, parents);
}

/** Return the {@link BlockEndNode} for this block, or null if the block hasn't completed yet. */
@CheckForNull
public BlockEndNode getEndNode() {
return this.getExecution().getEndNode(this);
}
}
193 changes: 120 additions & 73 deletions src/main/java/org/jenkinsci/plugins/workflow/graph/FlowNode.java
Expand Up @@ -25,10 +25,6 @@
package org.jenkinsci.plugins.workflow.graph;

import com.google.common.base.Predicates;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.ImmutableList;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import hudson.Extension;
Expand All @@ -42,19 +38,15 @@
import java.io.ObjectStreamException;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;

import org.jenkinsci.plugins.workflow.actions.ErrorAction;
import org.jenkinsci.plugins.workflow.actions.LabelAction;
import org.jenkinsci.plugins.workflow.actions.PersistentAction;
Expand Down Expand Up @@ -90,17 +82,13 @@ protected FlowNode(FlowExecution exec, String id, List<FlowNode> parents) {
this.exec = exec;
this.parents = ImmutableList.copyOf(parents);
parentIds = ids();
// Note that enclosingId is set in the constructors of AtomNode, BlockEndNode, and BlockStartNode, since we need
// BlockEndNode in particular to have its start node beforehand.
}

protected FlowNode(FlowExecution exec, String id, FlowNode... parents) {
this.id = id;
this.exec = exec;
this.parents = ImmutableList.copyOf(parents);
parentIds = ids();
// Note that enclosingId is set in the constructors of AtomNode, BlockEndNode, and BlockStartNode, since we need
// BlockEndNode in particular to have its start node beforehand.
}

private List<String> ids() {
Expand Down Expand Up @@ -144,7 +132,114 @@ public final boolean isRunning() {
*/
@Exported(name="running")
public final boolean isActive() {
return this.getExecution().isActive(this);
if (this instanceof FlowEndNode) { // cf. JENKINS-26139
LOGGER.finer("shortcut: FlowEndNode is never active");
return false;
}
if (this instanceof BlockStartNode) {
Map<FlowExecutionOwner, Map<String, Boolean>> startNodesAreClosedByFlow = FlowL.startNodesAreClosedByFlow();
LOGGER.log(Level.FINER, "for {0}, startNodesAreClosedByFlow={1}", new Object[] {this, startNodesAreClosedByFlow});
Map<String, Boolean> startNodesAreClosed = startNodesAreClosedByFlow.get(exec.getOwner());
if (startNodesAreClosed != null) {
Boolean closed = startNodesAreClosed.get(id);
if (closed != null) { // quick version
LOGGER.log(Level.FINER, "quick closed={0}", closed);
return !closed;
} else {
LOGGER.log(Level.FINER, "no record of {0} in {1}, presumably GraphListener not working", new Object[] {this, exec});
}
} else {
LOGGER.log(Level.FINER, "no record of {0}, either FlowExecutionListener not working or it is already complete", exec);
}
} // atom or end node, or fall through to slow mode for start node
List<FlowNode> currentHeads = exec.getCurrentHeads();
if (currentHeads.contains(this)) {
LOGGER.log(Level.FINER, "{0} is a current head", this);
return true;
}
if (currentHeads.size() == 1 && currentHeads.get(0) instanceof FlowEndNode) { // i.e., exec.isComplete()
LOGGER.log(Level.FINER, "{0} is complete", exec);
return false;
}
if (this instanceof BlockStartNode) {
// Fallback (old workflow-job, old workflow-cps):
LOGGER.log(Level.FINER, "slow currentHeads={0}", currentHeads);
for (FlowNode headNode : currentHeads) {
if (new LinearBlockHoppingScanner().findFirstMatch(headNode, Predicates.equalTo(this)) != null) {
LOGGER.finer("slow match");
return true;
}
}
LOGGER.finer("slow no match");
return false;
}
LOGGER.log(Level.FINER, "{0} is not a current head nor a start node", this);
return false;
}
/**
* Cache of known block start node statuses.
* Keys are running executions ~ builds.
* Values are maps from {@link BlockStartNode#getId} to whether the corresponding {@link BlockEndNode} has been encountered.
* For old {@code workflow-job}, the top-level entries will be missing;
* for old {@code workflow-cps}, the second-level entries will be missing.
*/
@Restricted(DoNotUse.class)
@Extension public static final class GraphL implements GraphListener.Synchronous {
@Override public void onNewHead(FlowNode node) {
LOGGER.finer("GraphListener working");
if (node instanceof BlockStartNode || node instanceof BlockEndNode) {
Map<String, Boolean> startNodesAreClosed = FlowL.startNodesAreClosedByFlow().get(node.getExecution().getOwner());
if (startNodesAreClosed != null) {
if (node instanceof BlockStartNode) {
assert !startNodesAreClosed.containsKey(node.getId());
// Starting a block; record that it is open.
startNodesAreClosed.put(node.getId(), false);
} else {
// Closed a block; find the matching start node and record that it is now closed.
startNodesAreClosed.put(((BlockEndNode) node).getStartNode().getId(), true);
}
} // else we must have an old workflow-job
} // do not need to pay attention to atom nodes: either they are current heads, thus active, or they are not, thus inactive
}
}
@Restricted(DoNotUse.class)
@Extension public static final class FlowL extends FlowExecutionListener {
final Map<FlowExecutionOwner, Map<String, Boolean>> startNodesAreClosedByFlow = new HashMap<>();
static Map<FlowExecutionOwner, Map<String, Boolean>> startNodesAreClosedByFlow() {
FlowL flowL = ExtensionList.lookup(FlowExecutionListener.class).get(FlowL.class);
if (flowL == null) { // should not happen unless Jenkins is busted
throw new IllegalStateException("missing FlowNode.FlowL extension");
}
return flowL.startNodesAreClosedByFlow;
}
@Override public void onRunning(FlowExecution execution) {
LOGGER.finer("FlowExecutionListener working");
assert !startNodesAreClosedByFlow.containsKey(execution.getOwner());
startNodesAreClosedByFlow.put(execution.getOwner(), new HashMap<String, Boolean>());
}
@Override public void onResumed(FlowExecution execution) {
assert !startNodesAreClosedByFlow.containsKey(execution.getOwner());
Map<String, Boolean> startNodesAreClosed = new HashMap<String, Boolean>();
startNodesAreClosedByFlow.put(execution.getOwner(), startNodesAreClosed);
// To handle start nodes encountered in a prior Jenkins session, try to recreate the cache to date:
DepthFirstScanner dfs = new DepthFirstScanner();
dfs.setup(execution.getCurrentHeads());
for (FlowNode n : dfs) { // end nodes first, later the start nodes
if (n instanceof BlockEndNode) {
startNodesAreClosed.put(((BlockEndNode) n).getStartNode().getId(), true);
} else if (n instanceof BlockStartNode) {
if (!startNodesAreClosed.containsKey(n.getId())) {
// If we have not encountered the BlockEndNode, it remains open.
startNodesAreClosed.put(n.getId(), false);
}
}
}
}
@Override public void onCompleted(FlowExecution execution) {
assert startNodesAreClosedByFlow.containsKey(execution.getOwner());
// After a build finishes, we do not need the cache any more, since we do the equivalent of FlowExecution.isComplete relatively quickly:
startNodesAreClosedByFlow.remove(execution.getOwner());
}
}

/**
Expand Down Expand Up @@ -172,74 +267,26 @@ public List<FlowNode> getParents() {

@Nonnull
private List<FlowNode> loadParents(List<String> parentIds) {
try {
if (parentIds.size() == 1) {
return Collections.singletonList(exec.getNode(parentIds.get(0)));
} else {
List<FlowNode> _parents = new ArrayList<>(parentIds.size());
for (String parentId : parentIds) {
_parents.add(exec.getNode(parentId));
}
return _parents;
List<FlowNode> _parents = new ArrayList<>(parentIds.size());
for (String parentId : parentIds) {
try {
_parents.add(exec.getNode(parentId));
} catch (IOException x) {
LOGGER.log(Level.WARNING, "failed to load parents of " + id, x);
}
} catch (IOException x) {
LOGGER.log(Level.WARNING, "failed to load parents of " + id, x);
return Collections.emptyList();
}
}

/**
* Get the {@link #id} of the enclosing {@link BlockStartNode}for this node, or null if none.
* Only {@link FlowStartNode} and {@link FlowEndNode} should generally return null.
*/
@CheckForNull
public String getEnclosingId() {
FlowNode enclosing = this.exec.findEnclosingBlockStart(this);
return enclosing != null ? enclosing.getId() : null;
}

/**
* Get the list of enclosing {@link BlockStartNode}s, starting from innermost, for this node.
* May be empty if we are the {@link FlowStartNode} or {@link FlowEndNode}
*/
@Nonnull
public List<FlowNode> getEnclosingBlocks() {
return (List)this.exec.findAllEnclosingBlockStarts(this);
}

/** Return an iterator over all enclosing blocks.
* Prefer this to {@link #getEnclosingBlocks()} unless you need ALL nodes, because it can evaluate lazily. */
@Nonnull
public Iterable<BlockStartNode> iterateEnclosingBlocks() {
return this.exec.iterateEnclosingBlocks(this);
}

/**
* Returns a read-only view of the IDs for enclosing blocks of this flow node, innermost first. May be empty.
*/
@Nonnull
public List<String> getAllEnclosingIds() {
List<FlowNode> nodes = getEnclosingBlocks();
ArrayList<String> output = new ArrayList<String>(nodes.size());
for (FlowNode f : nodes) {
output.add(f.getId());
}
return output;
return _parents;
}

@Restricted(DoNotUse.class)
@Exported(name="parents")
@Nonnull
public List<String> getParentIds() {
if (parentIds != null) {
return Collections.unmodifiableList(parentIds);
} else {
List<String> ids = new ArrayList<>(parents.size());
for (FlowNode parent : getParents()) {
ids.add(parent.getId());
}
return ids;
List<String> ids = new ArrayList<>(2);
for (FlowNode parent : getParents()) {
ids.add(parent.getId());
}
return ids;
}

/**
Expand Down Expand Up @@ -302,7 +349,7 @@ public BallColor getIconColor() {
/**
* Gets a human readable name for this type of the node.
*
* This is used to implement {@link #getDisplayName()} as a fallback in case {@link LabelAction} does not exist.
* This is used to implement {@link #getDisplayName()} as a fallback in case {@link LabelAction} doesnt exist.
*/
protected abstract String getTypeDisplayName();

Expand Down

0 comments on commit 88ffdfc

Please sign in to comment.