[JENKINS-20280] Use topological sorting to decide the order to override variables.

This commit is contained in:
ikedam committed 2013-11-01 23:55:29 +09:00
1 parent 562c64bbdb
commit 6d56de71cb
2 files changed
+106 -120

No files matched your search

+99 -118
View File
@@ -26,6 +26,8 @@ package hudson;
import hudson.remoting.Callable;
import hudson.remoting.VirtualChannel;
import hudson.util.CaseInsensitiveComparator;
import hudson.util.CyclicGraphDetector;
import hudson.util.CyclicGraphDetector.CycleDetectedException;
import hudson.util.VariableResolver;
import java.io.File;
@@ -40,6 +42,7 @@ import java.util.TreeMap;
import java.util.Arrays;
import java.util.TreeSet;
import java.util.UUID;
import java.util.logging.Logger;
/**
* Environment variables.
@@ -69,6 +72,7 @@ import java.util.UUID;
* @author Kohsuke Kawaguchi
*/
public class EnvVars extends TreeMap<String,String> {
private static Logger LOGGER = Logger.getLogger(EnvVars.class.getName());
/**
* If this {@link EnvVars} object represents the whole environment variable set,
* not just a partial list used for overriding later, then we need to know
@@ -158,21 +162,8 @@ public class EnvVars extends TreeMap<String,String> {
/**
* Calculates the order to override variables.
*
* We should override variables in a following order:
* <ol>
* <li>variables that does not contain variable expressions.</li>
* <li>variables that refers variables overridden in 1.</li>
* <li>variables that refers variables overridden in 2.</li>
* <li>...</li>
* <li>(last) variables contains '+' (as PATH+MAVEN)</li>
* </ol>
* Sort variables with topological sort with their reference graph.
*
* This class orders variables in a following way:
* <ol>
* <li>scan each overriding variables and list all referred variables (includes indirect references).</li>
* <li>sort variables with a number of referring variables (ascending order).</li>
* </ol>
*
* This is package accessible for testing purpose.
*/
static class OverrideOrderCalculator {
@@ -198,143 +189,133 @@ public class EnvVars extends TreeMap<String,String> {
}
}
private static class VariableReferenceSorter extends CyclicGraphDetector<String> {
// map from a variable to a set of variables that variable refers.
private final Map<String, Set<String>> refereeSetMap;
public VariableReferenceSorter(Map<String, Set<String>> refereeSetMap) {
this.refereeSetMap = refereeSetMap;
}
@Override
protected Iterable<? extends String> getEdges(String n) {
// return variables referred from the variable.
if (!refereeSetMap.containsKey(n)) {
// there is a case a non-existing variable is referred...
return Collections.emptySet();
}
return refereeSetMap.get(n);
}
};
private final Comparator<? super String> comparator;
private final EnvVars target;
private final Map<String,String> overrides;
/**
* set of variables that a variable is REFERRING.
* When A=${B}, refereeSetMap.get("A").contains("B").
* Also contains indirect references, when A=${B}, B=${C},
* refereeSetMap.get("A").contains("C").
*/
private Map<String, Set<String>> refereeSetMap;
/**
* set of variables that a variable is REFERRED BY.
* When A=${B}, referrerSetMap.get("B").contains("A").
* Also contains indirect references, when A=${B}, B=${C},
* referrerSetMap.get("C").contains("A").
*/
private Map<String, Set<String>> referrerSetMap;
private Map<String, Set<String>> refereeSetMap;
private List<String> orderedVariableNames;
public OverrideOrderCalculator(EnvVars target, Map<String,String> overrides) {
comparator = target.comparator();
this.target = target;
this.overrides = overrides;
refereeSetMap = new TreeMap<String, Set<String>>(comparator);
referrerSetMap = new TreeMap<String, Set<String>>(comparator);
scan();
}
private int getRefereeNum(String variable) {
if (refereeSetMap.containsKey(variable)) {
return refereeSetMap.get(variable).size();
}
return 0;
public List<String> getOrderedVariableNames() {
return orderedVariableNames;
}
public List<String> getOrderedVariableNames() {
List<String> nameList = new ArrayList<String>(overrides.keySet());
Collections.sort(nameList, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
if (o1.indexOf('+') > -1) {
if (o2.indexOf('+') > -1) {
// ABC+FOO == XYZ+BAR
return 0;
}
// ABC+FOO > BAR
return 1;
}
if (o2.indexOf('+') > -1) {
// FOO < ABC+BAR
return -1;
}
// depends on the number of variables each variable refers.
return getRefereeNum(o1) - getRefereeNum(o2);
// Cut the reference to the variable in a cycle.
private void cutCycleAt(String referee, List<String> cycle) {
// cycle contains variables in referrer-to-referee order.
// This should not be negative, for the first and last one is same.
int refererIndex = cycle.lastIndexOf(referee) - 1;
assert(refererIndex >= 0);
String referrer = cycle.get(refererIndex);
boolean removed = refereeSetMap.get(referrer).remove(referee);
assert(removed);
LOGGER.warning(String.format("Cyclic reference detected: %s", Util.join(cycle," -> ")));
LOGGER.warning(String.format("Cut the reference %s -> %s", referrer, referee));
}
// Cut the variable reference in a cycle.
private void cutCycle(List<String> cycle) {
// if an existing variable is contained in that cycle,
// cut the cycle with that variable:
// existing:
// PATH=/usr/bin
// overriding:
// PATH1=/usr/local/bin:${PATH}
// PATH=/opt/something/bin:${PATH1}
// then consider reference PATH1 -> PATH can be ignored.
for (String referee: cycle) {
if (target.containsKey(referee)) {
cutCycleAt(referee, cycle);
return;
}
});
return nameList;
}
// if not, cut the reference to the first one.
cutCycleAt(cycle.get(0), cycle);
}
/**
* Scan all variables and list all referring variables.
*/
public void scan() {
refereeSetMap = new TreeMap<String, Set<String>>(comparator);
List<String> extendingVariableNames = new ArrayList<String>();
TraceResolver resolver = new TraceResolver(comparator);
for (String currentVar: overrides.keySet()) {
if (currentVar.indexOf('+') > 0) {
for (Map.Entry<String, String> entry: overrides.entrySet()) {
if (entry.getKey().indexOf('+') > 0) {
// XYZ+AAA variables should be always processed in last.
extendingVariableNames.add(entry.getKey());
continue;
}
resolver.clear();
Util.replaceMacro(overrides.get(currentVar), resolver);
Util.replaceMacro(entry.getValue(), resolver);
// Variables directly referred from the current scanning variable.
Set<String> refereeSet = resolver.referredVariables;
if (refereeSet.isEmpty()) {
// nothing to do if this variables does not refer other variables.
// Ignore self reference.
refereeSet.remove(entry.getKey());
refereeSetMap.put(entry.getKey(), refereeSet);
}
VariableReferenceSorter sorter;
while(true) {
sorter = new VariableReferenceSorter(refereeSetMap);
try {
sorter.run(refereeSetMap.keySet());
} catch(CycleDetectedException e) {
// cyclic reference found.
// cut the cycle and retry.
@SuppressWarnings("unchecked")
List<String> cycle = e.cycle;
cutCycle(cycle);
continue;
}
// Find indirect referred variables:
// A=${B}
// CurrentVar=${A}
// -> CurrentVar refers B.
Set<String> indirectRefereeSet = new TreeSet<String>(comparator);
for (String referee: refereeSet) {
if (refereeSetMap.containsKey(referee)) {
indirectRefereeSet.addAll(refereeSetMap.get(referee));
}
}
// now contains variables referred both directly and indirectly.
refereeSet.addAll(indirectRefereeSet);
// Variables refers the current scanning variable.
// this contains both direct and indirect reference.
Set<String> referrerSet = referrerSetMap.get(currentVar);
// what I have to do:
// 1. Create a link between the current scanning variable and referred variables.
// 1-a. register the current variable as a referrer of referred variables.
// 1-b. register referred variables as a referee of the current variable.
// 2. Create links between referring variables and referred variables.
// 2-a. register referring variables as referrers of referred variables.
// 2-b. register referred variables as referees of referring variables.
//
// Links between referring variables and the current scanning variable
// is already created from referring variables.
for (String referee: refereeSet) {
if (!referrerSetMap.containsKey(referee)) {
referrerSetMap.put(referee, new TreeSet<String>(comparator));
}
// 1-a. register the current variable as a referrer of referred variables.
referrerSetMap.get(referee).add(currentVar);
// 2-b. register referred variables as referees of referring variables.
if (referrerSet != null) {
referrerSetMap.get(referee).addAll(referrerSet);
}
}
if (!refereeSetMap.containsKey(currentVar)) {
refereeSetMap.put(currentVar, new TreeSet<String>(comparator));
}
// 1-b. register referred variables as a referee of the current variable.
refereeSetMap.get(currentVar).addAll(refereeSet);
if (referrerSet != null) {
for (String referer: referrerSet) {
// 2-b. register referred variables as referees of referring variables.
// For referrer refers the current scanning variable,
// refereeSetMap.get(referer) always exists.
refereeSetMap.get(referer).addAll(refereeSet);
}
break;
}
// When A refers B, the last appearance of B always comes after
// the last appearance of A.
List<String> reversedDuplicatedOrder = new ArrayList<String>(sorter.getSorted());
Collections.reverse(reversedDuplicatedOrder);
orderedVariableNames = new ArrayList<String>(overrides.size());
for(String key: reversedDuplicatedOrder) {
if(overrides.containsKey(key) && !orderedVariableNames.contains(key)) {
orderedVariableNames.add(key);
}
}
Collections.reverse(orderedVariableNames);
orderedVariableNames.addAll(extendingVariableNames);
}
}