Skip to content

Commit

Permalink
[FIXED JENKINS-18196] Loading the last 3 successful builds may be ver…
Browse files Browse the repository at this point in the history
…y expensive in certain rare, but possible situations (all newer builds failed, only some very old builds were successful).

Go only up to 6 build into the past for calculating the estimated duration.
Also take failed builds into account, if we don't find any successful ones.
(cherry picked from commit 04d85eb)
  • Loading branch information
kutzi authored and olivergondza committed Sep 13, 2013
1 parent 3f460f0 commit 693ee2a
Show file tree
Hide file tree
Showing 4 changed files with 106 additions and 19 deletions.
49 changes: 48 additions & 1 deletion core/src/main/java/hudson/model/Job.java
Expand Up @@ -26,6 +26,7 @@
import com.google.common.base.Function;
import com.google.common.collect.Collections2;
import com.infradna.tool.bridge_method_injector.WithBridgeMethods;

import hudson.EnvVars;
import hudson.Extension;
import hudson.ExtensionPoint;
Expand Down Expand Up @@ -67,6 +68,7 @@
import jenkins.util.io.OnMaster;
import net.sf.json.JSONException;
import net.sf.json.JSONObject;

import org.jfree.chart.ChartFactory;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.CategoryAxis;
Expand All @@ -87,6 +89,7 @@
import org.kohsuke.stapler.interceptor.RequirePOST;

import javax.servlet.ServletException;

import java.awt.*;
import java.io.*;
import java.net.URLEncoder;
Expand Down Expand Up @@ -873,8 +876,52 @@ public List<RunT> getLastBuildsOverThreshold(int numberOfBuilds, Result threshol
return result;
}

/**
* Returns candidate build for calculating the estimated duration of the current run.
*
* Returns the 3 last successful (stable or unstable) builds, if there are any.
* Failing to find 3 of those, it will return up to 3 last unsuccessful builds.
*
* In any case it will not go more than 6 builds into the past to avoid costly build loading.
*/
@SuppressWarnings("unchecked")
public List<RunT> getEstimedDurationCandidates() {
List<RunT> candidates = new ArrayList<RunT>(3);
RunT lastSuccessful = (RunT) Permalink.LAST_SUCCESSFUL_BUILD.resolve(this);
int lastSuccessfulNumber = -1;
if (lastSuccessful != null) {
candidates.add(lastSuccessful);
lastSuccessfulNumber = lastSuccessful.getNumber();
}

int i = 0;
RunT r = (RunT) Permalink.LAST_BUILD.resolve(this);
List<RunT> fallbackCandidates = new ArrayList<RunT>(3);
while (r != null && candidates.size() < 3 && i < 6) {
if (!r.isBuilding() && r.getResult() != null && r.getNumber() != lastSuccessfulNumber) {
Result result = r.getResult();
if (result.isBetterOrEqualTo(Result.UNSTABLE)) {
candidates.add(r);
} else if (result.isCompleteBuild()) {
fallbackCandidates.add(r);
}
}
i++;
r = r.getPreviousBuild();
}

while (candidates.size() < 3) {
if (fallbackCandidates.isEmpty())
break;
RunT run = fallbackCandidates.remove(0);
candidates.add(run);
}

return candidates;
}

