Java tutorial
/* * Copyright 2012 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.gradle.execution.plan; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import org.gradle.api.Action; import org.gradle.api.BuildCancelledException; import org.gradle.api.CircularReferenceException; import org.gradle.api.GradleException; import org.gradle.api.NonNullApi; import org.gradle.api.Project; import org.gradle.api.Task; import org.gradle.api.Transformer; import org.gradle.api.UncheckedIOException; import org.gradle.api.file.FileCollection; import org.gradle.api.internal.GradleInternal; import org.gradle.api.internal.TaskInternal; import org.gradle.api.internal.project.ProjectInternal; import org.gradle.api.internal.tasks.execution.DefaultTaskProperties; import org.gradle.api.internal.tasks.execution.TaskProperties; import org.gradle.api.internal.tasks.properties.PropertyWalker; import org.gradle.api.specs.Spec; import org.gradle.api.specs.Specs; import org.gradle.internal.Pair; import org.gradle.internal.file.PathToFileResolver; import org.gradle.internal.graph.CachingDirectedGraphWalker; import org.gradle.internal.graph.DirectedGraph; import org.gradle.internal.graph.DirectedGraphRenderer; import org.gradle.internal.graph.GraphNodeRenderer; import org.gradle.internal.logging.text.StyledTextOutput; import org.gradle.internal.resources.ResourceDeadlockException; import org.gradle.internal.resources.ResourceLock; import org.gradle.internal.resources.ResourceLockState; import org.gradle.internal.service.ServiceRegistry; import org.gradle.internal.work.WorkerLeaseRegistry; import org.gradle.internal.work.WorkerLeaseService; import org.gradle.util.CollectionUtils; import org.gradle.util.Path; import javax.annotation.Nullable; import java.io.File; import java.io.IOException; import java.io.StringWriter; import java.util.AbstractCollection; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Deque; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * A reusable implementation of ExecutionPlan. The {@link #addEntryTasks(java.util.Collection)} and {@link #clear()} methods are NOT threadsafe, and callers must synchronize access to these methods. */ @NonNullApi public class DefaultExecutionPlan implements ExecutionPlan { private final Set<Node> nodesInUnknownState = Sets.newLinkedHashSet(); private final Set<TaskNode> entryTasks = new LinkedHashSet<TaskNode>(); private final NodeMapping nodeMapping = new NodeMapping(); private final List<Node> executionQueue = Lists.newLinkedList(); private final Map<Project, ResourceLock> projectLocks = Maps.newHashMap(); private final FailureCollector failureCollector = new FailureCollector(); private final TaskNodeFactory taskNodeFactory; private final TaskDependencyResolver dependencyResolver; private Spec<? super Task> filter = Specs.satisfyAll(); private boolean continueOnFailure; private final Set<Node> runningNodes = Sets.newIdentityHashSet(); private final Set<Node> filteredNodes = Sets.newIdentityHashSet(); private final Map<Node, MutationInfo> mutations = Maps.newIdentityHashMap(); private final Map<File, String> canonicalizedFileCache = Maps.newIdentityHashMap(); private final Map<Pair<Node, Node>, Boolean> reachableCache = Maps.newHashMap(); private final Set<Node> dependenciesCompleteCache = Sets.newHashSet(); private final WorkerLeaseService workerLeaseService; private final GradleInternal gradle; private boolean buildCancelled; public DefaultExecutionPlan(WorkerLeaseService workerLeaseService, GradleInternal gradle, TaskNodeFactory taskNodeFactory, TaskDependencyResolver dependencyResolver) { this.workerLeaseService = workerLeaseService; this.gradle = gradle; this.taskNodeFactory = taskNodeFactory; this.dependencyResolver = dependencyResolver; } @Override public String getDisplayName() { Path path = gradle.findIdentityPath(); if (path == null) { return "gradle"; } return path.toString(); } @VisibleForTesting TaskNode getNode(Task task) { return nodeMapping.get(task); } public void addEntryTasks(Collection<? extends Task> tasks) { final Deque<Node> queue = new ArrayDeque<Node>(); List<Task> sortedTasks = new ArrayList<Task>(tasks); Collections.sort(sortedTasks); for (Task task : sortedTasks) { TaskNode node = taskNodeFactory.getOrCreateNode(task); if (node.isMustNotRun()) { requireWithDependencies(node); } else if (filter.isSatisfiedBy(task)) { node.require(); } entryTasks.add(node); queue.add(node); } final Set<Node> visiting = Sets.newHashSet(); while (!queue.isEmpty()) { Node node = queue.getFirst(); if (node.getDependenciesProcessed()) { // Have already visited this node - skip it queue.removeFirst(); continue; } boolean filtered = !nodeSatisfiesTaskFilter(node); if (filtered) { // Task is not required - skip it queue.removeFirst(); node.dependenciesProcessed(); node.doNotRequire(); filteredNodes.add(node); continue; } if (visiting.add(node)) { // Have not seen this node before - add its dependencies to the head of the queue and leave this // node in the queue // Make sure it has been configured node.prepareForExecution(); node.resolveDependencies(dependencyResolver, new Action<Node>() { @Override public void execute(Node targetNode) { if (!visiting.contains(targetNode)) { queue.addFirst(targetNode); } } }); if (node.isRequired()) { for (Node successor : node.getDependencySuccessors()) { if (nodeSatisfiesTaskFilter(successor)) { successor.require(); } } } else { nodesInUnknownState.add(node); } } else { // Have visited this node's dependencies - add it to the graph queue.removeFirst(); visiting.remove(node); node.dependenciesProcessed(); } } resolveNodesInUnknownState(); } private boolean nodeSatisfiesTaskFilter(Node successor) { if (successor instanceof LocalTaskNode) { return filter.isSatisfiedBy(((LocalTaskNode) successor).getTask()); } return true; } private void resolveNodesInUnknownState() { List<Node> queue = Lists.newArrayList(nodesInUnknownState); Set<Node> visiting = Sets.newHashSet(); while (!queue.isEmpty()) { Node node = queue.get(0); if (node.isInKnownState()) { queue.remove(0); continue; } if (visiting.add(node)) { for (Node hardPredecessor : node.getDependencyPredecessors()) { if (!visiting.contains(hardPredecessor)) { queue.add(0, hardPredecessor); } } } else { queue.remove(0); visiting.remove(node); node.mustNotRun(); for (Node predecessor : node.getDependencyPredecessors()) { assert predecessor.isRequired() || predecessor.isMustNotRun(); if (predecessor.isRequired()) { node.require(); break; } } } } } private void requireWithDependencies(Node node) { if (node.isMustNotRun() && nodeSatisfiesTaskFilter(node)) { node.require(); for (Node dependency : node.getDependencySuccessors()) { requireWithDependencies(dependency); } } } public void determineExecutionPlan() { List<NodeInVisitingSegment> nodeQueue = Lists .newArrayList(Iterables.transform(entryTasks, new Function<TaskNode, NodeInVisitingSegment>() { private int index; @Override @SuppressWarnings("NullableProblems") public NodeInVisitingSegment apply(TaskNode taskNode) { return new NodeInVisitingSegment(taskNode, index++); } })); int visitingSegmentCounter = nodeQueue.size(); HashMultimap<Node, Integer> visitingNodes = HashMultimap.create(); Deque<GraphEdge> walkedShouldRunAfterEdges = new ArrayDeque<GraphEdge>(); Deque<Node> path = new ArrayDeque<Node>(); Map<Node, Integer> planBeforeVisiting = Maps.newHashMap(); while (!nodeQueue.isEmpty()) { NodeInVisitingSegment nodeInVisitingSegment = nodeQueue.get(0); int currentSegment = nodeInVisitingSegment.visitingSegment; Node node = nodeInVisitingSegment.node; if (node.isIncludeInGraph() || nodeMapping.contains(node)) { nodeQueue.remove(0); visitingNodes.remove(node, currentSegment); maybeRemoveProcessedShouldRunAfterEdge(walkedShouldRunAfterEdges, node); continue; } boolean alreadyVisited = visitingNodes.containsKey(node); visitingNodes.put(node, currentSegment); if (!alreadyVisited) { // Have not seen this node before - add its dependencies to the head of the queue and leave this // node in the queue recordEdgeIfArrivedViaShouldRunAfter(walkedShouldRunAfterEdges, path, node); removeShouldRunAfterSuccessorsIfTheyImposeACycle(visitingNodes, nodeInVisitingSegment); takePlanSnapshotIfCanBeRestoredToCurrentTask(planBeforeVisiting, node); for (Node successor : node.getAllSuccessorsInReverseOrder()) { if (visitingNodes.containsEntry(successor, currentSegment)) { if (!walkedShouldRunAfterEdges.isEmpty()) { //remove the last walked should run after edge and restore state from before walking it GraphEdge toBeRemoved = walkedShouldRunAfterEdges.pop(); // Should run after edges only exist between tasks, so this cast is safe TaskNode sourceTask = (TaskNode) toBeRemoved.from; TaskNode targetTask = (TaskNode) toBeRemoved.to; sourceTask.removeShouldSuccessor(targetTask); restorePath(path, toBeRemoved); restoreQueue(nodeQueue, visitingNodes, toBeRemoved); restoreExecutionPlan(planBeforeVisiting, toBeRemoved); break; } else { onOrderingCycle(successor, node); } } nodeQueue.add(0, new NodeInVisitingSegment(successor, currentSegment)); } path.push(node); } else { // Have visited this node's dependencies - add it to the end of the plan nodeQueue.remove(0); maybeRemoveProcessedShouldRunAfterEdge(walkedShouldRunAfterEdges, node); visitingNodes.remove(node, currentSegment); path.pop(); nodeMapping.add(node); MutationInfo mutations = getOrCreateMutationsOf(node); for (Node dependency : node.getDependencySuccessors()) { getOrCreateMutationsOf(dependency).consumingNodes.add(node); mutations.producingNodes.add(dependency); } if (node instanceof LocalTaskNode) { LocalTaskNode taskNode = (LocalTaskNode) node; TaskInternal task = taskNode.getTask(); Project project = task.getProject(); projectLocks.put(project, getOrCreateProjectLock(project)); // Add any finalizers to the queue for (Node finalizer : taskNode.getFinalizers()) { if (!visitingNodes.containsKey(finalizer)) { nodeQueue.add(finalizerTaskPosition(finalizer, nodeQueue), new NodeInVisitingSegment(finalizer, visitingSegmentCounter++)); } } } } } executionQueue.clear(); Iterables.addAll(executionQueue, nodeMapping); } @Override public Set<Task> getDependencies(Task task) { TaskNode node = nodeMapping.get(task); ImmutableSet.Builder<Task> builder = ImmutableSet.builder(); for (Node dependencyNode : node.getDependencySuccessors()) { dependencyNode.collectTaskInto(builder); } return builder.build(); } private MutationInfo getOrCreateMutationsOf(Node node) { MutationInfo mutations = this.mutations.get(node); if (mutations == null) { mutations = new MutationInfo(node); this.mutations.put(node, mutations); } return mutations; } private void maybeRemoveProcessedShouldRunAfterEdge(Deque<GraphEdge> walkedShouldRunAfterEdges, Node node) { GraphEdge edge = walkedShouldRunAfterEdges.peek(); if (edge != null && edge.to.equals(node)) { walkedShouldRunAfterEdges.pop(); } } private void restoreExecutionPlan(Map<Node, Integer> planBeforeVisiting, GraphEdge toBeRemoved) { int count = planBeforeVisiting.get(toBeRemoved.from); nodeMapping.retainFirst(count); } private void restoreQueue(List<NodeInVisitingSegment> nodeQueue, HashMultimap<Node, Integer> visitingNodes, GraphEdge toBeRemoved) { NodeInVisitingSegment nextInQueue = null; while (nextInQueue == null || !toBeRemoved.from.equals(nextInQueue.node)) { nextInQueue = nodeQueue.get(0); visitingNodes.remove(nextInQueue.node, nextInQueue.visitingSegment); if (!toBeRemoved.from.equals(nextInQueue.node)) { nodeQueue.remove(0); } } } private void restorePath(Deque<Node> path, GraphEdge toBeRemoved) { Node removedFromPath = null; while (!toBeRemoved.from.equals(removedFromPath)) { removedFromPath = path.pop(); } } private void removeShouldRunAfterSuccessorsIfTheyImposeACycle(final HashMultimap<Node, Integer> visitingNodes, final NodeInVisitingSegment nodeWithVisitingSegment) { Node node = nodeWithVisitingSegment.node; if (!(node instanceof TaskNode)) { return; } Iterables.removeIf(((TaskNode) node).getShouldSuccessors(), new Predicate<Node>() { @Override @SuppressWarnings("NullableProblems") public boolean apply(Node input) { return visitingNodes.containsEntry(input, nodeWithVisitingSegment.visitingSegment); } }); } private void takePlanSnapshotIfCanBeRestoredToCurrentTask(Map<Node, Integer> planBeforeVisiting, Node node) { if (node instanceof TaskNode && !((TaskNode) node).getShouldSuccessors().isEmpty()) { planBeforeVisiting.put(node, nodeMapping.size()); } } private void recordEdgeIfArrivedViaShouldRunAfter(Deque<GraphEdge> walkedShouldRunAfterEdges, Deque<Node> path, Node node) { if (!(node instanceof TaskNode)) { return; } Node previous = path.peek(); if (previous instanceof TaskNode && ((TaskNode) previous).getShouldSuccessors().contains(node)) { walkedShouldRunAfterEdges.push(new GraphEdge(previous, node)); } } /** * Given a finalizer task, determine where in the current node queue that it should be inserted. * The finalizer should be inserted after any of it's preceding tasks. */ private int finalizerTaskPosition(Node finalizer, final List<NodeInVisitingSegment> nodeQueue) { if (nodeQueue.size() == 0) { return 0; } Set<Node> precedingTasks = getAllPrecedingNodes(finalizer); Set<Integer> precedingTaskIndices = CollectionUtils.collect(precedingTasks, new Transformer<Integer, Node>() { @Override public Integer transform(final Node dependsOnTask) { return Iterables.indexOf(nodeQueue, new Predicate<NodeInVisitingSegment>() { @Override @SuppressWarnings("NullableProblems") public boolean apply(NodeInVisitingSegment nodeInVisitingSegment) { return nodeInVisitingSegment.node.equals(dependsOnTask); } }); } }); return Collections.max(precedingTaskIndices) + 1; } private Set<Node> getAllPrecedingNodes(Node finalizer) { Set<Node> precedingNodes = Sets.newHashSet(); Deque<Node> candidateNodes = new ArrayDeque<Node>(); // Consider every node that must run before the finalizer Iterables.addAll(candidateNodes, finalizer.getAllSuccessors()); // For each candidate node, add it to the preceding nodes. while (!candidateNodes.isEmpty()) { Node precedingNode = candidateNodes.pop(); if (precedingNodes.add(precedingNode) && precedingNode instanceof TaskNode) { // Any node that the preceding task must run after is also a preceding node. candidateNodes.addAll(((TaskNode) precedingNode).getMustSuccessors()); candidateNodes.addAll(((TaskNode) precedingNode).getFinalizingSuccessors()); } } return precedingNodes; } private void onOrderingCycle(Node successor, Node node) { CachingDirectedGraphWalker<Node, Void> graphWalker = new CachingDirectedGraphWalker<Node, Void>( new DirectedGraph<Node, Void>() { @Override public void getNodeValues(Node node, Collection<? super Void> values, Collection<? super Node> connectedNodes) { connectedNodes.addAll(node.getDependencySuccessors()); if (node instanceof TaskNode) { TaskNode taskNode = (TaskNode) node; connectedNodes.addAll(taskNode.getMustSuccessors()); connectedNodes.addAll(taskNode.getFinalizingSuccessors()); } } }); graphWalker.add(entryTasks); List<Set<Node>> cycles = graphWalker.findCycles(); if (cycles.isEmpty()) { // TODO: This isn't correct. This means that we've detected a cycle while determining the execution plan, but the graph walker did not find one. // https://github.com/gradle/gradle/issues/2293 throw new GradleException("Misdetected cycle between " + node + " and " + successor + ". Help us by reporting this to https://github.com/gradle/gradle/issues/2293"); } final List<Node> firstCycle = new ArrayList<Node>(cycles.get(0)); Collections.sort(firstCycle); DirectedGraphRenderer<Node> graphRenderer = new DirectedGraphRenderer<Node>(new GraphNodeRenderer<Node>() { @Override public void renderTo(Node node, StyledTextOutput output) { output.withStyle(StyledTextOutput.Style.Identifier).text(node); } }, new DirectedGraph<Node, Object>() { @Override public void getNodeValues(Node node, Collection<? super Object> values, Collection<? super Node> connectedNodes) { for (Node dependency : firstCycle) { if (node.hasHardSuccessor(dependency)) { connectedNodes.add(dependency); } } } }); StringWriter writer = new StringWriter(); graphRenderer.renderTo(firstCycle.get(0), writer); throw new CircularReferenceException( String.format("Circular dependency between the following tasks:%n%s", writer.toString())); } public void clear() { taskNodeFactory.clear(); dependencyResolver.clear(); entryTasks.clear(); nodeMapping.clear(); executionQueue.clear(); projectLocks.clear(); failureCollector.clearFailures(); mutations.clear(); canonicalizedFileCache.clear(); reachableCache.clear(); dependenciesCompleteCache.clear(); runningNodes.clear(); } @Override public Set<Task> getTasks() { return nodeMapping.getTasks(); } @Override public Set<Task> getFilteredTasks() { ImmutableSet.Builder<Task> builder = ImmutableSet.builder(); for (Node filteredNode : filteredNodes) { if (filteredNode instanceof LocalTaskNode) { builder.add(((LocalTaskNode) filteredNode).getTask()); } } return builder.build(); } public void useFilter(Spec<? super Task> filter) { this.filter = filter; } public void setContinueOnFailure(boolean continueOnFailre) { this.continueOnFailure = continueOnFailre; } @Override @Nullable public Node selectNext(WorkerLeaseRegistry.WorkerLease workerLease, ResourceLockState resourceLockState) { if (allProjectsLocked()) { return null; } Iterator<Node> iterator = executionQueue.iterator(); while (iterator.hasNext()) { Node node = iterator.next(); if (node.isReady() && allDependenciesComplete(node)) { MutationInfo mutations = getResolvedMutationInfo(node); // TODO: convert output file checks to a resource lock if (!tryLockProjectFor(node) || !workerLease.tryLock() || !canRunWithCurrentlyExecutedNodes(node, mutations)) { resourceLockState.releaseLocks(); continue; } if (node.allDependenciesSuccessful()) { recordNodeStarted(node); node.startExecution(); } else { node.skipExecution(); } iterator.remove(); return node; } } return null; } private boolean tryLockProjectFor(Node node) { if (node instanceof LocalTaskNode) { return getProjectLock((LocalTaskNode) node).tryLock(); } else { return true; } } private void unlockProjectFor(Node node) { if (node instanceof LocalTaskNode) { getProjectLock((LocalTaskNode) node).unlock(); } } private ResourceLock getProjectLock(LocalTaskNode taskNode) { return projectLocks.get(taskNode.getTask().getProject()); } private MutationInfo getResolvedMutationInfo(Node node) { MutationInfo mutations = this.mutations.get(node); if (!mutations.resolved) { resolveMutations(mutations, node); } return mutations; } private void resolveMutations(MutationInfo mutations, Node node) { if (node instanceof LocalTaskNode) { LocalTaskNode taskNode = (LocalTaskNode) node; TaskInternal task = taskNode.getTask(); ProjectInternal project = (ProjectInternal) task.getProject(); ServiceRegistry serviceRegistry = project.getServices(); PathToFileResolver resolver = serviceRegistry.get(PathToFileResolver.class); PropertyWalker propertyWalker = serviceRegistry.get(PropertyWalker.class); TaskProperties taskProperties = DefaultTaskProperties.resolve(propertyWalker, resolver, task); mutations.outputPaths.addAll(getOutputPaths(canonicalizedFileCache, taskNode, taskProperties.getOutputFiles(), taskProperties.getLocalStateFiles())); mutations.destroyablePaths.addAll( getDestroyablePaths(canonicalizedFileCache, taskNode, taskProperties.getDestroyableFiles())); mutations.hasFileInputs = !taskProperties.getInputFileProperties().isEmpty(); mutations.hasOutputs = taskProperties.hasDeclaredOutputs(); mutations.hasLocalState = !taskProperties.getLocalStateFiles().isEmpty(); mutations.resolved = true; if (!mutations.destroyablePaths.isEmpty()) { if (mutations.hasOutputs) { throw new IllegalStateException("Task " + taskNode + " has both outputs and destroyables defined. A task can define either outputs or destroyables, but not both."); } if (mutations.hasFileInputs) { throw new IllegalStateException("Task " + taskNode + " has both inputs and destroyables defined. A task can define either inputs or destroyables, but not both."); } if (mutations.hasLocalState) { throw new IllegalStateException("Task " + taskNode + " has both local state and destroyables defined. A task can define either local state or destroyables, but not both."); } } } } private boolean allDependenciesComplete(Node node) { if (dependenciesCompleteCache.contains(node)) { return true; } boolean dependenciesComplete = node.allDependenciesComplete(); if (dependenciesComplete) { dependenciesCompleteCache.add(node); } return dependenciesComplete; } private boolean allProjectsLocked() { for (ResourceLock lock : projectLocks.values()) { if (!lock.isLocked()) { return false; } } return true; } private ResourceLock getOrCreateProjectLock(Project project) { Path buildPath = ((ProjectInternal) project).getMutationState().getOwner().getIdentityPath(); Path projectPath = ((ProjectInternal) project).getIdentityPath(); return workerLeaseService.getProjectLock(buildPath, projectPath); } private boolean canRunWithCurrentlyExecutedNodes(Node node, MutationInfo mutations) { Set<String> candidateNodeDestroyables = mutations.destroyablePaths; if (!runningNodes.isEmpty()) { Set<String> candidateNodeOutputs = mutations.outputPaths; Set<String> candidateMutations = !candidateNodeOutputs.isEmpty() ? candidateNodeOutputs : candidateNodeDestroyables; if (hasNodeWithOverlappingMutations(candidateMutations)) { return false; } } return !doesDestroyNotYetConsumedOutputOfAnotherNode(node, candidateNodeDestroyables); } private static ImmutableSet<String> canonicalizedPaths(final Map<File, String> cache, Iterable<File> files) { ImmutableSet.Builder<String> builder = ImmutableSet.builder(); for (File file : files) { String path; try { path = cache.get(file); if (path == null) { path = file.getCanonicalPath(); cache.put(file, path); } builder.add(path); } catch (IOException e) { throw new UncheckedIOException(e); } } return builder.build(); } private boolean hasNodeWithOverlappingMutations(Set<String> candidateMutationPaths) { if (!candidateMutationPaths.isEmpty()) { for (Node runningNode : runningNodes) { MutationInfo runningMutations = mutations.get(runningNode); Iterable<String> runningMutationPaths = Iterables.concat(runningMutations.outputPaths, runningMutations.destroyablePaths); if (hasOverlap(candidateMutationPaths, runningMutationPaths)) { return true; } } } return false; } private boolean doesDestroyNotYetConsumedOutputOfAnotherNode(Node destroyer, Set<String> destroyablePaths) { if (!destroyablePaths.isEmpty()) { for (MutationInfo producingNode : mutations.values()) { if (!producingNode.node.isComplete()) { // We don't care about producing nodes that haven't finished yet continue; } if (producingNode.consumingNodes.isEmpty()) { // We don't care about nodes whose output is not consumed by anyone anymore continue; } if (!hasOverlap(destroyablePaths, producingNode.outputPaths)) { // No overlap no cry continue; } for (Node consumer : producingNode.consumingNodes) { if (doesConsumerDependOnDestroyer(consumer, destroyer)) { // If there's an explicit dependency from consuming node to destroyer, // then we accept that as the will of the user continue; } return true; } } } return false; } private boolean doesConsumerDependOnDestroyer(Node consumer, Node destroyer) { if (consumer == destroyer) { return true; } Pair<Node, Node> nodePair = Pair.of(consumer, destroyer); if (reachableCache.get(nodePair) != null) { return reachableCache.get(nodePair); } boolean reachable = false; for (Node dependency : consumer.getAllSuccessors()) { if (!dependency.isComplete()) { if (doesConsumerDependOnDestroyer(dependency, destroyer)) { reachable = true; } } } reachableCache.put(nodePair, reachable); return reachable; } private static boolean hasOverlap(Iterable<String> paths1, Iterable<String> paths2) { for (String path1 : paths1) { for (String path2 : paths2) { String overLappedPath = getOverLappedPath(path1, path2); if (overLappedPath != null) { return true; } } } return false; } private static Set<String> getOutputPaths(Map<File, String> canonicalizedFileCache, TaskNode task, FileCollection outputFiles, FileCollection localStateFiles) { try { return canonicalizedPaths(canonicalizedFileCache, Iterables.concat(outputFiles, localStateFiles)); } catch (ResourceDeadlockException e) { throw new IllegalStateException(deadlockMessage(task, "an output or local state", "outputs"), e); } } private static Set<String> getDestroyablePaths(Map<File, String> canonicalizedFileCache, TaskNode task, FileCollection destroyableFiles) { try { return canonicalizedPaths(canonicalizedFileCache, destroyableFiles); } catch (ResourceDeadlockException e) { throw new IllegalStateException(deadlockMessage(task, "a destroyable", "destroyables"), e); } } private static String deadlockMessage(TaskNode task, String singular, String plural) { return String.format( "A deadlock was detected while resolving the %s for task '%s'. This can be caused, for instance, by %s property causing dependency resolution.", plural, task, singular); } @Nullable private static String getOverLappedPath(String firstPath, String secondPath) { if (firstPath.equals(secondPath)) { return firstPath; } if (firstPath.length() == secondPath.length()) { return null; } String shorter; String longer; if (firstPath.length() > secondPath.length()) { shorter = secondPath; longer = firstPath; } else { shorter = firstPath; longer = secondPath; } boolean isOverlapping = longer.startsWith(shorter) && longer.charAt(shorter.length()) == File.separatorChar; if (isOverlapping) { return shorter; } else { return null; } } private void recordNodeStarted(Node node) { runningNodes.add(node); } private void recordNodeCompleted(Node node) { runningNodes.remove(node); MutationInfo mutations = this.mutations.get(node); for (Node producer : mutations.producingNodes) { MutationInfo producerMutations = this.mutations.get(producer); if (producerMutations.consumingNodes.remove(node) && canRemoveMutation(producerMutations)) { this.mutations.remove(producer); } } if (canRemoveMutation(mutations)) { this.mutations.remove(node); } } private static boolean canRemoveMutation(@Nullable MutationInfo mutations) { return mutations != null && mutations.node.isComplete() && mutations.consumingNodes.isEmpty(); } @Override public void nodeComplete(Node node) { try { if (!node.isComplete()) { enforceFinalizers(node); if (node.isFailed()) { handleFailure(node); } node.finishExecution(); recordNodeCompleted(node); } } finally { unlockProjectFor(node); } } private static void enforceFinalizers(Node node) { if (!(node instanceof TaskNode)) { return; } for (Node finalizerNode : ((TaskNode) node).getFinalizers()) { if (finalizerNode.isRequired() || finalizerNode.isMustNotRun()) { enforceWithDependencies(finalizerNode, Sets.<Node>newHashSet()); } } } private static void enforceWithDependencies(Node nodeInfo, Set<Node> enforcedNodes) { Deque<Node> candidateNodes = new ArrayDeque<Node>(); candidateNodes.add(nodeInfo); while (!candidateNodes.isEmpty()) { Node node = candidateNodes.pop(); if (!enforcedNodes.contains(node)) { enforcedNodes.add(node); candidateNodes.addAll(node.getDependencySuccessors()); if (node.isMustNotRun() || node.isRequired()) { node.enforceRun(); } } } } @Override public void abortAllAndFail(Throwable t) { abortExecution(true); this.failureCollector.addFailure(t); } private void handleFailure(Node node) { Throwable executionFailure = node.getExecutionFailure(); if (executionFailure != null) { // Always abort execution for an execution failure (as opposed to a node failure) abortExecution(); this.failureCollector.addFailure(executionFailure); return; } // Failure try { if (!continueOnFailure) { node.rethrowNodeFailure(); } this.failureCollector.addFailure(node.getNodeFailure()); } catch (Exception e) { // If the failure handler rethrows exception, then execution of other nodes is aborted. (--continue will collect failures) abortExecution(); this.failureCollector.addFailure(e); } } private boolean abortExecution() { return abortExecution(false); } @Override public void cancelExecution() { buildCancelled = abortExecution() || buildCancelled; } private boolean abortExecution(boolean abortAll) { boolean aborted = false; for (Node node : nodeMapping) { // Allow currently executing and enforced tasks to complete, but skip everything else. if (node.isRequired()) { node.skipExecution(); aborted = true; } // If abortAll is set, also stop enforced tasks. if (abortAll && node.isReady()) { node.abortExecution(); aborted = true; } } return aborted; } @Override public void collectFailures(Collection<? super Throwable> failures) { if (buildCancelled) { failures.add(new BuildCancelledException()); } failures.addAll(failureCollector.getFailures()); } @Override public boolean allNodesComplete() { for (Node node : nodeMapping) { if (!node.isComplete()) { return false; } } // TODO:lptr why don't we check runningNodes here like we do in hasNodesRemaining()? return true; } @Override public boolean hasNodesRemaining() { for (Node node : executionQueue) { if (!node.isComplete()) { return true; } } return !runningNodes.isEmpty(); } @Override public int size() { return nodeMapping.nodes.size(); } private static class GraphEdge { private final Node from; private final Node to; private GraphEdge(Node from, Node to) { this.from = from; this.to = to; } } private static class NodeInVisitingSegment { private final Node node; private final int visitingSegment; private NodeInVisitingSegment(Node node, int visitingSegment) { this.node = node; this.visitingSegment = visitingSegment; } } private static class MutationInfo { final Node node; final Set<Node> consumingNodes = Sets.newHashSet(); final Set<Node> producingNodes = Sets.newHashSet(); final Set<String> outputPaths = Sets.newHashSet(); final Set<String> destroyablePaths = Sets.newHashSet(); boolean hasFileInputs; boolean hasOutputs; boolean hasLocalState; boolean resolved; MutationInfo(Node node) { this.node = node; } } private static class NodeMapping extends AbstractCollection<Node> { private final Map<Task, LocalTaskNode> taskMapping = Maps.newLinkedHashMap(); private final Set<Node> nodes = Sets.newLinkedHashSet(); @Override public boolean contains(Object o) { return nodes.contains(o); } @Override public boolean add(Node node) { if (!nodes.add(node)) { return false; } if (node instanceof LocalTaskNode) { LocalTaskNode taskNode = (LocalTaskNode) node; taskMapping.put(taskNode.getTask(), taskNode); } return true; } public TaskNode get(Task task) { TaskNode taskNode = taskMapping.get(task); if (taskNode == null) { throw new IllegalStateException( "Task is not part of the execution plan, no dependency information is available."); } return taskNode; } public Set<Task> getTasks() { return taskMapping.keySet(); } @Override public Iterator<Node> iterator() { return nodes.iterator(); } @Override public void clear() { nodes.clear(); taskMapping.clear(); } @Override public int size() { return nodes.size(); } public void retainFirst(int count) { Iterator<Node> executionPlanIterator = nodes.iterator(); for (int i = 0; i < count; i++) { executionPlanIterator.next(); } while (executionPlanIterator.hasNext()) { Node removedNode = executionPlanIterator.next(); executionPlanIterator.remove(); if (removedNode instanceof LocalTaskNode) { taskMapping.remove(((LocalTaskNode) removedNode).getTask()); } } } } }