diff --git a/changelog.html b/changelog.html
index ad2cc71d7b..5ee68ba6fe 100644
--- a/changelog.html
+++ b/changelog.html
@@ -55,7 +55,9 @@ Upcoming changes
- -
+
-
+ Remember the lastStable/Failed/Successful/etc builds to avoid eager loading builds.
+ (issue 16089)
diff --git a/core/src/main/java/hudson/Util.java b/core/src/main/java/hudson/Util.java
index 76f2171813..241dfa747b 100644
--- a/core/src/main/java/hudson/Util.java
+++ b/core/src/main/java/hudson/Util.java
@@ -1144,6 +1144,12 @@ public class Util {
* Resolves symlink, if the given file is a symlink. Otherwise return null.
*
* If the resolution fails, report an error.
+ *
+ * @return
+ * null if the given file is not a symlink.
+ * If the symlink is absolute, the returned string is an absolute path.
+ * If the symlink is relative, the returned string is that relative representation.
+ * The relative path is meant to be resolved from the location of the symlink.
*/
public static String resolveSymlink(File link) throws InterruptedException, IOException {
try { // Java 7
diff --git a/core/src/main/java/hudson/model/Job.java b/core/src/main/java/hudson/model/Job.java
index bad60d378c..2d8afeccfe 100644
--- a/core/src/main/java/hudson/model/Job.java
+++ b/core/src/main/java/hudson/model/Job.java
@@ -799,13 +799,7 @@ public abstract class Job, RunT extends Run BUILTIN = new CopyOnWriteArrayList();
+ public static final Permalink LAST_BUILD = new Permalink() {
+ public String getDisplayName() {
+ return Messages.Permalink_LastBuild();
+ }
+
+ public String getId() {
+ return "lastBuild";
+ }
+
+ public Run,?> resolve(Job,?> job) {
+ return job.getLastBuild();
+ }
+ };
+ public static final Permalink LAST_STABLE_BUILD = new PeepholePermalink() {
+ public String getDisplayName() {
+ return Messages.Permalink_LastStableBuild();
+ }
+
+ public String getId() {
+ return "lastStableBuild";
+ }
+
+ @Override
+ public boolean apply(Run, ?> run) {
+ return !run.isBuilding() && run.getResult()==Result.SUCCESS;
+ }
+ };
+ public static final Permalink LAST_SUCCESSFUL_BUILD = new PeepholePermalink() {
+ public String getDisplayName() {
+ return Messages.Permalink_LastSuccessfulBuild();
+ }
+
+ public String getId() {
+ return "lastSuccessfulBuild";
+ }
+
+ @Override
+ public boolean apply(Run, ?> run) {
+ return !run.isBuilding() && run.getResult().isBetterOrEqualTo(Result.UNSTABLE);
+ }
+ };
+ public static final Permalink LAST_FAILED_BUILD = new PeepholePermalink() {
+ public String getDisplayName() {
+ return Messages.Permalink_LastFailedBuild();
+ }
+
+ public String getId() {
+ return "lastFailedBuild";
+ }
+
+ @Override
+ public boolean apply(Run, ?> run) {
+ return !run.isBuilding() && run.getResult()==Result.FAILURE;
+ }
+ };
+
+ public static final Permalink LAST_UNSTABLE_BUILD = new PeepholePermalink() {
+ public String getDisplayName() {
+ return Messages.Permalink_LastUnstableBuild();
+ }
+
+ public String getId() {
+ return "lastUnstableBuild";
+ }
+
+ @Override
+ public boolean apply(Run, ?> run) {
+ return !run.isBuilding() && run.getResult()==Result.UNSTABLE;
+ }
+ };
+
+ public static final Permalink LAST_UNSUCCESSFUL_BUILD = new PeepholePermalink() {
+ public String getDisplayName() {
+ return Messages.Permalink_LastUnsuccessfulBuild();
+ }
+
+ public String getId() {
+ return "lastUnsuccessfulBuild";
+ }
+
+ @Override
+ public boolean apply(Run, ?> run) {
+ return !run.isBuilding() && run.getResult()!=Result.SUCCESS;
+ }
+ };
+
static {
- BUILTIN.add(new Permalink() {
- public String getDisplayName() {
- return Messages.Permalink_LastBuild();
- }
-
- public String getId() {
- return "lastBuild";
- }
-
- public Run,?> resolve(Job,?> job) {
- return job.getLastBuild();
- }
- });
-
- BUILTIN.add(new Permalink() {
- public String getDisplayName() {
- return Messages.Permalink_LastStableBuild();
- }
-
- public String getId() {
- return "lastStableBuild";
- }
-
- public Run,?> resolve(Job,?> job) {
- return job.getLastStableBuild();
- }
- });
-
- BUILTIN.add(new Permalink() {
- public String getDisplayName() {
- return Messages.Permalink_LastSuccessfulBuild();
- }
-
- public String getId() {
- return "lastSuccessfulBuild";
- }
-
- public Run,?> resolve(Job,?> job) {
- return job.getLastSuccessfulBuild();
- }
- });
-
- BUILTIN.add(new Permalink() {
- public String getDisplayName() {
- return Messages.Permalink_LastFailedBuild();
- }
-
- public String getId() {
- return "lastFailedBuild";
- }
-
- public Run,?> resolve(Job,?> job) {
- return job.getLastFailedBuild();
- }
- });
-
- BUILTIN.add(new Permalink() {
- public String getDisplayName() {
- return Messages.Permalink_LastUnstableBuild();
- }
-
- public String getId() {
- return "lastUnstableBuild";
- }
-
- public Run,?> resolve(Job,?> job) {
- return job.getLastUnstableBuild();
- }
- });
-
- BUILTIN.add(new Permalink() {
- public String getDisplayName() {
- return Messages.Permalink_LastUnsuccessfulBuild();
- }
-
- public String getId() {
- return "lastUnsuccessfulBuild";
- }
-
- public Run,?> resolve(Job,?> job) {
- return job.getLastUnsuccessfulBuild();
- }
- });
+ BUILTIN.add(LAST_BUILD);
+ BUILTIN.add(LAST_STABLE_BUILD);
+ BUILTIN.add(LAST_SUCCESSFUL_BUILD);
+ BUILTIN.add(LAST_FAILED_BUILD);
+ BUILTIN.add(LAST_UNSTABLE_BUILD);
+ BUILTIN.add(LAST_UNSUCCESSFUL_BUILD);
}
}
}
diff --git a/core/src/main/java/hudson/model/Result.java b/core/src/main/java/hudson/model/Result.java
index d7f4a5970c..312c1ff494 100644
--- a/core/src/main/java/hudson/model/Result.java
+++ b/core/src/main/java/hudson/model/Result.java
@@ -84,6 +84,7 @@ public final class Result implements Serializable, CustomExportedBean {
* Default ball color for this status.
*/
public final BallColor color;
+ private boolean stable;
private Result(String name, BallColor color, int ordinal) {
this.name = name;
diff --git a/core/src/main/java/jenkins/model/PeepholePermalink.java b/core/src/main/java/jenkins/model/PeepholePermalink.java
new file mode 100644
index 0000000000..9ba47f0638
--- /dev/null
+++ b/core/src/main/java/jenkins/model/PeepholePermalink.java
@@ -0,0 +1,184 @@
+package jenkins.model;
+
+import com.google.common.base.Predicate;
+import hudson.Functions;
+import hudson.Util;
+import hudson.model.Job;
+import hudson.model.PermalinkProjectAction.Permalink;
+import hudson.model.Run;
+import hudson.util.AtomicFileWriter;
+import hudson.util.StreamTaskListener;
+import org.apache.commons.io.FileUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * Convenient base implementation for {@link Permalink}s that satisfy
+ * certain properties.
+ *
+ *
+ * For a permalink to be able to use this, it has to satisfy the following:
+ *
+ *
+ * Given a job J, permalink is a function F that computes a build B.
+ * A peephole permalink is a subset of this function that can be
+ * deduced to the "peep-hole" function G(B)->bool:
+ *
+ *
+ * F(J) = { newest B | G(B)==true }
+ *
+ *
+ *
+ *
+ * Intuitively speaking, a peep-hole permalink resolves to the latest build that
+ * satisfies a certain characteristics that can be determined solely by looking
+ * at the build and nothing else (most commonly its build result.)
+ *
+ *
+ * This base class provides a file-based caching mechanism that avoids
+ * walking the long build history. The cache is a symlink to the build directory
+ * where symlinks are supported, and text file that contains the build number otherwise.
+ *
+ *
+ * The implementation transparently tolerates G(B) that goes from true to false over time
+ * (it simply scans the history till find the new matching build.) To tolerate G(B)
+ * that goes from false to true, you need to be able to intercept whenever G(B) changes
+ * from false to true, then call {@link #resolve(Job)} to check the current permalink target
+ * is up to date, then call {@link #updateCache(Job, int)} if it needs updating.
+ *
+ * @author Kohsuke Kawaguchi
+ * @since 1.507
+ */
+public abstract class PeepholePermalink extends Permalink implements Predicate> {
+ /**
+ * Checks if the given build satifies the peep-hole criteria.
+ *
+ * This is the "G(B)" as described in the class javadoc.
+ */
+ public abstract boolean apply(Run,?> run);
+
+ /**
+ * The file in which the permalink target gets recorded.
+ */
+ protected File getPermalinkFile(Job,?> job) {
+ return new File(job.getRootDir(),"permalinks/"+getId());
+ }
+
+ /**
+ * Resolves the permalink by using the cache if possible.
+ */
+ @Override
+ public Run, ?> resolve(Job, ?> job) {
+ File f = getPermalinkFile(job);
+ Run,?> b=null;
+
+ try {
+ String target = null;
+ if (USE_SYMLINK) { // f.exists() return false if symlink exists but point to a non-existent directory
+ target = Util.resolveSymlink(f);
+ if (target==null && f.exists()) {
+ // if this file isn't a symlink, it must be a regular file
+ target = FileUtils.readFileToString(f,"UTF-8").trim();
+ }
+ } else {
+ if (f.exists()) {
+ // if this file isn't a symlink, it must be a regular file
+ target = FileUtils.readFileToString(f,"UTF-8").trim();
+ }
+ }
+
+ if (target!=null) {
+ int n = Integer.parseInt(Util.getFileName(target));
+ if (n==RESOLVES_TO_NONE) return null;
+
+ b = job.getBuildByNumber(n);
+ if (b!=null && apply(b))
+ return b; // found it (in the most efficient way possible)
+
+ // the cache is stale. start the search
+ if (b==null)
+ b=job.getNearestOldBuild(n);
+ }
+ } catch (InterruptedException e) {
+ LOGGER.log(Level.WARNING, "Failed to read permalink cache:" + f, e);
+ // if we fail to read the cache, fall back to the re-computation
+ } catch (IOException e) {
+ LOGGER.log(Level.WARNING, "Failed to read permalink cache:" + f, e);
+ // if we fail to read the cache, fall back to the re-computation
+ }
+
+ if (b==null) {
+ // no cache
+ b = job.getLastBuild();
+ }
+
+ int n;
+ // start from the build 'b' and locate the build that matches the criteria going back in time
+ while (true) {
+ if (b==null) {
+ n = RESOLVES_TO_NONE;
+ break;
+ }
+ if (apply(b)) {
+ n = b.getNumber();
+ break;
+ }
+
+ b=b.getPreviousBuild();
+ }
+
+ updateCache(job,n);
+ return b;
+ }
+
+ /**
+ * Remembers the value 'n' in the cache for future {@link #resolve(Job)}.
+ */
+ protected void updateCache(Job,?> job, int n) {
+ File cache = getPermalinkFile(job);
+ cache.getParentFile().mkdirs();
+
+ try {
+ StringWriter w = new StringWriter();
+ StreamTaskListener listener = new StreamTaskListener(w);
+
+ if (USE_SYMLINK) {
+ Util.createSymlink(cache.getParentFile(),"../builds/"+n,cache.getName(),listener);
+ } else {
+ // symlink not supported. use a regular
+ AtomicFileWriter cw = new AtomicFileWriter(cache);
+ try {
+ cw.write(String.valueOf(n));
+ cw.commit();
+ } finally {
+ cw.abort();
+ }
+ }
+ } catch (IOException e) {
+ LOGGER.log(Level.WARNING, "Failed to update permalink cache for " + job, e);
+ cache.delete();
+ } catch (InterruptedException e) {
+ LOGGER.log(Level.WARNING, "Failed to update permalink cache for "+job,e);
+ cache.delete();
+ }
+ }
+
+ private static final int RESOLVES_TO_NONE = -1;
+
+ private static final Logger LOGGER = Logger.getLogger(PeepholePermalink.class.getName());
+
+ /**
+ * True if we use the symlink as cache, false if plain text file.
+ *
+ *
+ * On Windows, even with Java7, using symlinks require one to go through quite a few hoops
+ * (you need to change the security policy to specifically have this permission, then
+ * you better not be in the administrator group because this token gets filtered out
+ * on UAC-enabled Windows.)
+ */
+ public static boolean USE_SYMLINK = !Functions.isWindows();
+}