/*
* The MIT License
*
* Copyright 2014 Jesse Glick.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.model.lazy;
import hudson.Extension;
import hudson.model.AbstractItem;
import hudson.model.Item;
import hudson.model.ItemGroup;
import hudson.model.Job;
import hudson.model.Queue;
import hudson.model.Run;
import hudson.model.RunMap;
import hudson.model.listeners.ItemListener;
import hudson.model.queue.SubTask;
import hudson.widgets.BuildHistoryWidget;
import hudson.widgets.HistoryWidget;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.DoNotUse;
import static java.util.logging.Level.FINER;
import jenkins.model.RunIdMigrator;
/**
* Makes it easier to use a lazy {@link RunMap} from a {@link Job} implementation.
* Provides method implementations for some abstract {@link Job} methods,
* as well as some methods which are not abstract but which you should override.
* <p>Should be kept in a {@code transient} field in the job.
* @since 1.556
*/
@SuppressWarnings({"unchecked", "rawtypes"}) // BuildHistoryWidget, and AbstractItem.getParent
public abstract class LazyBuildMixIn<JobT extends Job<JobT,RunT> & Queue.Task & LazyBuildMixIn.LazyLoadingJob<JobT,RunT>, RunT extends Run<JobT,RunT> & LazyBuildMixIn.LazyLoadingRun<JobT,RunT>> {
private static final Logger LOGGER = Logger.getLogger(LazyBuildMixIn.class.getName());
@SuppressWarnings("deprecation") // [JENKINS-15156] builds accessed before onLoad or onCreatedFromScratch called
private @Nonnull RunMap<RunT> builds = new RunMap<RunT>();
/**
* Initializes this mixin.
* Call this from a constructor and {@link AbstractItem#onLoad} to make sure it is always initialized.
*/
protected LazyBuildMixIn() {}
protected abstract JobT asJob();
/**
* 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<RunT> getRunMap() {
return builds;
}
/**
* Same as {@link #getRunMap} but suitable for {@link Job#_getRuns}.
*/
public final RunMap<RunT> _getRuns() {
assert builds.baseDirInitialized() : "neither onCreatedFromScratch nor onLoad called on " + asJob() + " yet";
return builds;
}
/**
* Something to be called from {@link Job#onCreatedFromScratch}.
*/
public final void onCreatedFromScratch() {
builds = createBuildRunMap();
}
/**
* Something to be called from {@link Job#onLoad}.
*/
@SuppressWarnings("unchecked")
public void onLoad(ItemGroup<? extends Item> parent, String name) throws IOException {
RunMap<RunT> _builds = createBuildRunMap();
int max = _builds.maxNumberOnDisk();
int next = asJob().getNextBuildNumber();
if (next <= max) {
LOGGER.log(Level.WARNING, "JENKINS-27530: improper nextBuildNumber {0} detected in {1} with highest build number {2}; adjusting", new Object[] {next, asJob(), max});
asJob().updateNextBuildNumber(max + 1);
}
RunMap<RunT> 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() == asJob().getClass()) {
currentBuilds = (RunMap<RunT>) ((LazyLoadingJob) current).getLazyBuildMixIn().builds;
}
}
if (currentBuilds != null) {
// if we are reloading, keep all those that are still building intact
for (RunT r : currentBuilds.getLoadedBuilds().values()) {
if (r.isBuilding()) {
// Do not use RunMap.put(Run):
_builds.put(r.getNumber(), r);
LOGGER.log(Level.FINE, "keeping reloaded {0}", r);
}
}
}
this.builds = _builds;
}
private RunMap<RunT> createBuildRunMap() {
RunMap<RunT> r = new RunMap<RunT>(asJob().getBuildDir(), new RunMap.Constructor<RunT>() {
@Override public RunT create(File dir) throws IOException {
return loadBuild(dir);
}
});
RunIdMigrator runIdMigrator = asJob().runIdMigrator;
assert runIdMigrator != null;
r.runIdMigrator = runIdMigrator;
return r;
}
/**
* 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<RunT> getBuildClass();
/**
* Loads an existing build record from disk.
* The default implementation just calls the ({@link Job}, {@link File}) constructor of {@link #getBuildClass}.
*/
public RunT loadBuild(File dir) throws IOException {
try {
return getBuildClass().getConstructor(asJob().getClass(), File.class).newInstance(asJob(), 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}.
* Suitable for {@link SubTask#createExecutable}.
*/
public final synchronized RunT newBuild() throws IOException {
try {
RunT lastBuild = getBuildClass().getConstructor(asJob().getClass()).newInstance(asJob());
builds.put(lastBuild);
lastBuild.getPreviousBuild(); // JENKINS-20662: create connection to previous build
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(RunT run) {
if (!builds.remove(run)) {
LOGGER.log(Level.WARNING, "{0} did not contain {1} to begin with", new Object[] {asJob(), run});
}
}
/**
* Suitable for {@link Job#getBuild}.
*/
public final RunT getBuild(String id) {
return builds.getById(id);
}
/**
* Suitable for {@link Job#getBuildByNumber}.
*/
public final RunT getBuildByNumber(int n) {
return builds.getByNumber(n);
}
/**
* Suitable for {@link Job#getFirstBuild}.
*/
public final RunT getFirstBuild() {
return builds.oldestBuild();
}
/**
* Suitable for {@link Job#getLastBuild}.
*/
public final @CheckForNull RunT getLastBuild() {
return builds.newestBuild();
}
/**
* Suitable for {@link Job#getNearestBuild}.
*/
public final RunT getNearestBuild(int n) {
return builds.search(n, AbstractLazyLoadRunMap.Direction.ASC);
}
/**
* Suitable for {@link Job#getNearestOldBuild}.
*/
public final RunT getNearestOldBuild(int n) {
return builds.search(n, AbstractLazyLoadRunMap.Direction.DESC);
}
/**
* Suitable for {@link Job#createHistoryWidget}.
*/
public final HistoryWidget createHistoryWidget() {
return new BuildHistoryWidget(asJob(), builds, Job.HISTORY_ADAPTER);
}
/**
* Marker for a {@link Job} which uses this mixin.
*/
public interface LazyLoadingJob<JobT extends Job<JobT,RunT> & Queue.Task & LazyBuildMixIn.LazyLoadingJob<JobT,RunT>, RunT extends Run<JobT,RunT> & LazyLoadingRun<JobT,RunT>> {
LazyBuildMixIn<JobT,RunT> getLazyBuildMixIn();
}
public interface LazyLoadingRun<JobT extends Job<JobT,RunT> & Queue.Task & LazyBuildMixIn.LazyLoadingJob<JobT,RunT>, RunT extends Run<JobT,RunT> & LazyLoadingRun<JobT,RunT>> {
RunMixIn<JobT,RunT> getRunMixIn();
}
/**
* Accompanying helper for the run type.
* Stateful but should be held in a {@code transient final} field.
*/
public static abstract class RunMixIn<JobT extends Job<JobT,RunT> & Queue.Task & LazyBuildMixIn.LazyLoadingJob<JobT,RunT>, RunT extends Run<JobT,RunT> & LazyLoadingRun<JobT,RunT>> {
/**
* Pointers to form bi-directional link between adjacent runs using
* {@link LazyBuildMixIn}.
*
* <p>
* Some {@link Run}s do lazy-loading, so we don't use
* {@link #previousBuildR} and {@link #nextBuildR}, and instead use these
* fields and point to {@link #selfReference} (or {@link #none}) of
* adjacent builds.
*/
private volatile BuildReference<RunT> previousBuildR, nextBuildR;
/**
* Used in {@link #previousBuildR} and {@link #nextBuildR} to indicate
* that we know there is no next/previous build (as opposed to {@code null},
* which is used to indicate we haven't determined if there is a next/previous
* build.)
*/
@SuppressWarnings({"unchecked", "rawtypes"})
private static final BuildReference NONE = new BuildReference("NONE", null);
@SuppressWarnings("unchecked")
private BuildReference<RunT> none() {
return NONE;
}
private BuildReference<RunT> selfReference;
protected RunMixIn() {}
protected abstract RunT asRun();
/**
* To implement {@link Run#createReference}.
*/
public final synchronized BuildReference<RunT> createReference() {
if (selfReference == null) {
selfReference = new BuildReference<RunT>(asRun().getId(), asRun());
}
return selfReference;
}
/**
* To implement {@link Run#dropLinks}.
*/
public final void dropLinks() {
if (nextBuildR != null) {
RunT nb = nextBuildR.get();
if (nb != null) {
nb.getRunMixIn().previousBuildR = previousBuildR;
}
}
if (previousBuildR != null) {
RunT pb = previousBuildR.get();
if (pb != null) {
pb.getRunMixIn().nextBuildR = nextBuildR;
}
}
// make this build object unreachable by other Runs
createReference().clear();
}
/**
* To implement {@link Run#getPreviousBuild}.
*/
public final RunT getPreviousBuild() {
while (true) {
BuildReference<RunT> r = previousBuildR; // capture the value once
if (r == null) {
// having two neighbors pointing to each other is important to make RunMap.removeValue work
JobT _parent = asRun().getParent();
if (_parent == null) {
throw new IllegalStateException("no parent for " + asRun().number);
}
RunT pb = _parent.getLazyBuildMixIn()._getRuns().search(asRun().number - 1, AbstractLazyLoadRunMap.Direction.DESC);
if (pb != null) {
pb.getRunMixIn().nextBuildR = createReference(); // establish bi-di link
this.previousBuildR = pb.getRunMixIn().createReference();
LOGGER.log(FINER, "Linked {0}<->{1} in getPreviousBuild()", new Object[]{this, pb});
return pb;
} else {
this.previousBuildR = none();
return null;
}
}
if (r == none()) {
return null;
}
RunT referent = r.get();
if (referent != null) {
return referent;
}
// the reference points to a GC-ed object, drop the reference and do it again
this.previousBuildR = null;
}
}
/**
* To implement {@link Run#getNextBuild}.
*/
public final RunT getNextBuild() {
while (true) {
BuildReference<RunT> r = nextBuildR; // capture the value once
if (r == null) {
// having two neighbors pointing to each other is important to make RunMap.removeValue work
RunT nb = asRun().getParent().getLazyBuildMixIn()._getRuns().search(asRun().number + 1, AbstractLazyLoadRunMap.Direction.ASC);
if (nb != null) {
nb.getRunMixIn().previousBuildR = createReference(); // establish bi-di link
this.nextBuildR = nb.getRunMixIn().createReference();
LOGGER.log(FINER, "Linked {0}<->{1} in getNextBuild()", new Object[]{this, nb});
return nb;
} else {
this.nextBuildR = none();
return null;
}
}
if (r == none()) {
return null;
}
RunT referent = r.get();
if (referent != null) {
return referent;
}
// the reference points to a GC-ed object, drop the reference and do it again
this.nextBuildR = null;
}
}
}
@Restricted(DoNotUse.class)
@Extension public static final class ItemListenerImpl extends ItemListener {
@Override public void onLocationChanged(Item item, String oldFullName, String newFullName) {
if (item instanceof LazyLoadingJob) {
RunMap<?> builds = ((LazyLoadingJob) item).getLazyBuildMixIn().builds;
builds.updateBaseDir(((Job) item).getBuildDir());
}
}
}
}