All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.gradle.execution.plan.DetermineExecutionPlanAction Maven / Gradle / Ivy

There is a newer version: 8.11.1
Show newest version
/*
 * Copyright 2022 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.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import org.gradle.api.CircularReferenceException;
import org.gradle.api.GradleException;
import org.gradle.api.internal.TaskInternal;
import org.gradle.api.internal.project.ProjectInternal;
import org.gradle.api.internal.tasks.TaskDestroyablesInternal;
import org.gradle.api.internal.tasks.TaskLocalStateInternal;
import org.gradle.internal.graph.CachingDirectedGraphWalker;
import org.gradle.internal.graph.DirectedGraphRenderer;
import org.gradle.internal.logging.text.StyledTextOutput;
import org.gradle.internal.properties.OutputFilePropertyType;
import org.gradle.internal.properties.PropertyValue;
import org.gradle.internal.properties.PropertyVisitor;
import org.gradle.internal.properties.bean.PropertyWalker;
import org.gradle.internal.reflect.validation.TypeValidationContext;

import java.io.StringWriter;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;

import static java.lang.String.format;
import static org.gradle.execution.plan.NodeSets.sortedListOf;

/**
 * Determines the execution plan, checking for cycles and making sure `finalizedBy` constraints are honored.
 *
 * The final plan is communicated via the mutation of the given `nodeMapping`, `ordinalNodeAccess` and `finalizers` collections.
 * `entryNodes` is not changed by this class.
 *
 * 

Note about finalizers

* A dependency of a finalizer must not run until it is known to be needed by something else that should run. * So if the dependency is only required by a finalizer, then it should not start until the finalizer is ready to start * (ie the finalized task has completed). But if the dependency is also required by some node reachable from the * command-line tasks, then it should start as soon as its dependencies are ready. * Or if the dependency is required by multiple finalizers, then it should not start until one of those finalizer * is ready to start. */ class DetermineExecutionPlanAction { private final DefaultExecutionPlan.NodeMapping nodeMapping; private final OrdinalNodeAccess ordinalNodeAccess; private final Set entryNodes; private final Set finalizers; private final LinkedList nodeQueue = new LinkedList<>(); private final HashMultimap visitingNodes = HashMultimap.create(); private final Deque walkedShouldRunAfterEdges = new ArrayDeque<>(); private final Deque path = new ArrayDeque<>(); private final Map planBeforeVisiting = new HashMap<>(); private int visitingSegmentCounter = 0; /** * See {@link DetermineExecutionPlanAction} */ public DetermineExecutionPlanAction(DefaultExecutionPlan.NodeMapping nodeMapping, OrdinalNodeAccess ordinalNodeAccess, Set entryNodes, Set finalizers) { this.entryNodes = entryNodes; this.nodeMapping = nodeMapping; this.ordinalNodeAccess = ordinalNodeAccess; this.finalizers = finalizers; } public ImmutableList run() { updateFinalizerGroups(); processEntryNodes(); processNodeQueue(); return createOrdinalRelationshipsAndCollectNodes(); } /** * Create the finalizer group for each finalizer and propagate these groups down to all dependencies. */ private void updateFinalizerGroups() { if (finalizers.isEmpty()) { return; } // Collect the finalizers and their dependencies so that each node is ordered before all of its dependencies LinkedList nodes = new LinkedList<>(); Set visiting = new HashSet<>(); Set visited = new HashSet<>(); Deque queue = new ArrayDeque<>(finalizers); while (!queue.isEmpty()) { Node node = queue.peek(); if (node.isCannotRunInAnyPlan() || visited.contains(node)) { // Already visited node or node cannot execute (eg has already executed), skip queue.remove(); } else if (visiting.add(node)) { // Haven't seen this node for (Node successor : node.getDependencySuccessors()) { queue.addFirst(successor); } } else { // Have visited the dependencies of this node, add it to the start of the list (so that it is earlier in the list that // all of its dependencies) visiting.remove(node); visited.add(node); nodes.addFirst(node); } } for (Node node : nodes) { node.maybeInheritFinalizerGroups(); } } private void processEntryNodes() { for (Node node : entryNodes) { nodeQueue.add(new NodeInVisitingSegment(node, visitingSegmentCounter++)); } } private void processNodeQueue() { while (!nodeQueue.isEmpty()) { final NodeInVisitingSegment nodeInVisitingSegment = nodeQueue.peekFirst(); final int currentSegment = nodeInVisitingSegment.visitingSegment; final Node node = nodeInVisitingSegment.node; if (node.isDoNotIncludeInPlan() || nodeMapping.contains(node)) { // Discard the node because it has already been visited or should not be included, for example: // - it has already executed in another execution plan // - it is reachable only via a must-run-after or should-run-after edge // - it is filtered nodeQueue.removeFirst(); visitingNodes.remove(node, currentSegment); maybeRemoveProcessedShouldRunAfterEdge(node); continue; } if (visitingNodes.put(node, currentSegment)) { // Have not seen this node before - add its dependencies to the head of the queue and leave this // node in the queue if (node instanceof TaskNode) { TaskNode taskNode = (TaskNode) node; recordEdgeIfArrivedViaShouldRunAfter(path, taskNode); removeShouldRunAfterSuccessorsIfTheyImposeACycle(taskNode, nodeInVisitingSegment.visitingSegment); takePlanSnapshotIfCanBeRestoredToCurrentTask(planBeforeVisiting, taskNode); } // Add any finalizers to the queue just after the current node for (Node finalizer : node.getFinalizers()) { addFinalizerToQueue(visitingSegmentCounter++, finalizer); } ListIterator insertPoint = nodeQueue.listIterator(); for (Node successor : node.getAllSuccessors()) { 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(toBeRemoved); restoreExecutionPlan(planBeforeVisiting, toBeRemoved); break; } else { onOrderingCycle(successor, node); } } insertPoint.add(new NodeInVisitingSegment(successor, currentSegment)); } path.push(node); } else { // Have visited this node's dependencies - add it to the end of the plan nodeQueue.removeFirst(); maybeRemoveProcessedShouldRunAfterEdge(node); visitingNodes.remove(node, currentSegment); path.pop(); nodeMapping.add(node); } } } private ImmutableList createOrdinalRelationshipsAndCollectNodes() { ImmutableList.Builder scheduledNodes = ImmutableList.builderWithExpectedSize(nodeMapping.size()); for (Node node : nodeMapping) { node.maybeUpdateOrdinalGroup(); createOrdinalRelationships(node, scheduledNodes); scheduledNodes.add(node); } nodeMapping.addAll(ordinalNodeAccess.getAllNodes()); return scheduledNodes.build(); } private void addFinalizerToQueue(int visitingSegmentCounter, Node finalizer) { int insertPosition = 1; int pos = 0; for (NodeInVisitingSegment segment : nodeQueue) { if (segment.node == finalizer) { // Already later in the queue return; } // Need to insert the finalizer immediately after the last node that it finalizes if (finalizer.getFinalizingSuccessors().contains(segment.node) && pos > insertPosition) { insertPosition = pos; } pos++; } nodeQueue.add(insertPosition, new NodeInVisitingSegment(finalizer, visitingSegmentCounter)); } private void maybeRemoveProcessedShouldRunAfterEdge(Node node) { GraphEdge edge = walkedShouldRunAfterEdges.peek(); if (edge != null && edge.to.equals(node)) { walkedShouldRunAfterEdges.pop(); } } private void restoreExecutionPlan(Map planBeforeVisiting, GraphEdge toBeRemoved) { int count = planBeforeVisiting.get(toBeRemoved.from); nodeMapping.retainFirst(count); } private void restoreQueue(GraphEdge toBeRemoved) { NodeInVisitingSegment nextInQueue = null; while (nextInQueue == null || !toBeRemoved.from.equals(nextInQueue.node)) { nextInQueue = nodeQueue.peekFirst(); visitingNodes.remove(nextInQueue.node, nextInQueue.visitingSegment); if (!toBeRemoved.from.equals(nextInQueue.node)) { nodeQueue.removeFirst(); } } } private void restorePath(Deque path, GraphEdge toBeRemoved) { Node removedFromPath = null; while (!toBeRemoved.from.equals(removedFromPath)) { removedFromPath = path.pop(); } } private void removeShouldRunAfterSuccessorsIfTheyImposeACycle(TaskNode node, int visitingSegment) { Iterables.removeIf( node.getShouldSuccessors(), input -> visitingNodes.containsEntry(input, visitingSegment) ); } private void takePlanSnapshotIfCanBeRestoredToCurrentTask(Map planBeforeVisiting, TaskNode node) { if (!node.getShouldSuccessors().isEmpty()) { planBeforeVisiting.put(node, nodeMapping.size()); } } private void recordEdgeIfArrivedViaShouldRunAfter(Deque path, TaskNode node) { Node previous = path.peek(); if (!(previous instanceof TaskNode)) { return; } TaskNode previousTaskNode = (TaskNode) previous; if (previousTaskNode.getShouldSuccessors().contains(node)) { walkedShouldRunAfterEdges.push(new GraphEdge(previous, node)); } } private void onOrderingCycle(Node successor, Node currentNode) { List> cycles = findCycles(successor); 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 " + currentNode + " and " + successor + ". Help us by reporting this to https://github.com/gradle/gradle/issues/2293"); } StringWriter cycleString = renderOrderingCycle(cycles.get(0)); throw new CircularReferenceException(format("Circular dependency between the following tasks:%n%s", cycleString)); } private List> findCycles(Node successor) { CachingDirectedGraphWalker graphWalker = new CachingDirectedGraphWalker<>((node, values, connectedNodes) -> { node.getHardSuccessors().forEach(connectedNodes::add); }); graphWalker.add(successor); return graphWalker.findCycles(); } private StringWriter renderOrderingCycle(Set nodes) { List cycle = sortedListOf(nodes); DirectedGraphRenderer graphRenderer = new DirectedGraphRenderer<>( (it, output) -> output.withStyle(StyledTextOutput.Style.Identifier).text(it), (it, values, connectedNodes) -> { for (Node dependency : cycle) { Set successors = Sets.newHashSet(it.getHardSuccessors()); if (dependency instanceof TaskNode && successors.contains(dependency)) { connectedNodes.add(dependency); } } }); StringWriter writer = new StringWriter(); graphRenderer.renderTo(cycle.get(0), writer); return writer; } private void createOrdinalRelationships(Node node, ImmutableList.Builder scheduleBuilder) { if (!(node instanceof LocalTaskNode)) { return; } OrdinalGroup ordinal = node.getOrdinal(); if (ordinal == null) { return; } LocalTaskNode taskNode = (LocalTaskNode) node; TaskClassifier taskClassifier = classifyTask(taskNode); if (taskClassifier.isDestroyer()) { ordinalNodeAccess.addDestroyerNode(ordinal, taskNode, scheduleBuilder::add); } else if (taskClassifier.isProducer()) { ordinalNodeAccess.addProducerNode(ordinal, taskNode, scheduleBuilder::add); } } /** * Walk the properties of the task to determine if it is a destroyer or a producer (or neither). */ private TaskClassifier classifyTask(LocalTaskNode taskNode) { TaskClassifier taskClassifier = new TaskClassifier(); TaskInternal task = taskNode.getTask(); PropertyWalker propertyWalker = propertyWalkerOf(task); propertyWalker.visitProperties(task, TypeValidationContext.NOOP, taskClassifier); task.getOutputs().visitRegisteredProperties(taskClassifier); if (taskClassifier.isDestroyer()) { // avoid walking further properties after discovering the task is destroyer return taskClassifier; } ((TaskDestroyablesInternal) task.getDestroyables()).visitRegisteredProperties(taskClassifier); if (taskClassifier.isDestroyer()) { // avoid walking further properties after discovering the task is destroyer return taskClassifier; } ((TaskLocalStateInternal) task.getLocalState()).visitRegisteredProperties(taskClassifier); return taskClassifier; } private PropertyWalker propertyWalkerOf(TaskInternal task) { return ((ProjectInternal) task.getProject()).getServices().get(PropertyWalker.class); } private static class TaskClassifier implements PropertyVisitor { private boolean isProducer; private boolean isDestroyer; @Override public void visitOutputFileProperty(String propertyName, boolean optional, PropertyValue value, OutputFilePropertyType filePropertyType) { isProducer = true; } @Override public void visitDestroyableProperty(Object value) { isDestroyer = true; } @Override public void visitLocalStateProperty(Object value) { isProducer = true; } public boolean isProducer() { return isProducer; } public boolean isDestroyer() { return isDestroyer; } } private static class NodeInVisitingSegment { private final Node node; private final int visitingSegment; private NodeInVisitingSegment(Node node, int visitingSegment) { this.node = node; this.visitingSegment = visitingSegment; } @Override public String toString() { return "NodeInVisitingSegment{" + "node=" + node + ", visitingSegment=" + visitingSegment + '}'; } } private static class GraphEdge { private final Node from; private final Node to; private GraphEdge(Node from, Node to) { this.from = from; this.to = to; } @Override public String toString() { return "GraphEdge{" + "from=" + from + ", to=" + to + '}'; } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy