package hudson.matrix;
import groovy.lang.GroovyRuntimeException;
import hudson.AbortException;
import hudson.Extension;
import hudson.console.ModelHyperlinkNote;
import hudson.matrix.MatrixBuild.MatrixBuildExecution;
import hudson.matrix.listeners.MatrixBuildListener;
import hudson.model.Action;
import hudson.model.BuildListener;
import hudson.model.Cause.UpstreamCause;
import hudson.model.ParametersAction;
import hudson.model.Queue;
import hudson.model.ResourceController;
import hudson.model.Result;
import hudson.model.Run;
import hudson.model.queue.CauseOfBlockage;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.TreeSet;
import javax.annotation.Nullable;
import org.kohsuke.stapler.DataBoundConstructor;
/**
* {@link MatrixExecutionStrategy} that captures historical behavior.
*
* <p>
* This class is somewhat complex because historically this wasn't an extension point and so
* people tried to put various logics that cover different use cases into one place.
* Going forward, people are encouraged to create subtypes to implement a custom logic that suits their needs.
*
* @author Kohsuke Kawaguchi
* @since 1.456
*/
public class DefaultMatrixExecutionStrategyImpl extends MatrixExecutionStrategy {
private volatile boolean runSequentially;
/**
* Filter to select a number of combinations to build first
*/
private volatile String touchStoneCombinationFilter;
/**
* Required result on the touchstone combinations, in order to
* continue with the rest
*/
private volatile Result touchStoneResultCondition;
private volatile MatrixConfigurationSorter sorter;
@DataBoundConstructor
public DefaultMatrixExecutionStrategyImpl(Boolean runSequentially, boolean hasTouchStoneCombinationFilter, String touchStoneCombinationFilter, Result touchStoneResultCondition, MatrixConfigurationSorter sorter) {
this(runSequentially!=null ? runSequentially : false,
hasTouchStoneCombinationFilter ? touchStoneCombinationFilter : null,
hasTouchStoneCombinationFilter ? touchStoneResultCondition : null,
sorter);
}
public DefaultMatrixExecutionStrategyImpl(boolean runSequentially, String touchStoneCombinationFilter, Result touchStoneResultCondition, MatrixConfigurationSorter sorter) {
this.runSequentially = runSequentially;
this.touchStoneCombinationFilter = touchStoneCombinationFilter;
this.touchStoneResultCondition = touchStoneResultCondition;
this.sorter = sorter;
}
public DefaultMatrixExecutionStrategyImpl() {
this(false,false,null,null,null);
}
public boolean getHasTouchStoneCombinationFilter() {
return touchStoneCombinationFilter!=null;
}
/**
* If true, {@link MatrixRun}s are run sequentially, instead of running in parallel.
*
* TODO: this should be subsumed by {@link ResourceController}.
*/
public boolean isRunSequentially() {
return runSequentially;
}
public void setRunSequentially(boolean runSequentially) {
this.runSequentially = runSequentially;
}
public String getTouchStoneCombinationFilter() {
return touchStoneCombinationFilter;
}
public void setTouchStoneCombinationFilter(String touchStoneCombinationFilter) {
this.touchStoneCombinationFilter = touchStoneCombinationFilter;
}
public Result getTouchStoneResultCondition() {
return touchStoneResultCondition;
}
public void setTouchStoneResultCondition(Result touchStoneResultCondition) {
this.touchStoneResultCondition = touchStoneResultCondition;
}
public MatrixConfigurationSorter getSorter() {
return sorter;
}
public void setSorter(MatrixConfigurationSorter sorter) {
this.sorter = sorter;
}
@Override
public Result run(MatrixBuildExecution execution) throws InterruptedException, IOException {
Collection<MatrixConfiguration> touchStoneConfigurations = new HashSet<MatrixConfiguration>();
Collection<MatrixConfiguration> delayedConfigurations = new HashSet<MatrixConfiguration>();
filterConfigurations(
execution,
touchStoneConfigurations,
delayedConfigurations
);
if (notifyStartBuild(execution.getAggregators())) return Result.FAILURE;
if (sorter != null) {
touchStoneConfigurations = createTreeSet(touchStoneConfigurations, sorter);
delayedConfigurations = createTreeSet(delayedConfigurations, sorter);
}
if(!runSequentially)
for(MatrixConfiguration c : touchStoneConfigurations)
scheduleConfigurationBuild(execution, c);
PrintStream logger = execution.getListener().getLogger();
Result r = Result.SUCCESS;
for (MatrixConfiguration c : touchStoneConfigurations) {
if(runSequentially)
scheduleConfigurationBuild(execution, c);
MatrixRun run = waitForCompletion(execution, c);
notifyEndBuild(run,execution.getAggregators());
logger.println(Messages.MatrixBuild_Completed(ModelHyperlinkNote.encodeTo(c), getResult(run)));
r = r.combine(getResult(run));
}
if (touchStoneResultCondition != null && r.isWorseThan(touchStoneResultCondition)) {
logger.printf("Touchstone configurations resulted in %s, so aborting...%n", r);
return r;
}
if(!runSequentially)
for(MatrixConfiguration c : delayedConfigurations)
scheduleConfigurationBuild(execution, c);
for (MatrixConfiguration c : delayedConfigurations) {
if(runSequentially)
scheduleConfigurationBuild(execution, c);
MatrixRun run = waitForCompletion(execution, c);
notifyEndBuild(run,execution.getAggregators());
logger.println(Messages.MatrixBuild_Completed(ModelHyperlinkNote.encodeTo(c), getResult(run)));
r = r.combine(getResult(run));
}
return r;
}
private void filterConfigurations(
final MatrixBuildExecution execution,
final Collection<MatrixConfiguration> touchStoneConfigurations,
final Collection<MatrixConfiguration> delayedConfigurations
) throws AbortException {
final MatrixBuild build = execution.getBuild();
final FilterScript combinationFilter = FilterScript.parse(execution.getProject().getCombinationFilter(), FilterScript.ACCEPT_ALL);
final FilterScript touchStoneFilter = FilterScript.parse(getTouchStoneCombinationFilter(), FilterScript.REJECT_ALL);
try {
for (MatrixConfiguration c: execution.getActiveConfigurations()) {
if (!MatrixBuildListener.buildConfiguration(build, c)) continue; // skip rebuild
final Combination combination = c.getCombination();
if (touchStoneFilter != null && touchStoneFilter.apply(execution, combination)) {
touchStoneConfigurations.add(c);
} else if (combinationFilter.apply(execution, combination)) {
delayedConfigurations.add(c);
}
}
} catch (GroovyRuntimeException ex) {
PrintStream logger = execution.getListener().getLogger();
logger.println(ex.getMessage());
ex.printStackTrace(logger);
throw new AbortException("Failed executing combination filter");
}
}
private Result getResult(@Nullable MatrixRun run) {
// null indicates that the run was cancelled before it even gets going
return run!=null ? run.getResult() : Result.ABORTED;
}
private boolean notifyStartBuild(List<MatrixAggregator> aggregators) throws InterruptedException, IOException {
for (MatrixAggregator a : aggregators)
if(!a.startBuild())
return true;
return false;
}
private void notifyEndBuild(MatrixRun b, List<MatrixAggregator> aggregators) throws InterruptedException, IOException {
if (b==null) return; // can happen if the configuration run gets cancelled before it gets started.
for (MatrixAggregator a : aggregators)
if(!a.endRun(b))
throw new AbortException();
}
private <T> TreeSet<T> createTreeSet(Collection<T> items, Comparator<T> sorter) {
TreeSet<T> r = new TreeSet<T>(sorter);
r.addAll(items);
return r;
}
/** Function to start schedule a single configuration
*
* This function schedule a build of a configuration passing all of the Matrixchild actions
* that are present in the parent build.
*
* @param exec Matrix build that is the parent of the configuration
* @param c Configuration to schedule
*/
private void scheduleConfigurationBuild(MatrixBuildExecution exec, MatrixConfiguration c) {
MatrixBuild build = exec.getBuild();
exec.getListener().getLogger().println(Messages.MatrixBuild_Triggering(ModelHyperlinkNote.encodeTo(c)));
// filter the parent actions for those that can be passed to the individual jobs.
List<Action> childActions = new ArrayList<Action>(build.getActions(MatrixChildAction.class));
childActions.addAll(build.getActions(ParametersAction.class)); // used to implement MatrixChildAction
c.scheduleBuild(childActions, new UpstreamCause((Run)build));
}
private MatrixRun waitForCompletion(MatrixBuildExecution exec, MatrixConfiguration c) throws InterruptedException, IOException {
BuildListener listener = exec.getListener();
String whyInQueue = "";
long startTime = System.currentTimeMillis();
// wait for the completion
int appearsCancelledCount = 0;
while(true) {
MatrixRun b = c.getBuildByNumber(exec.getBuild().getNumber());
// two ways to get beyond this. one is that the build starts and gets done,
// or the build gets cancelled before it even started.
if(b!=null && !b.isBuilding()) {
Result buildResult = b.getResult();
if(buildResult!=null)
return b;
}
Queue.Item qi = c.getQueueItem();
if(b==null && qi==null)
appearsCancelledCount++;
else
appearsCancelledCount = 0;
if(appearsCancelledCount>=5) {
// there's conceivably a race condition in computating b and qi, as their computation
// are not synchronized. There are indeed several reports of Hudson incorrectly assuming
// builds being cancelled. See
// http://www.nabble.com/Master-slave-problem-tt14710987.html and also
// http://www.nabble.com/Anyone-using-AccuRev-plugin--tt21634577.html#a21671389
// because of this, we really make sure that the build is cancelled by doing this 5
// times over 5 seconds
listener.getLogger().println(Messages.MatrixBuild_AppearsCancelled(ModelHyperlinkNote.encodeTo(c)));
return null;
}
if(qi!=null) {
// if the build seems to be stuck in the queue, display why
String why = qi.getWhy();
if(why != null && !why.equals(whyInQueue) && System.currentTimeMillis()-startTime>5000) {
// fix race condition and prevent NPE when resource gets out of the queue between getWhy() and causeOfBlockage()
CauseOfBlockage cause = qi.getCauseOfBlockage();
if (cause != null) {
listener.getLogger().print("Configuration " + ModelHyperlinkNote.encodeTo(c)+" is still in the queue: ");
cause.print(listener); //this is still shown on the same line
whyInQueue = why;
}
}
}
Thread.sleep(1000);
}
}
@Extension
public static class DescriptorImpl extends MatrixExecutionStrategyDescriptor {
@Override
public String getDisplayName() {
return "Classic";
}
}
}