diff --git a/core/src/main/java/hudson/matrix/MatrixConfiguration.java b/core/src/main/java/hudson/matrix/MatrixConfiguration.java index f57fff9682d52e6d8cdd245f43c04e077eaf8553..9704a35a9611629520c637a38c54630583842bc4 100644 --- a/core/src/main/java/hudson/matrix/MatrixConfiguration.java +++ b/core/src/main/java/hudson/matrix/MatrixConfiguration.java @@ -248,7 +248,7 @@ public class MatrixConfiguration extends Project lastBuild.number = lb.getNumber(); - builds.put(lastBuild); + _getRuns().put(lastBuild); return lastBuild; } diff --git a/core/src/main/java/hudson/model/AbstractBuild.java b/core/src/main/java/hudson/model/AbstractBuild.java index eeeaf4e9272b2e3553e528c9550301e2a6f2ed74..d1e9902c4398a78df2869c91ec761ba307967fcc 100644 --- a/core/src/main/java/hudson/model/AbstractBuild.java +++ b/core/src/main/java/hudson/model/AbstractBuild.java @@ -244,7 +244,7 @@ public abstract class AbstractBuild

,R extends Abs if (r==null) { // having two neighbors pointing to each other is important to make RunMap.removeValue work - R nb = getParent().builds.search(number+1, Direction.ASC); + R nb = getParent()._getRuns().search(number+1, Direction.ASC); if (nb!=null) { ((AbstractBuild)nb).previousBuild = selfReference; // establish bi-di link this.nextBuild = nb.selfReference; diff --git a/core/src/main/java/hudson/model/AbstractProject.java b/core/src/main/java/hudson/model/AbstractProject.java index 7271a76e80be561eadd6bb096a993249c6722d6a..523a1a2de5b57ee92f592ab93dd4bfc7e1a83e85 100644 --- a/core/src/main/java/hudson/model/AbstractProject.java +++ b/core/src/main/java/hudson/model/AbstractProject.java @@ -32,7 +32,6 @@ import com.infradna.tool.bridge_method_injector.WithBridgeMethods; import hudson.AbortException; import hudson.CopyOnWrite; import hudson.EnvVars; -import hudson.Extension; import hudson.ExtensionPoint; import hudson.FeedAdapter; import hudson.FilePath; @@ -49,7 +48,6 @@ import hudson.model.Fingerprint.RangeSet; import hudson.model.PermalinkProjectAction.Permalink; import hudson.model.Queue.Executable; import hudson.model.Queue.Task; -import hudson.model.RunMap.Constructor; import hudson.model.labels.LabelAtom; import hudson.model.labels.LabelExpression; import hudson.model.listeners.ItemListener; @@ -86,11 +84,9 @@ import hudson.util.AlternativeUiTextProvider.Message; import hudson.util.DescribableList; import hudson.util.FormValidation; import hudson.util.TimeUnit2; -import hudson.widgets.BuildHistoryWidget; import hudson.widgets.HistoryWidget; import java.io.File; import java.io.IOException; -import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; @@ -118,7 +114,7 @@ import jenkins.model.Jenkins; import jenkins.model.JenkinsLocationConfiguration; import jenkins.model.ModelObjectWithChildren; import jenkins.model.Uptime; -import jenkins.model.lazy.AbstractLazyLoadRunMap.Direction; +import jenkins.model.lazy.LazyBuildMixIn; import jenkins.scm.DefaultSCMCheckoutStrategyImpl; import jenkins.scm.SCMCheckoutStrategy; import jenkins.scm.SCMCheckoutStrategyDescriptor; @@ -129,7 +125,6 @@ import org.acegisecurity.context.SecurityContext; import org.acegisecurity.context.SecurityContextHolder; import org.jenkinsci.bytecode.AdaptField; import org.kohsuke.accmod.Restricted; -import org.kohsuke.accmod.restrictions.DoNotUse; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.args4j.Argument; import org.kohsuke.args4j.CmdLineException; @@ -173,15 +168,16 @@ public abstract class AbstractProject

,R extends A */ private volatile transient SCMRevisionState pollingBaseline = null; + private transient LazyBuildMixIn buildMixIn; + /** * All the builds keyed by their build number. - * + * Kept here for binary compatibility only; otherwise use {@link #buildMixIn}. * External code should use {@link #getBuildByNumber(int)} or {@link #getLastBuild()} and traverse via * {@link Run#getPreviousBuild()} */ @Restricted(NoExternalUse.class) - @SuppressWarnings("deprecation") // [JENKINS-15156] builds accessed before onLoad or onCreatedFromScratch called - protected transient RunMap builds = new RunMap(); + protected transient RunMap builds; /** * The quiet period. Null to delegate to the system default. @@ -274,6 +270,7 @@ public abstract class AbstractProject

,R extends A protected AbstractProject(ItemGroup parent, String name) { super(parent,name); + initBuildMixIn(); if(Jenkins.getInstance()!=null && !Jenkins.getInstance().getNodes().isEmpty()) { // if a new job is configured with Hudson that already has slave nodes @@ -282,6 +279,15 @@ public abstract class AbstractProject

,R extends A } } + private void initBuildMixIn() { + buildMixIn = new LazyBuildMixIn(this) { + @Override protected Class getBuildClass() { + return AbstractProject.this.getBuildClass(); + } + }; + builds = buildMixIn.getRunMap(); + } + @Override public synchronized void save() throws IOException { super.save(); @@ -291,7 +297,8 @@ public abstract class AbstractProject

,R extends A @Override public void onCreatedFromScratch() { super.onCreatedFromScratch(); - builds = createBuildRunMap(); + buildMixIn.onCreatedFromScratch(); + builds = buildMixIn.getRunMap(); // solicit initial contributions, especially from TransientProjectActionFactory updateTransientActions(); } @@ -299,33 +306,7 @@ public abstract class AbstractProject

,R extends A @Override public void onLoad(ItemGroup parent, String name) throws IOException { super.onLoad(parent, name); - - RunMap builds = createBuildRunMap(); - - RunMap currentBuilds = this.builds; - - if (currentBuilds==null && parent!=null) { - // are we overwriting what currently exist? - // this is primarily when Jenkins is getting reloaded - Item current; - try { - current = parent.getItem(name); - } catch (RuntimeException x) { - LOGGER.log(Level.WARNING, "failed to look up " + name + " in " + parent, x); - current = null; - } - if (current!=null && current.getClass()==getClass()) { - currentBuilds = ((AbstractProject)current).builds; - } - } - if (currentBuilds !=null) { - // if we are reloading, keep all those that are still building intact - for (R r : currentBuilds.getLoadedBuilds().values()) { - if (r.isBuilding()) - builds.put(r); - } - } - this.builds = builds; + initBuildMixIn(); triggers().setOwner(this); for (Trigger t : triggers()) { t.start(this, Items.currentlyUpdatingByXml()); @@ -338,14 +319,6 @@ public abstract class AbstractProject

,R extends A updateTransientActions(); } - private RunMap createBuildRunMap() { - return new RunMap(getBuildDir(), new Constructor() { - public R create(File dir) throws IOException { - return loadBuild(dir); - } - }); - } - @WithBridgeMethods(List.class) protected DescribableList,TriggerDescriptor> triggers() { if (triggers == null) { @@ -1039,18 +1012,12 @@ public abstract class AbstractProject

,R extends A @Override public RunMap _getRuns() { - if (builds == null) { - throw new IllegalStateException("no run map created yet for " + this); - } - assert builds.baseDirInitialized() : "neither onCreatedFromScratch nor onLoad called on " + this + " yet"; - return builds; + return buildMixIn._getRuns(); } @Override public void removeRun(R run) { - if (!this.builds.remove(run)) { - LOGGER.log(Level.WARNING, "{0} did not contain {1} to begin with", new Object[] {this, run}); - } + buildMixIn.removeRun(run); } /** @@ -1060,7 +1027,7 @@ public abstract class AbstractProject

,R extends A */ @Override public R getBuild(String id) { - return builds.getById(id); + return buildMixIn.getBuild(id); } /** @@ -1070,7 +1037,7 @@ public abstract class AbstractProject

,R extends A */ @Override public R getBuildByNumber(int n) { - return builds.getByNumber(n); + return buildMixIn.getBuildByNumber(n); } /** @@ -1080,22 +1047,22 @@ public abstract class AbstractProject

,R extends A */ @Override public R getFirstBuild() { - return builds.oldestBuild(); + return buildMixIn.getFirstBuild(); } @Override public @CheckForNull R getLastBuild() { - return builds.newestBuild(); + return buildMixIn.getLastBuild(); } @Override public R getNearestBuild(int n) { - return builds.search(n, Direction.ASC); + return buildMixIn.getNearestBuild(n); } @Override public R getNearestOldBuild(int n) { - return builds.search(n, Direction.DESC); + return buildMixIn.getNearestOldBuild(n); } /** @@ -1106,61 +1073,19 @@ public abstract class AbstractProject

,R extends A */ protected abstract Class getBuildClass(); - // keep track of the previous time we started a build - private transient long lastBuildStartTime; - /** * Creates a new build of this project for immediate execution. + * Suitable for {@link SubTask#createExecutable}. */ protected synchronized R newBuild() throws IOException { - // make sure we don't start two builds in the same second - // so the build directories will be different too - long timeSinceLast = System.currentTimeMillis() - lastBuildStartTime; - if (timeSinceLast < 1000) { - try { - Thread.sleep(1000 - timeSinceLast); - } catch (InterruptedException e) { - } - } - lastBuildStartTime = System.currentTimeMillis(); - try { - R lastBuild = getBuildClass().getConstructor(getClass()).newInstance(this); - builds.put(lastBuild); - return lastBuild; - } catch (InstantiationException e) { - throw new Error(e); - } catch (IllegalAccessException e) { - throw new Error(e); - } catch (InvocationTargetException e) { - throw handleInvocationTargetException(e); - } catch (NoSuchMethodException e) { - throw new Error(e); - } - } - - private IOException handleInvocationTargetException(InvocationTargetException e) { - Throwable t = e.getTargetException(); - if(t instanceof Error) throw (Error)t; - if(t instanceof RuntimeException) throw (RuntimeException)t; - if(t instanceof IOException) return (IOException)t; - throw new Error(t); + return buildMixIn.newBuild(); } /** * Loads an existing build record from disk. */ protected R loadBuild(File dir) throws IOException { - try { - return getBuildClass().getConstructor(getClass(),File.class).newInstance(this,dir); - } catch (InstantiationException e) { - throw new Error(e); - } catch (IllegalAccessException e) { - throw new Error(e); - } catch (InvocationTargetException e) { - throw handleInvocationTargetException(e); - } catch (NoSuchMethodException e) { - throw new Error(e); - } + return buildMixIn.loadBuild(dir); } /** @@ -1820,7 +1745,7 @@ public abstract class AbstractProject

,R extends A @Override protected HistoryWidget createHistoryWidget() { - return new BuildHistoryWidget(this,builds,HISTORY_ADAPTER); + return buildMixIn.createHistoryWidget(); } public boolean isParameterized() { @@ -2426,14 +2351,4 @@ public abstract class AbstractProject

,R extends A public abstract FormValidation check(@Nonnull AbstractProject project, @Nonnull Label label); } - @Restricted(DoNotUse.class) - @Extension public static final class ItemListenerImpl extends ItemListener { - @Override public void onLocationChanged(Item item, String oldFullName, String newFullName) { - if (item instanceof AbstractProject) { - AbstractProject p = (AbstractProject) item; - p.builds.updateBaseDir(p.getBuildDir()); - } - } - } - } diff --git a/core/src/main/java/hudson/model/Job.java b/core/src/main/java/hudson/model/Job.java index d73e1f6b686f36612816b372fd82d0c492d32bf3..56734c0e9faa5e4f34b9fb6862a1e01b4b19f3e9 100644 --- a/core/src/main/java/hudson/model/Job.java +++ b/core/src/main/java/hudson/model/Job.java @@ -98,6 +98,7 @@ import java.util.*; import java.util.List; import static javax.servlet.http.HttpServletResponse.*; +import jenkins.model.lazy.LazyBuildMixIn; /** * A job is an runnable entity under the monitoring of Hudson. @@ -570,11 +571,14 @@ public abstract class Job, RunT extends Run(this, getBuilds(), HISTORY_ADAPTER); } - protected static final HistoryWidget.Adapter HISTORY_ADAPTER = new Adapter() { + public static final HistoryWidget.Adapter HISTORY_ADAPTER = new Adapter() { public int compare(Run record, String key) { try { int k = Integer.parseInt(key); @@ -674,6 +678,7 @@ public abstract class Job, RunT extends Run, RunT extends Run, RunT extends Run m = _getRuns().headMap(n - 1); // the map should @@ -738,6 +745,7 @@ public abstract class Job, RunT extends Run m = _getRuns().tailMap(n); @@ -787,6 +795,7 @@ public abstract class Job, RunT extends Run _getRuns(); @@ -795,11 +804,13 @@ public abstract class Job, RunT extends Run, RunT extends RunShould be kept in a {@code transient} field in the job. + * @since TODO + */ +public abstract class LazyBuildMixIn

,R extends Run> { + + private static final Logger LOGGER = Logger.getLogger(LazyBuildMixIn.class.getName()); + + private final Job job; + + @SuppressWarnings("deprecation") // [JENKINS-15156] builds accessed before onLoad or onCreatedFromScratch called + private @Nonnull RunMap builds = new RunMap(); + + // keep track of the previous time we started a build + private long lastBuildStartTime; + + /** + * Initializes this mixin based on a job. + * Call this from a constructor and {@link AbstractItem#onLoad} to make sure it is always initialized. + * @param job the owning job (should be of type {@code P} and assignable to {@link Task}) + */ + public LazyBuildMixIn(Job job) { + this.job = job; + } + + /** + * Gets the raw model. + * Normally should not be called as such. + * Note that the initial value is replaced during {@link #onCreatedFromScratch} or {@link #onLoad}. + */ + public final @Nonnull RunMap getRunMap() { + return builds; + } + + /** + * Same as {@link #getRunMap} but suitable for {@link Job#_getRuns}, which you must override to be public. + */ + public final RunMap _getRuns() { + assert builds.baseDirInitialized() : "neither onCreatedFromScratch nor onLoad called on " + job + " yet"; + return builds; + } + + /** + * Something to be called from {@link AbstractItem#onCreatedFromScratch}. + */ + public final void onCreatedFromScratch() { + builds = createBuildRunMap(); + } + + /** + * Something to be called from {@link AbstractItem#onLoad}. + */ + @SuppressWarnings("unchecked") + public void onLoad(ItemGroup parent, String name) throws IOException { + RunMap _builds = createBuildRunMap(); + RunMap currentBuilds = this.builds; + if (parent != null) { + // are we overwriting what currently exist? + // this is primarily when Jenkins is getting reloaded + Item current; + try { + current = parent.getItem(name); + } catch (RuntimeException x) { + LOGGER.log(Level.WARNING, "failed to look up " + name + " in " + parent, x); + current = null; + } + if (current != null && current.getClass() == job.getClass()) { + try { + currentBuilds = (RunMap) current.getClass().getMethod("_getRuns").invoke(current); + } catch (Exception x) { + assert false : "you should have made _getRuns public in " + job.getClass(); + } + } + } + if (currentBuilds != null) { + // if we are reloading, keep all those that are still building intact + for (R r : currentBuilds.getLoadedBuilds().values()) { + if (r.isBuilding()) { + _builds.put(r); + } + } + } + this.builds = _builds; + } + + private RunMap createBuildRunMap() { + return new RunMap(job.getBuildDir(), new RunMap.Constructor() { + @Override public R create(File dir) throws IOException { + return loadBuild(dir); + } + }); + } + + /** + * Type token for the build type. + * The build class must have two constructors: + * one taking the project type ({@code P}); + * and one taking {@code P}, then {@link File}. + */ + protected abstract Class getBuildClass(); + + /** + * Loads an existing build record from disk. + * The default implementation just calls the ({@link Job}, {@link File}) constructor of {@link #getBuildClass}. + */ + public R loadBuild(File dir) throws IOException { + try { + return getBuildClass().getConstructor(job.getClass(), File.class).newInstance(job, dir); + } catch (InstantiationException e) { + throw new Error(e); + } catch (IllegalAccessException e) { + throw new Error(e); + } catch (InvocationTargetException e) { + throw handleInvocationTargetException(e); + } catch (NoSuchMethodException e) { + throw new Error(e); + } + } + + /** + * Creates a new build of this project for immediate execution. + * Calls the ({@link Job}) constructor of {@link #getBuildClass}. + */ + @SuppressWarnings("SleepWhileHoldingLock") + @edu.umd.cs.findbugs.annotations.SuppressWarnings("SWL_SLEEP_WITH_LOCK_HELD") + public final synchronized R newBuild() throws IOException { + // make sure we don't start two builds in the same second + // so the build directories will be different too + long timeSinceLast = System.currentTimeMillis() - lastBuildStartTime; + if (timeSinceLast < 1000) { + try { + Thread.sleep(1000 - timeSinceLast); + } catch (InterruptedException e) { + } + } + lastBuildStartTime = System.currentTimeMillis(); + try { + R lastBuild = getBuildClass().getConstructor(job.getClass()).newInstance(job); + builds.put(lastBuild); + return lastBuild; + } catch (InstantiationException e) { + throw new Error(e); + } catch (IllegalAccessException e) { + throw new Error(e); + } catch (InvocationTargetException e) { + throw handleInvocationTargetException(e); + } catch (NoSuchMethodException e) { + throw new Error(e); + } + } + + private IOException handleInvocationTargetException(InvocationTargetException e) { + Throwable t = e.getTargetException(); + if (t instanceof Error) { + throw (Error) t; + } + if (t instanceof RuntimeException) { + throw (RuntimeException) t; + } + if (t instanceof IOException) { + return (IOException) t; + } + throw new Error(t); + } + + /** + * Suitable for {@link Job#removeRun}. + */ + public final void removeRun(R run) { + if (!builds.remove(run)) { + LOGGER.log(Level.WARNING, "{0} did not contain {1} to begin with", new Object[] {job, run}); + } + } + + /** + * Suitable for {@link Job#getBuild}. + */ + public final R getBuild(String id) { + return builds.getById(id); + } + + /** + * Suitable for {@link Job#getBuildByNumber}. + */ + public final R getBuildByNumber(int n) { + return builds.getByNumber(n); + } + + /** + * Suitable for {@link Job#getFirstBuild}. + */ + public final R getFirstBuild() { + return builds.oldestBuild(); + } + + /** + * Suitable for {@link Job#getLastBuild}. + */ + public final @CheckForNull R getLastBuild() { + return builds.newestBuild(); + } + + /** + * Suitable for {@link Job#getNearestBuild}. + */ + public final R getNearestBuild(int n) { + return builds.search(n, AbstractLazyLoadRunMap.Direction.ASC); + } + + /** + * Suitable for {@link Job#getNearestOldBuild}. + */ + public final R getNearestOldBuild(int n) { + return builds.search(n, AbstractLazyLoadRunMap.Direction.DESC); + } + + /** + * Suitable for {@link Job#createHistoryWidget}. + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + public final HistoryWidget createHistoryWidget() { + return new BuildHistoryWidget((Task) job, builds, Job.HISTORY_ADAPTER); + } + + @Restricted(DoNotUse.class) + @Extension public static final class ItemListenerImpl extends ItemListener { + @Override public void onLocationChanged(Item item, String oldFullName, String newFullName) { + if (item instanceof Job) { + RunMap builds; + try { + builds = (RunMap) item.getClass().getMethod("_getRuns").invoke(item); + } catch (NoSuchMethodException x) { + // OK, did not override this to be public + return; + } catch (ClassCastException x) { + // override it to be public but of a different type, fine + return; + } catch (Exception x) { + LOGGER.log(Level.WARNING, null, x); + return; + } + builds.updateBaseDir(((Job) item).getBuildDir()); + } + } + } + +}