public long getEstimatedDuration() {
List<RunT> builds = getLastBuildsOverThreshold(3, Result.UNSTABLE);
List<RunT> builds = getEstimedDurationCandidates();

if(builds.isEmpty()) return -1;

Expand Down
27 changes: 21 additions & 6 deletions core/src/main/java/hudson/model/Result.java
Expand Up @@ -48,30 +48,30 @@ public final class Result implements Serializable, CustomExportedBean {
/**
* The build had no errors.
*/
public static final Result SUCCESS = new Result("SUCCESS",BallColor.BLUE,0);
public static final Result SUCCESS = new Result("SUCCESS",BallColor.BLUE,0,true);
/**
* The build had some errors but they were not fatal.
* For example, some tests failed.
*/
public static final Result UNSTABLE = new Result("UNSTABLE",BallColor.YELLOW,1);
public static final Result UNSTABLE = new Result("UNSTABLE",BallColor.YELLOW,1,true);
/**
* The build had a fatal error.
*/
public static final Result FAILURE = new Result("FAILURE",BallColor.RED,2);
public static final Result FAILURE = new Result("FAILURE",BallColor.RED,2,true);
/**
* The module was not built.
* <p>
* This status code is used in a multi-stage build (like maven2)
* where a problem in earlier stage prevented later stages from building.
*/
public static final Result NOT_BUILT = new Result("NOT_BUILT",BallColor.NOTBUILT,3);
public static final Result NOT_BUILT = new Result("NOT_BUILT",BallColor.NOTBUILT,3,false);
/**
* The build was manually aborted.
*
* If you are catching {@link InterruptedException} and interpreting it as {@link #ABORTED},
* you should check {@link Executor#abortResult()} instead (starting 1.417.)
*/
public static final Result ABORTED = new Result("ABORTED",BallColor.ABORTED,4);
public static final Result ABORTED = new Result("ABORTED",BallColor.ABORTED,4,false);

private final String name;

Expand All @@ -84,11 +84,17 @@ public final class Result implements Serializable, CustomExportedBean {
* Default ball color for this status.
*/
public final BallColor color;

/**
* Is this a complete build - i.e. did it run to the end (not aborted)?
*/
public final boolean completeBuild;

private Result(String name, BallColor color, int ordinal) {
private Result(String name, BallColor color, int ordinal, boolean complete) {
this.name = name;
this.color = color;
this.ordinal = ordinal;
this.completeBuild = complete;
}

/**
Expand Down Expand Up @@ -116,6 +122,15 @@ public boolean isBetterThan(Result that) {
public boolean isBetterOrEqualTo(Result that) {
return this.ordinal <= that.ordinal;
}

/**
* Is this a complete build - i.e. did it run to the end (not aborted)?
*
* @since TODO
*/
public boolean isCompleteBuild() {
return this.completeBuild;
}


@Override
Expand Down
Expand Up @@ -350,7 +350,7 @@ public long getEstimatedDuration() {
* to the sum of durations of the module builds.
*/
private long estimateModuleSetBuildDurationOverhead(int numberOfBuilds) {
List<MavenModuleSetBuild> moduleSetBuilds = getPreviousBuildsOverThreshold(numberOfBuilds, Result.UNSTABLE);
List<MavenModuleSetBuild> moduleSetBuilds = getParent().getEstimedDurationCandidates();

if (moduleSetBuilds.isEmpty()) {
return 0;
Expand Down
47 changes: 36 additions & 11 deletions test/src/test/java/hudson/model/SimpleJobTest.java
Expand Up @@ -4,15 +4,21 @@
import java.util.SortedMap;
import java.util.TreeMap;

import junit.framework.Assert;
import org.jvnet.hudson.test.HudsonTestCase;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.JenkinsRule;

/**
* Unit test for {@link Job}.
*/
@SuppressWarnings("rawtypes")
public class SimpleJobTest extends HudsonTestCase {
public class SimpleJobTest {

@Rule
public JenkinsRule rule = new JenkinsRule();

@Test
public void testGetEstimatedDuration() throws IOException {

final SortedMap<Integer, TestBuild> runs = new TreeMap<Integer, TestBuild>();
Expand All @@ -28,12 +34,13 @@ public void testGetEstimatedDuration() throws IOException {
TestBuild lastBuild = new TestBuild(project, Result.SUCCESS, 42, previousBuild);
runs.put(1, lastBuild);

// without assuming to know to much about the internal calculation
// without assuming to know too much about the internal calculation
// we can only assume that the result is between the maximum and the minimum
Assert.assertTrue(project.getEstimatedDuration() < 42);
Assert.assertTrue(project.getEstimatedDuration() > 15);
Assert.assertTrue("Expected < 42, but was "+project.getEstimatedDuration(), project.getEstimatedDuration() < 42);
Assert.assertTrue("Expected > 15, but was "+project.getEstimatedDuration(), project.getEstimatedDuration() > 15);
}

@Test
public void testGetEstimatedDurationWithOneRun() throws IOException {

final SortedMap<Integer, TestBuild> runs = new TreeMap<Integer, TestBuild>();
Expand All @@ -58,6 +65,7 @@ public void testGetEstimatedDurationWithFailedRun() throws IOException {
Assert.assertEquals(-1, project.getEstimatedDuration());
}

@Test
public void testGetEstimatedDurationWithNoRuns() throws IOException {

final SortedMap<Integer, TestBuild> runs = new TreeMap<Integer, TestBuild>();
Expand All @@ -67,13 +75,17 @@ public void testGetEstimatedDurationWithNoRuns() throws IOException {
Assert.assertEquals(-1, project.getEstimatedDuration());
}

@Test
public void testGetEstimatedDurationIfPrevious3BuildsFailed() throws IOException {

final SortedMap<Integer, TestBuild> runs = new TreeMap<Integer, TestBuild>();

Job project = createMockProject(runs);

TestBuild prev4Build = new TestBuild(project, Result.SUCCESS, 1, null);
TestBuild prev5Build = new TestBuild(project, Result.UNSTABLE, 1, null);
runs.put(6, prev5Build);

TestBuild prev4Build = new TestBuild(project, Result.SUCCESS, 1, prev5Build);
runs.put(5, prev4Build);

TestBuild prev3Build = new TestBuild(project, Result.SUCCESS, 1, prev4Build);
Expand All @@ -88,9 +100,21 @@ public void testGetEstimatedDurationIfPrevious3BuildsFailed() throws IOException
TestBuild lastBuild = new TestBuild(project, Result.FAILURE, 50, previousBuild);
runs.put(1, lastBuild);

// failed builds must not be used. Instead the last successful builds before them
// must be used
Assert.assertEquals(project.getEstimatedDuration(), 1);
// failed builds must not be used, if there are succesfulBuilds available.
Assert.assertEquals(1, project.getEstimatedDuration());
}

@Test
public void testGetEstimatedDurationIfNoSuccessfulBuildTakeDurationOfFailedBuild() throws IOException {

final SortedMap<Integer, TestBuild> runs = new TreeMap<Integer, TestBuild>();

Job project = createMockProject(runs);

TestBuild lastBuild = new TestBuild(project, Result.FAILURE, 50, null);
runs.put(1, lastBuild);

Assert.assertEquals(50, project.getEstimatedDuration());
}

private Job createMockProject(final SortedMap<Integer, TestBuild> runs) {
Expand Down Expand Up @@ -124,13 +148,14 @@ public boolean isBuilding() {

}

@SuppressWarnings("unchecked")
private class TestJob extends Job implements TopLevelItem {

int i;
private final SortedMap<Integer, TestBuild> runs;

public TestJob(SortedMap<Integer, TestBuild> runs) {
super(SimpleJobTest.this.jenkins, "name");
super(rule.jenkins, "name");
this.runs = runs;
i = 1;
}
Expand Down

0 comments on commit 693ee2a

Please sign in to comment.