mirror of
https://github.com/jenkinsci/jenkins.git
synced 2026-11-14 04:22:16 +00:00
Merge commit '711b3325adf629585d60aa5a2bcac1d49b8324ae'
This commit is contained in:
commit
4c474cea6f
24 files changed
+858
-1428
No files matched your search
@@ -105,7 +105,10 @@ import javax.annotation.Nonnull;
|
||||
|
||||
import static javax.servlet.http.HttpServletResponse.*;
|
||||
import jenkins.model.ModelObjectWithChildren;
|
||||
import jenkins.model.RunIdMigrator;
|
||||
import jenkins.model.lazy.LazyBuildMixIn;
|
||||
import org.kohsuke.accmod.Restricted;
|
||||
import org.kohsuke.accmod.restrictions.NoExternalUse;
|
||||
|
||||
/**
|
||||
* A job is an runnable entity under the monitoring of Hudson.
|
||||
@@ -162,6 +165,9 @@ public abstract class Job<JobT extends Job<JobT, RunT>, RunT extends Run<JobT, R
|
||||
// this should have been DescribableList but now it's too late
|
||||
protected CopyOnWriteList<JobProperty<? super JobT>> properties = new CopyOnWriteList<JobProperty<? super JobT>>();
|
||||
|
||||
@Restricted(NoExternalUse.class)
|
||||
public transient RunIdMigrator runIdMigrator;
|
||||
|
||||
protected Job(ItemGroup parent, String name) {
|
||||
super(parent, name);
|
||||
}
|
||||
@@ -172,11 +178,21 @@ public abstract class Job<JobT extends Job<JobT, RunT>, RunT extends Run<JobT, R
|
||||
holdOffBuildUntilSave = holdOffBuildUntilUserSave;
|
||||
}
|
||||
|
||||
@Override public void onCreatedFromScratch() {
|
||||
super.onCreatedFromScratch();
|
||||
runIdMigrator = new RunIdMigrator();
|
||||
runIdMigrator.created(getBuildDir());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoad(ItemGroup<? extends Item> parent, String name)
|
||||
throws IOException {
|
||||
super.onLoad(parent, name);
|
||||
|
||||
File buildDir = getBuildDir();
|
||||
runIdMigrator = new RunIdMigrator();
|
||||
runIdMigrator.migrate(buildDir, Jenkins.getInstance().getRootDir());
|
||||
|
||||
TextFile f = getNextBuildNumberFile();
|
||||
if (f.exists()) {
|
||||
// starting 1.28, we store nextBuildNumber in a separate file.
|
||||
@@ -188,7 +204,7 @@ public abstract class Job<JobT extends Job<JobT, RunT>, RunT extends Run<JobT, R
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
// try to infer the value of the next build number from the existing build records. See JENKINS-11563
|
||||
File[] folders = this.getBuildDir().listFiles(new FileFilter() {
|
||||
File[] folders = buildDir.listFiles(new FileFilter() {
|
||||
public boolean accept(File file) {
|
||||
return file.isDirectory() && file.getName().matches("[0-9]+");
|
||||
}
|
||||
@@ -638,8 +654,7 @@ public abstract class Job<JobT extends Job<JobT, RunT>, RunT extends Run<JobT, R
|
||||
File oldBuildDir = getBuildDir();
|
||||
super.movedTo(destination, newItem, destDir);
|
||||
File newBuildDir = getBuildDir();
|
||||
if (oldBuildDir.isDirectory() && !newBuildDir.isDirectory()) {
|
||||
FileUtils.forceMkdir(destDir.getParentFile());
|
||||
if (oldBuildDir.isDirectory()) {
|
||||
FileUtils.moveDirectory(oldBuildDir, newBuildDir);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ import hudson.model.Descriptor.FormException;
|
||||
import hudson.model.Run.RunExecution;
|
||||
import hudson.model.listeners.RunListener;
|
||||
import hudson.model.listeners.SaveableListener;
|
||||
import hudson.model.queue.SubTask;
|
||||
import hudson.search.SearchIndexBuilder;
|
||||
import hudson.security.ACL;
|
||||
import hudson.security.AccessControlled;
|
||||
@@ -74,7 +75,6 @@ import java.io.StringWriter;
|
||||
import java.io.Writer;
|
||||
import java.nio.charset.Charset;
|
||||
import java.text.DateFormat;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
@@ -108,6 +108,7 @@ import jenkins.model.PeepholePermalink;
|
||||
import jenkins.model.RunAction2;
|
||||
import jenkins.model.StandardArtifactManager;
|
||||
import jenkins.model.lazy.BuildReference;
|
||||
import jenkins.model.lazy.LazyBuildMixIn;
|
||||
import jenkins.util.VirtualFile;
|
||||
import jenkins.util.io.OnMaster;
|
||||
import net.sf.json.JSONObject;
|
||||
@@ -146,7 +147,7 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
|
||||
* In earlier versions < 1.24, this number is not unique nor continuous,
|
||||
* but going forward, it will, and this really replaces the build id.
|
||||
*/
|
||||
public /*final*/ int number;
|
||||
public transient /*final*/ int number;
|
||||
|
||||
/**
|
||||
* Previous build. Can be null.
|
||||
@@ -172,10 +173,13 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
|
||||
*/
|
||||
/* does not compile on JDK 7: private*/ volatile transient RunT previousBuildInProgress;
|
||||
|
||||
/** ID as used for historical build records; otherwise null. */
|
||||
private @CheckForNull String id;
|
||||
|
||||
/**
|
||||
* When the build is scheduled.
|
||||
*/
|
||||
protected transient final long timestamp;
|
||||
protected /*final*/ long timestamp;
|
||||
|
||||
/**
|
||||
* When the build has started running.
|
||||
@@ -266,23 +270,12 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
|
||||
*/
|
||||
private @CheckForNull ArtifactManager artifactManager;
|
||||
|
||||
private static final SimpleDateFormat CANONICAL_ID_FORMATTER = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss");
|
||||
public static final ThreadLocal<SimpleDateFormat> ID_FORMATTER = new IDFormatterProvider();
|
||||
private static final class IDFormatterProvider extends ThreadLocal<SimpleDateFormat> {
|
||||
@Override
|
||||
protected SimpleDateFormat initialValue() {
|
||||
synchronized (CANONICAL_ID_FORMATTER) {
|
||||
return (SimpleDateFormat) CANONICAL_ID_FORMATTER.clone();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a new {@link Run}.
|
||||
* @param job Owner job
|
||||
*/
|
||||
protected Run(@Nonnull JobT job) throws IOException {
|
||||
this(job, new GregorianCalendar());
|
||||
this(job, System.currentTimeMillis());
|
||||
this.number = project.assignBuildNumber();
|
||||
LOGGER.log(FINER, "new {0} @{1}", new Object[] {this, hashCode()});
|
||||
}
|
||||
@@ -290,24 +283,29 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
|
||||
/**
|
||||
* Constructor for creating a {@link Run} object in
|
||||
* an arbitrary state.
|
||||
* {@link #number} must be set manually.
|
||||
* <p>May be used in a {@link SubTask#createExecutable} (instead of calling {@link LazyBuildMixIn#newBuild}).
|
||||
* For example, {@code MatrixConfiguration.newBuild} does this
|
||||
* so that the {@link #timestamp} as well as {@link #number} are shared with the parent build.
|
||||
*/
|
||||
protected Run(@Nonnull JobT job, @Nonnull Calendar timestamp) {
|
||||
this(job,timestamp.getTimeInMillis());
|
||||
}
|
||||
|
||||
/** @see #Run(Job, Calendar) */
|
||||
protected Run(@Nonnull JobT job, long timestamp) {
|
||||
this.project = job;
|
||||
this.timestamp = timestamp;
|
||||
this.state = State.NOT_STARTED;
|
||||
getRootDir().mkdirs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a run from a log file.
|
||||
*/
|
||||
protected Run(@Nonnull JobT project, @Nonnull File buildDir) throws IOException {
|
||||
this(project, parseTimestampFromBuildDir(buildDir));
|
||||
this.project = project;
|
||||
this.previousBuildInProgress = _this(); // loaded builds are always completed
|
||||
number = Integer.parseInt(buildDir.getName());
|
||||
reload();
|
||||
}
|
||||
|
||||
@@ -390,33 +388,6 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
|
||||
}
|
||||
}
|
||||
|
||||
static class InvalidDirectoryNameException extends IOException {
|
||||
InvalidDirectoryNameException(File buildDir) {
|
||||
super("Invalid directory name " + buildDir);
|
||||
}
|
||||
}
|
||||
|
||||
/*package*/ static long parseTimestampFromBuildDir(@Nonnull File buildDir)
|
||||
throws IOException, InvalidDirectoryNameException {
|
||||
try {
|
||||
if(Util.isSymlink(buildDir)) {
|
||||
// "Util.resolveSymlink(file)" resolves NTFS symlinks.
|
||||
File target = Util.resolveSymlinkToFile(buildDir);
|
||||
if(target != null)
|
||||
buildDir = target;
|
||||
}
|
||||
// canonicalization to ensure we are looking at the ID in the directory name
|
||||
// as opposed to build numbers which are used in symlinks
|
||||
// (just in case the symlink check above did not work)
|
||||
buildDir = buildDir.getCanonicalFile();
|
||||
return ID_FORMATTER.get().parse(buildDir.getName()).getTime();
|
||||
} catch (ParseException e) {
|
||||
throw new InvalidDirectoryNameException(buildDir);
|
||||
} catch (InterruptedException e) {
|
||||
throw new IOException("Interrupted while resolving symlink directory "+buildDir,e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains 'this' in a more type safe signature.
|
||||
*/
|
||||
@@ -1000,21 +971,13 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
|
||||
|
||||
/**
|
||||
* Unique ID of this build.
|
||||
* Usually the decimal form of {@link #number}, but may be a formatted timestamp for historical builds.
|
||||
*/
|
||||
@Exported
|
||||
public @Nonnull String getId() {
|
||||
return ID_FORMATTER.get().format(new Date(timestamp));
|
||||
return id != null ? id : Integer.toString(number);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the date formatter used to convert the directory name in to a timestamp.
|
||||
* This is nasty exposure of private data, but needed all the time the directory
|
||||
* containing the build is used as it's timestamp.
|
||||
*/
|
||||
public static @Nonnull DateFormat getIDFormatter() {
|
||||
return ID_FORMATTER.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @CheckForNull Descriptor getDescriptorByName(String className) {
|
||||
return Jenkins.getInstance().getDescriptorByName(className);
|
||||
@@ -1027,7 +990,7 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
|
||||
*/
|
||||
@Override
|
||||
public @Nonnull File getRootDir() {
|
||||
return new File(project.getBuildDir(),getId());
|
||||
return new File(project.getBuildDir(), Integer.toString(number));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1502,10 +1465,6 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
|
||||
RunListener.fireDeleted(this);
|
||||
|
||||
synchronized (this) { // avoid holding a lock while calling plugin impls of onDeleted
|
||||
// if we have a symlink, delete it, too
|
||||
File link = new File(project.getBuildDir(), String.valueOf(getNumber()));
|
||||
link.delete();
|
||||
|
||||
File tmp = new File(rootDir.getParentFile(),'.'+rootDir.getName());
|
||||
|
||||
if (tmp.exists()) {
|
||||
@@ -1832,8 +1791,7 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a symlink from build number to ID.
|
||||
* Also makes sure that {@code lastSuccessful} and {@code lastStable} legacy links in the project’s root directory exist.
|
||||
* Makes sure that {@code lastSuccessful} and {@code lastStable} legacy links in the project’s root directory exist.
|
||||
* Normally you do not need to call this explicitly, since {@link #execute} does so,
|
||||
* but this may be needed if you are creating synthetic {@link Run}s as part of a container project (such as Maven builds in a module set).
|
||||
* You should also ensure that {@link RunListener#fireStarted} and {@link RunListener#fireCompleted} are called.
|
||||
@@ -1842,7 +1800,6 @@ public abstract class Run <JobT extends Job<JobT,RunT>,RunT extends Run<JobT,Run
|
||||
* @since 1.530
|
||||
*/
|
||||
public final void updateSymlinks(@Nonnull TaskListener listener) throws InterruptedException {
|
||||
Util.createSymlink(getParent().getBuildDir(), getId(), String.valueOf(getNumber()), listener);
|
||||
createSymlink(listener, "lastSuccessful", PermalinkProjectAction.Permalink.LAST_SUCCESSFUL_BUILD);
|
||||
createSymlink(listener, "lastStable", PermalinkProjectAction.Permalink.LAST_STABLE_BUILD);
|
||||
}
|
||||
|
||||
@@ -24,10 +24,7 @@
|
||||
package hudson.model;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FilenameFilter;
|
||||
import java.io.IOException;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Iterator;
|
||||
@@ -38,10 +35,14 @@ import java.util.logging.Level;
|
||||
|
||||
import static java.util.logging.Level.*;
|
||||
import java.util.logging.Logger;
|
||||
import jenkins.model.RunIdMigrator;
|
||||
import jenkins.model.lazy.AbstractLazyLoadRunMap;
|
||||
import static jenkins.model.lazy.AbstractLazyLoadRunMap.Direction.*;
|
||||
import jenkins.model.lazy.BuildReference;
|
||||
import jenkins.model.lazy.LazyBuildMixIn;
|
||||
import org.apache.commons.collections.comparators.ReverseComparator;
|
||||
import org.kohsuke.accmod.Restricted;
|
||||
import org.kohsuke.accmod.restrictions.NoExternalUse;
|
||||
|
||||
/**
|
||||
* {@link Map} from build number to {@link Run}.
|
||||
@@ -61,6 +62,10 @@ public final class RunMap<R extends Run<?,R>> extends AbstractLazyLoadRunMap<R>
|
||||
|
||||
private Constructor<R> cons;
|
||||
|
||||
/** Normally overwritten by {@link LazyBuildMixIn#onLoad} or {@link LazyBuildMixIn#onCreatedFromScratch}, in turn created during {@link Job#onLoad}. */
|
||||
@Restricted(NoExternalUse.class)
|
||||
public RunIdMigrator runIdMigrator = new RunIdMigrator();
|
||||
|
||||
// TODO: before first complete build
|
||||
// patch up next/previous build link
|
||||
|
||||
@@ -118,6 +123,7 @@ public final class RunMap<R extends Run<?,R>> extends AbstractLazyLoadRunMap<R>
|
||||
@Override
|
||||
public boolean removeValue(R run) {
|
||||
run.dropLinks();
|
||||
runIdMigrator.delete(dir, run.getId());
|
||||
return super.removeValue(run);
|
||||
}
|
||||
|
||||
@@ -169,11 +175,32 @@ public final class RunMap<R extends Run<?,R>> extends AbstractLazyLoadRunMap<R>
|
||||
return r.getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a <em>new</em> build to the map.
|
||||
* Do not use when loading existing builds (use {@link #put(Integer, Object)}).
|
||||
*/
|
||||
@Override
|
||||
public R put(R r) {
|
||||
// Defense against JENKINS-23152 and its ilk.
|
||||
File rootDir = r.getRootDir();
|
||||
if (rootDir.isDirectory()) {
|
||||
throw new IllegalStateException(rootDir + " already existed; will not overwite with " + r);
|
||||
}
|
||||
proposeNewNumber(r.getNumber());
|
||||
rootDir.mkdirs();
|
||||
return super._put(r);
|
||||
}
|
||||
|
||||
@Override public R getById(String id) {
|
||||
int n;
|
||||
try {
|
||||
n = Integer.parseInt(id);
|
||||
} catch (NumberFormatException x) {
|
||||
n = runIdMigrator.findNumber(id);
|
||||
}
|
||||
return getByNumber(n);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reuses the same reference as much as we can.
|
||||
* <p>
|
||||
@@ -185,31 +212,6 @@ public final class RunMap<R extends Run<?,R>> extends AbstractLazyLoadRunMap<R>
|
||||
return r.createReference();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected FilenameFilter createDirectoryFilter() {
|
||||
final SimpleDateFormat formatter = Run.ID_FORMATTER.get();
|
||||
|
||||
return new FilenameFilter() {
|
||||
@Override public boolean accept(File dir, String name) {
|
||||
if (name.startsWith("0000")) {
|
||||
// JENKINS-1461 sometimes create bogus data directories with impossible dates, such as year 0, April 31st,
|
||||
// or August 0th. Date object doesn't roundtrip those, so we eventually fail to load this data.
|
||||
// Don't even bother trying.
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
if (formatter.format(formatter.parse(name)).equals(name)) {
|
||||
return true;
|
||||
}
|
||||
} catch (ParseException e) {
|
||||
// fall through
|
||||
}
|
||||
LOGGER.log(FINE, "Skipping {0} in {1}", new Object[] {name, dir});
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected R retrieve(File d) throws IOException {
|
||||
if(new File(d,"build.xml").exists()) {
|
||||
@@ -221,17 +223,6 @@ public final class RunMap<R extends Run<?,R>> extends AbstractLazyLoadRunMap<R>
|
||||
LOGGER.log(FINEST, "Loaded " + b.getFullDisplayName() + " in " + Thread.currentThread().getName(), new ThisIsHowItsLoaded());
|
||||
}
|
||||
return b;
|
||||
} catch (Run.InvalidDirectoryNameException x) {
|
||||
Level lvl;
|
||||
try {
|
||||
Integer.parseInt(d.getName());
|
||||
// JENKINS-15587: just an mangled symlink
|
||||
lvl = Level.FINE;
|
||||
} catch (NumberFormatException x2) {
|
||||
// potentially a real build dir, maybe a bug
|
||||
lvl = Level.WARNING;
|
||||
}
|
||||
LOGGER.log(lvl, "skipping non-build directory {0}", d);
|
||||
} catch (IOException e) {
|
||||
LOGGER.log(Level.WARNING, "could not load " + d, e);
|
||||
} catch (InstantiationError e) {
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
package jenkins.diagnostics.ooom;
|
||||
|
||||
import hudson.FilePath;
|
||||
import hudson.model.Job;
|
||||
import hudson.model.TaskListener;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* ID and build number of one build.
|
||||
*/
|
||||
public final class BuildPtr implements Comparable<BuildPtr> {
|
||||
final Job job;
|
||||
|
||||
final File buildDir;
|
||||
/**
|
||||
* Timestamp build ID.
|
||||
*/
|
||||
public final String id;
|
||||
/**
|
||||
* Build number found from the disk.
|
||||
*/
|
||||
public final int n;
|
||||
|
||||
/**
|
||||
* Position of this build according to the ordering induced by {@link #n}
|
||||
*/
|
||||
int posByN;
|
||||
|
||||
/**
|
||||
* Position of this build according to the ordering induced by {@link #id}
|
||||
*/
|
||||
int posByID;
|
||||
|
||||
|
||||
BuildPtr(Job job, File buildDir, int n) {
|
||||
this.job = job;
|
||||
this.n = n;
|
||||
this.id = buildDir.getName();
|
||||
this.buildDir = buildDir;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return buildDir.toString()+":#"+n;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* If this build and that build are inconsistent, in that
|
||||
* their numbers and timestamps are ordering in the wrong direction.
|
||||
*/
|
||||
public boolean isInconsistentWith(BuildPtr that) {
|
||||
return signOfCompare(this.posByN,that.posByN) * signOfCompare(this.posByID,that.posByID) < 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* sign of (a-b).
|
||||
*/
|
||||
private static int signOfCompare(int a, int b) {
|
||||
if (a>b) return 1;
|
||||
if (a<b) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix the problem by moving the out of order builds into a place that Jenkins won't look at.
|
||||
*
|
||||
* TODO: another way to fix this is by adjusting the ID and pretend that the build happened
|
||||
* at a different timestamp.
|
||||
*/
|
||||
public void fix(TaskListener listener) throws IOException, InterruptedException {
|
||||
File dir = new File(job.getRootDir(), "outOfOrderBuilds");
|
||||
dir.mkdirs();
|
||||
File dst = new File(dir, buildDir.getName());
|
||||
listener.getLogger().println("Renaming "+buildDir);
|
||||
listener.getLogger().println(" -> "+dst);
|
||||
if (!buildDir.renameTo(dst)) {
|
||||
FilePath bd = new FilePath(buildDir);
|
||||
bd.copyRecursiveTo(new FilePath(dst));
|
||||
bd.deleteRecursive();
|
||||
}
|
||||
|
||||
// if there's a symlink delete it
|
||||
new File(buildDir.getParentFile(),String.valueOf(n)).delete();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(BuildPtr that) {
|
||||
return this.id.compareTo(that.id);
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package jenkins.diagnostics.ooom;
|
||||
|
||||
import hudson.Extension;
|
||||
import hudson.model.AsyncPeriodicWork;
|
||||
import hudson.model.Job;
|
||||
import hudson.model.TaskListener;
|
||||
import hudson.util.TimeUnit2;
|
||||
import jenkins.model.Jenkins;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Discovers {@link Problem}s periodically in the background and
|
||||
* pass them on to {@link OutOfOrderBuildMonitor}.
|
||||
*
|
||||
* @author Kohsuke Kawaguchi
|
||||
*/
|
||||
@Extension
|
||||
public class OutOfOrderBuildDetector extends AsyncPeriodicWork {
|
||||
@Inject
|
||||
private OutOfOrderBuildMonitor monitor;
|
||||
|
||||
|
||||
public OutOfOrderBuildDetector() {
|
||||
super("Out of order build detection");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void execute(TaskListener listener) throws IOException, InterruptedException {
|
||||
execute(listener, 10*1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the check synchronously.
|
||||
*
|
||||
* @param delay
|
||||
* delay in the number of milli-seconds to reduce the load on I/O.
|
||||
*/
|
||||
public void execute(TaskListener listener, int delay) throws InterruptedException {
|
||||
for (Job j : Jenkins.getInstance().getAllItems(Job.class)) {
|
||||
listener.getLogger().println("Scanning " + j.getFullName());
|
||||
|
||||
Problem p = Problem.find(j);
|
||||
if (p!=null) {
|
||||
monitor.addProblem(p);
|
||||
listener.getLogger().println(" found problems: "+p);
|
||||
}
|
||||
|
||||
Thread.sleep(delay);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getRecurrencePeriod() {
|
||||
return TimeUnit2.DAYS.toMillis(1);
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package jenkins.diagnostics.ooom;
|
||||
|
||||
import hudson.Extension;
|
||||
import hudson.model.TaskListener;
|
||||
import hudson.util.HttpResponses;
|
||||
import jenkins.management.AsynchronousAdministrativeMonitor;
|
||||
import org.kohsuke.stapler.HttpResponse;
|
||||
import org.kohsuke.stapler.interceptor.RequirePOST;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Reports any {@link Problem}s found and report them in the "Manage Jenkins" page.
|
||||
*
|
||||
* @author Kohsuke Kawaguchi
|
||||
*/
|
||||
@Extension
|
||||
public class OutOfOrderBuildMonitor extends AsynchronousAdministrativeMonitor {
|
||||
private final Set<Problem> problems = Collections.synchronizedSet(new LinkedHashSet<Problem>());
|
||||
|
||||
@Override
|
||||
public boolean isActivated() {
|
||||
return !problems.isEmpty() || getLogFile().exists();
|
||||
}
|
||||
|
||||
void addProblem(Problem p) {
|
||||
problems.add(p);
|
||||
}
|
||||
|
||||
public Set<Problem> getProblems() {
|
||||
return Collections.unmodifiableSet(new LinkedHashSet<Problem>(problems));
|
||||
}
|
||||
|
||||
@RequirePOST
|
||||
public HttpResponse doFix() {
|
||||
start(false);
|
||||
return HttpResponses.forwardToPreviousPage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Discards the current log file so that the "stuff is completed" message will be gone.
|
||||
*/
|
||||
@RequirePOST
|
||||
public HttpResponse doDismiss() {
|
||||
getLogFile().delete();
|
||||
return HttpResponses.forwardToPreviousPage();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
return "Fix Out-of-order Builds";
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getLogFile() {
|
||||
return super.getLogFile();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void fix(TaskListener listener) throws Exception {
|
||||
Set<Problem> problems = getProblems();
|
||||
for (Problem problem : problems) {
|
||||
problem.fix(listener);
|
||||
}
|
||||
this.problems.removeAll(problems);
|
||||
}
|
||||
}
|
||||
@@ -1,302 +0,0 @@
|
||||
package jenkins.diagnostics.ooom;
|
||||
|
||||
import hudson.AbortException;
|
||||
import hudson.Util;
|
||||
import hudson.model.AbstractProject;
|
||||
import hudson.model.Job;
|
||||
import hudson.model.Run;
|
||||
import hudson.model.TaskListener;
|
||||
import jenkins.model.lazy.AbstractLazyLoadRunMap;
|
||||
import org.xml.sax.InputSource;
|
||||
|
||||
import javax.xml.xpath.XPathConstants;
|
||||
import javax.xml.xpath.XPathExpression;
|
||||
import javax.xml.xpath.XPathExpressionException;
|
||||
import javax.xml.xpath.XPathFactory;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.text.ParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.SortedMap;
|
||||
import java.util.TreeMap;
|
||||
import java.util.TreeSet;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import static java.util.logging.Level.*;
|
||||
|
||||
/**
|
||||
* Look at build numbers in build.xml and compare them with build IDs (timestamps).
|
||||
*
|
||||
* When they are inconsistent (newer build timestamp-wise has an older build number),
|
||||
* it'll confuse the binary search in {@link AbstractLazyLoadRunMap}, so detect them and report them.
|
||||
*
|
||||
* @author Kohsuke Kawaguchi
|
||||
*/
|
||||
public final class Problem {
|
||||
public final Job job;
|
||||
|
||||
/**
|
||||
* A smallest set of builds whose removals would correct the order
|
||||
* inconsistency problem.
|
||||
*/
|
||||
private final Set<BuildPtr> offenders = new TreeSet<BuildPtr>();
|
||||
|
||||
/**
|
||||
* Scans the inconsistencies and return the information about it.
|
||||
*
|
||||
* @return null if no problems were found.
|
||||
*/
|
||||
public static Problem find(Job j) {
|
||||
Problem p = new Problem(j);
|
||||
if (p.countInconsistencies()==0) return null;
|
||||
return p;
|
||||
}
|
||||
|
||||
private Problem(Job j) {
|
||||
this.job = j;
|
||||
new Inspector().inspect();
|
||||
}
|
||||
|
||||
public Set<BuildPtr> getOffenders() {
|
||||
return Collections.unmodifiableSet(offenders);
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of inconsistencies, which is the number of builds whose IDs
|
||||
* have to be messed around on disk to collect problems.
|
||||
*/
|
||||
public int countInconsistencies() {
|
||||
return offenders.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Equality is based on the job.
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
Problem problem = (Problem) o;
|
||||
|
||||
return job.equals(problem.job);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return job.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return job.getFullName() + " " + Util.join(offenders);
|
||||
}
|
||||
|
||||
public void fix(TaskListener listener) throws IOException, InterruptedException {
|
||||
listener.getLogger().println("Fixing problems in " + job.getFullName());
|
||||
for (BuildPtr o : offenders) {
|
||||
o.fix(listener);
|
||||
}
|
||||
|
||||
if (job instanceof AbstractProject) {
|
||||
// let all the current references go and build a new one
|
||||
AbstractProject a = (AbstractProject) job;
|
||||
a._getRuns().purgeCache();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the problems and builds up the data model of {@link Problem}.
|
||||
*/
|
||||
class Inspector {
|
||||
/**
|
||||
* All the builds sorted by their {@link BuildPtr#n}
|
||||
*/
|
||||
private List<BuildPtr> byN;
|
||||
/**
|
||||
* All the builds sorted by their {@link BuildPtr#id}
|
||||
*/
|
||||
private List<BuildPtr> byId;
|
||||
|
||||
private final XPathExpression xpath;
|
||||
|
||||
Inspector() {
|
||||
try {
|
||||
xpath = XPathFactory.newInstance().newXPath().compile("/*/number/text()");
|
||||
} catch (XPathExpressionException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Simply report inconsistencies in the ordering.
|
||||
*/
|
||||
protected void inspect() {
|
||||
Map<Integer, BuildPtr> builds = scan();
|
||||
|
||||
byN = new ArrayList<BuildPtr>(builds.values());
|
||||
// this is already sorted by BuildPtr.n
|
||||
int i=0;
|
||||
for (BuildPtr b : byN) {
|
||||
b.posByN = i++;
|
||||
}
|
||||
|
||||
byId = new ArrayList<BuildPtr>(byN);
|
||||
Collections.sort(byId);
|
||||
i=0;
|
||||
for (BuildPtr b : byId) {
|
||||
b.posByID = i++;
|
||||
}
|
||||
|
||||
while (true) {
|
||||
BuildPtr b = pick();
|
||||
if (b==null)
|
||||
break;
|
||||
offenders.add(b);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the most inconsistent build, a build whose removal
|
||||
* would reduce the # of inconsistencies by the most.
|
||||
*
|
||||
* This process takes {@link #offenders} into account.
|
||||
*
|
||||
* @return null if there's no more build to remove.
|
||||
*/
|
||||
private BuildPtr pick() {
|
||||
BuildPtr worst=null;
|
||||
int worstScore=0;
|
||||
|
||||
for (BuildPtr b : byN) {
|
||||
if (offenders.contains(b))
|
||||
continue;
|
||||
|
||||
int score = score(b);
|
||||
if (score>worstScore) {
|
||||
worst = b;
|
||||
worstScore = score;
|
||||
}
|
||||
}
|
||||
|
||||
return worst;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of other builds the given build is inconsistent with,
|
||||
* excluding inconsistencies with {@link #offenders} (since those inconsistencies
|
||||
* are already effectively resolved by fixing offenders.)
|
||||
*/
|
||||
private int score(BuildPtr b) {
|
||||
int i=0;
|
||||
for (BuildPtr a : byN) {
|
||||
if (offenders.contains(a))
|
||||
continue;
|
||||
if (a.isInconsistentWith(b))
|
||||
i++;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks at the builds directory of the given job and builds up the full map of build number to its ID.
|
||||
*/
|
||||
protected SortedMap<Integer,BuildPtr> scan() {
|
||||
LOGGER.fine("Inspecting "+job);
|
||||
|
||||
SortedMap<Integer,BuildPtr> builds = new TreeMap<Integer,BuildPtr>();
|
||||
|
||||
File[] files = job.getBuildDir().listFiles();
|
||||
if (files==null) return builds;
|
||||
|
||||
for (File build : files) {
|
||||
try {
|
||||
LOGGER.finer("Inspecting " + build);
|
||||
|
||||
if (isInt(build.getName())) {
|
||||
// if this is a number, then it must be a build number
|
||||
String s = loadBuildNumberFromBuildXml(build);
|
||||
if (!s.equals(build.getName())) {
|
||||
LOGGER.warning(build+" contains build number "+s);
|
||||
// this index is invalid.
|
||||
if (build.delete()) {
|
||||
// index should be a symlink, and if so we can just delete it without losing data.
|
||||
LOGGER.info("Removed problematic index "+build);
|
||||
} else {
|
||||
// the deltion will fail if 'build' isn't just a symlink but an actual directory.
|
||||
// That is good, as we don't want to delete any real data.
|
||||
LOGGER.warning("Couldn't delete " + build);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
if (isID(build.getName())) {
|
||||
String bn = loadBuildNumberFromBuildXml(build);
|
||||
if (bn==null) {
|
||||
LOGGER.log(WARNING, "Failed to parse "+build);
|
||||
continue;
|
||||
}
|
||||
|
||||
int n;
|
||||
try {
|
||||
n = Integer.parseInt(bn);
|
||||
} catch (NumberFormatException e) {
|
||||
LOGGER.log(WARNING, "Expected number in " + build + " but found " + bn, e);
|
||||
continue;
|
||||
}
|
||||
|
||||
BuildPtr b = new BuildPtr(job,build,n);
|
||||
|
||||
BuildPtr o = builds.put(n, b);
|
||||
if (o != null) {
|
||||
LOGGER.log(WARNING, "Multiple builds have the same number: {0} vs. {1}", new Object[] {o, b});
|
||||
offenders.add(b.compareTo(o) > 0 ? o : b);
|
||||
}
|
||||
}
|
||||
} catch (XPathExpressionException e) {
|
||||
LOGGER.log(WARNING, "Failed to inspect "+build, e);
|
||||
} catch (AbortException e) {
|
||||
LOGGER.log(WARNING, "Failed to inspect "+build+": "+e.getMessage());
|
||||
} catch (IOException e) {
|
||||
LOGGER.log(WARNING, "Failed to inspect "+build, e);
|
||||
}
|
||||
}
|
||||
|
||||
return builds;
|
||||
}
|
||||
|
||||
private boolean isInt(String s) {
|
||||
try {
|
||||
Integer.parseInt(s);
|
||||
return true;
|
||||
} catch (NumberFormatException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isID(String s) {
|
||||
try {
|
||||
Run.ID_FORMATTER.get().parse(s);
|
||||
return true;
|
||||
} catch (ParseException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private String loadBuildNumberFromBuildXml(File dir) throws XPathExpressionException, IOException {
|
||||
File buildXml = new File(dir, "build.xml");
|
||||
if (!buildXml.exists())
|
||||
throw new AbortException(buildXml+" doesn't exist");
|
||||
String systemId = buildXml.toURI().toURL().toExternalForm();
|
||||
return (String)xpath.evaluate(new InputSource(systemId), XPathConstants.STRING);
|
||||
}
|
||||
}
|
||||
|
||||
private static final Logger LOGGER = Logger.getLogger(Problem.class.getName());
|
||||
}
|
||||
@@ -0,0 +1,407 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import hudson.Extension;
|
||||
import hudson.Util;
|
||||
import hudson.model.Job;
|
||||
import hudson.model.RootAction;
|
||||
import hudson.model.Run;
|
||||
import hudson.util.AtomicFileWriter;
|
||||
import hudson.util.StreamTaskListener;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.text.DateFormat;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import javax.annotation.CheckForNull;
|
||||
import javax.annotation.Nonnull;
|
||||
import org.apache.commons.io.Charsets;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.lang.time.FastDateFormat;
|
||||
import org.apache.tools.ant.BuildException;
|
||||
import org.kohsuke.accmod.Restricted;
|
||||
import org.kohsuke.accmod.restrictions.NoExternalUse;
|
||||
import org.kohsuke.stapler.HttpResponse;
|
||||
import org.kohsuke.stapler.StaplerProxy;
|
||||
import org.kohsuke.stapler.framework.io.WriterOutputStream;
|
||||
|
||||
import static java.util.logging.Level.*;
|
||||
|
||||
/**
|
||||
* Converts legacy {@code builds} directories to the current format.
|
||||
*
|
||||
* There would be one instance associated with each {@link Job}, to retain ID -> build# mapping.
|
||||
*
|
||||
* The {@link Job#getBuildDir} is passed to every method call (rather than being cached) in case it is moved.
|
||||
*/
|
||||
@Restricted(NoExternalUse.class)
|
||||
public final class RunIdMigrator {
|
||||
|
||||
static final Logger LOGGER = Logger.getLogger(RunIdMigrator.class.getName());
|
||||
private static final DateFormat LEGACY_ID_FORMATTER = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss");
|
||||
private static final String MAP_FILE = "legacyIds";
|
||||
/** avoids wasting a map for new jobs */
|
||||
private static final Map<String,Integer> EMPTY = new TreeMap<String,Integer>();
|
||||
|
||||
/**
|
||||
* Did we record "unmigrate" instruction for this $JENKINS_HOME? Yes if it's in the set.
|
||||
*/
|
||||
private static final Set<File> offeredToUnmigrate = Collections.synchronizedSet(new HashSet<File>());
|
||||
|
||||
private @Nonnull Map<String,Integer> idToNumber = EMPTY;
|
||||
|
||||
public RunIdMigrator() {}
|
||||
|
||||
/**
|
||||
* @return whether there was a file to load
|
||||
*/
|
||||
private boolean load(File dir) {
|
||||
File f = new File(dir, MAP_FILE);
|
||||
if (!f.isFile()) {
|
||||
return false;
|
||||
}
|
||||
if (f.length() == 0) {
|
||||
return true;
|
||||
}
|
||||
idToNumber = new TreeMap<String,Integer>();
|
||||
try {
|
||||
for (String line : FileUtils.readLines(f)) {
|
||||
int i = line.indexOf(' ');
|
||||
idToNumber.put(line.substring(0, i), Integer.parseInt(line.substring(i + 1)));
|
||||
}
|
||||
} catch (Exception x) { // IOException, IndexOutOfBoundsException, NumberFormatException
|
||||
LOGGER.log(WARNING, "could not read from " + f, x);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void save(File dir) {
|
||||
File f = new File(dir, MAP_FILE);
|
||||
try {
|
||||
AtomicFileWriter w = new AtomicFileWriter(f);
|
||||
try {
|
||||
for (Map.Entry<String,Integer> entry : idToNumber.entrySet()) {
|
||||
w.write(entry.getKey() + ' ' + entry.getValue() + '\n');
|
||||
}
|
||||
w.commit();
|
||||
} finally {
|
||||
w.abort();
|
||||
}
|
||||
} catch (IOException x) {
|
||||
LOGGER.log(WARNING, "could not save changes to " + f, x);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a job is first created.
|
||||
* Just saves an empty marker indicating that this job needs no migration.
|
||||
* @param dir as in {@link Job#getBuildDir}
|
||||
*/
|
||||
public void created(File dir) {
|
||||
save(dir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform one-time migration if this has not been done already.
|
||||
* Where previously there would be a {@code 2014-01-02_03-04-05/build.xml} specifying {@code <number>99</number>} plus a symlink {@code 99 → 2014-01-02_03-04-05},
|
||||
* after migration there will be just {@code 99/build.xml} specifying {@code <id>2014-01-02_03-04-05</id>} and {@code <timestamp>…</timestamp>} according to local time zone at time of migration.
|
||||
* Newly created builds are untouched.
|
||||
* Does not throw {@link IOException} since we make a best effort to migrate but do not consider it fatal to job loading if we cannot.
|
||||
* @param dir as in {@link Job#getBuildDir}
|
||||
* @param jenkinsHome root directory of Jenkins (for logging only)
|
||||
* @return true if migration was performed
|
||||
*/
|
||||
public synchronized boolean migrate(File dir, @CheckForNull File jenkinsHome) {
|
||||
if (load(dir)) {
|
||||
LOGGER.log(FINER, "migration already performed for {0}", dir);
|
||||
return false;
|
||||
}
|
||||
if (!dir.isDirectory()) {
|
||||
LOGGER.log(/* normal during Job.movedTo */FINE, "{0} was unexpectedly missing", dir);
|
||||
return false;
|
||||
}
|
||||
LOGGER.log(INFO, "Migrating build records in {0}", dir);
|
||||
doMigrate(dir);
|
||||
save(dir);
|
||||
if (jenkinsHome != null && offeredToUnmigrate.add(jenkinsHome))
|
||||
LOGGER.log(WARNING, "Build record migration is one-way. If you need to downgrade Jenkins, run: {0}", getUnmigrationCommandLine(jenkinsHome));
|
||||
return true;
|
||||
}
|
||||
|
||||
private static String getUnmigrationCommandLine(File jenkinsHome) {
|
||||
StringBuilder cp = new StringBuilder();
|
||||
for (Class<?> c : new Class<?>[] {RunIdMigrator.class, /* TODO how to calculate transitive dependencies automatically? */Charsets.class, WriterOutputStream.class, BuildException.class, FastDateFormat.class}) {
|
||||
URL location = c.getProtectionDomain().getCodeSource().getLocation();
|
||||
String locationS = location.toString();
|
||||
if (location.getProtocol().equals("file")) {
|
||||
try {
|
||||
locationS = new File(location.toURI()).getAbsolutePath();
|
||||
} catch (URISyntaxException x) {
|
||||
// never mind
|
||||
}
|
||||
}
|
||||
if (cp.length() > 0) {
|
||||
cp.append(File.pathSeparator);
|
||||
}
|
||||
cp.append(locationS);
|
||||
}
|
||||
return String.format("java -classpath \"%s\" %s \"%s\"", cp, RunIdMigrator.class.getName(), jenkinsHome);
|
||||
}
|
||||
|
||||
private static final Pattern NUMBER_ELT = Pattern.compile("(?m)^ <number>(\\d+)</number>(\r?\n)");
|
||||
private void doMigrate(File dir) {
|
||||
idToNumber = new TreeMap<String,Integer>();
|
||||
File[] kids = dir.listFiles();
|
||||
// Need to process symlinks first so we can rename to them.
|
||||
List<File> kidsList = new ArrayList<File>(Arrays.asList(kids));
|
||||
Iterator<File> it = kidsList.iterator();
|
||||
while (it.hasNext()) {
|
||||
File kid = it.next();
|
||||
String name = kid.getName();
|
||||
try {
|
||||
String link = Util.resolveSymlink(kid);
|
||||
if (link == null) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
Integer.parseInt(name);
|
||||
if (kid.delete()) {
|
||||
LOGGER.log(FINE, "deleted build number symlink {0} → {1}", new Object[] {name, link});
|
||||
} else {
|
||||
LOGGER.log(WARNING, "could not delete build number symlink {0} → {1}", new Object[] {name, link});
|
||||
}
|
||||
} catch (NumberFormatException x) {
|
||||
LOGGER.log(FINE, "skipping other symlink {0} → {1}", new Object[] {name, link});
|
||||
}
|
||||
it.remove();
|
||||
} catch (Exception x) {
|
||||
LOGGER.log(WARNING, "failed to process " + kid, x);
|
||||
}
|
||||
}
|
||||
it = kidsList.iterator();
|
||||
while (it.hasNext()) {
|
||||
File kid = it.next();
|
||||
try {
|
||||
String name = kid.getName();
|
||||
try {
|
||||
Integer.parseInt(name);
|
||||
LOGGER.log(FINE, "skipping new build dir {0}", name);
|
||||
continue;
|
||||
} catch (NumberFormatException x) {
|
||||
// OK, next…
|
||||
}
|
||||
if (!kid.isDirectory()) {
|
||||
LOGGER.log(FINE, "skipping non-directory {0}", name);
|
||||
continue;
|
||||
}
|
||||
long timestamp;
|
||||
try {
|
||||
synchronized (LEGACY_ID_FORMATTER) {
|
||||
timestamp = LEGACY_ID_FORMATTER.parse(name).getTime();
|
||||
}
|
||||
} catch (ParseException x) {
|
||||
LOGGER.log(WARNING, "found unexpected dir {0}", name);
|
||||
continue;
|
||||
}
|
||||
File buildXml = new File(kid, "build.xml");
|
||||
if (!buildXml.isFile()) {
|
||||
LOGGER.log(WARNING, "found no build.xml in {0}", name);
|
||||
continue;
|
||||
}
|
||||
String xml = FileUtils.readFileToString(buildXml, Charsets.UTF_8);
|
||||
Matcher m = NUMBER_ELT.matcher(xml);
|
||||
if (!m.find()) {
|
||||
LOGGER.log(WARNING, "could not find <number> in {0}/build.xml", name);
|
||||
continue;
|
||||
}
|
||||
int number = Integer.parseInt(m.group(1));
|
||||
String nl = m.group(2);
|
||||
xml = m.replaceFirst(" <id>" + name + "</id>" + nl + " <timestamp>" + timestamp + "</timestamp>" + nl);
|
||||
File newKid = new File(dir, Integer.toString(number));
|
||||
if (!kid.renameTo(newKid)) {
|
||||
LOGGER.log(WARNING, "failed to rename {0} to {1}", new Object[] {name, number});
|
||||
continue;
|
||||
}
|
||||
FileUtils.writeStringToFile(new File(newKid, "build.xml"), xml, Charsets.UTF_8);
|
||||
LOGGER.log(FINE, "fully processed {0} → {1}", new Object[] {name, number});
|
||||
idToNumber.put(name, number);
|
||||
} catch (Exception x) {
|
||||
LOGGER.log(WARNING, "failed to process " + kid, x);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up a historical run by ID.
|
||||
* @param id a nonnumeric ID which may be a valid {@link Run#getId}
|
||||
* @return the corresponding {@link Run#number}, or 0 if unknown
|
||||
*/
|
||||
public synchronized int findNumber(@Nonnull String id) {
|
||||
Integer number = idToNumber.get(id);
|
||||
return number != null ? number : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the record of a build.
|
||||
* @param dir as in {@link Job#getBuildDir}
|
||||
* @param id a {@link Run#getId}
|
||||
*/
|
||||
public synchronized void delete(File dir, String id) {
|
||||
if (idToNumber.remove(id) != null) {
|
||||
save(dir);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverses the migration, in case you want to revert to the older format.
|
||||
* @param args one parameter, {@code $JENKINS_HOME}
|
||||
*/
|
||||
public static void main(String... args) throws Exception {
|
||||
if (args.length != 1) {
|
||||
throw new Exception("pass one parameter, $JENKINS_HOME");
|
||||
}
|
||||
File root = new File(args[0]);
|
||||
File jobs = new File(root, "jobs");
|
||||
if (!jobs.isDirectory()) {
|
||||
throw new FileNotFoundException("no such $JENKINS_HOME " + root);
|
||||
}
|
||||
unmigrateJobsDir(jobs);
|
||||
}
|
||||
private static void unmigrateJobsDir(File jobs) throws Exception {
|
||||
for (File job : jobs.listFiles()) {
|
||||
File[] kids = job.listFiles();
|
||||
if (kids == null) {
|
||||
continue;
|
||||
}
|
||||
for (File kid : kids) {
|
||||
if (!kid.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
if (kid.getName().equals("builds")) {
|
||||
unmigrateBuildsDir(kid);
|
||||
} else {
|
||||
// Might be jobs, modules, promotions, etc.; we assume an ItemGroup.getRootDirFor implementation returns grandchildren.
|
||||
unmigrateJobsDir(kid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private static final Pattern ID_ELT = Pattern.compile("(?m)^ <id>([0-9_-]+)</id>(\r?\n)");
|
||||
private static final Pattern TIMESTAMP_ELT = Pattern.compile("(?m)^ <timestamp>(\\d+)</timestamp>(\r?\n)");
|
||||
/** Inverse of {@link #doMigrate}. */
|
||||
private static void unmigrateBuildsDir(File builds) throws Exception {
|
||||
File mapFile = new File(builds, MAP_FILE);
|
||||
if (!mapFile.isFile()) {
|
||||
System.err.println(builds + " does not look to have been migrated yet; skipping");
|
||||
return;
|
||||
}
|
||||
for (File build : builds.listFiles()) {
|
||||
int number;
|
||||
try {
|
||||
number = Integer.parseInt(build.getName());
|
||||
} catch (NumberFormatException x) {
|
||||
continue;
|
||||
}
|
||||
File buildXml = new File(build, "build.xml");
|
||||
if (!buildXml.isFile()) {
|
||||
System.err.println(buildXml + " did not exist");
|
||||
continue;
|
||||
}
|
||||
String xml = FileUtils.readFileToString(buildXml, Charsets.UTF_8);
|
||||
Matcher m = TIMESTAMP_ELT.matcher(xml);
|
||||
if (!m.find()) {
|
||||
System.err.println(buildXml + " did not contain <timestamp> as expected");
|
||||
continue;
|
||||
}
|
||||
long timestamp = Long.parseLong(m.group(1));
|
||||
String nl = m.group(2);
|
||||
xml = m.replaceFirst(" <number>" + number + "</number>" + nl);
|
||||
m = ID_ELT.matcher(xml);
|
||||
String id;
|
||||
if (m.find()) {
|
||||
id = m.group(1);
|
||||
xml = m.replaceFirst("");
|
||||
} else {
|
||||
// Post-migration build. We give it a new ID based on its timestamp.
|
||||
id = LEGACY_ID_FORMATTER.format(new Date(timestamp));
|
||||
}
|
||||
FileUtils.write(buildXml, xml, Charsets.UTF_8);
|
||||
if (!build.renameTo(new File(builds, id))) {
|
||||
System.err.println(build + " could not be renamed");
|
||||
}
|
||||
Util.createSymlink(builds, id, Integer.toString(number), StreamTaskListener.fromStderr());
|
||||
}
|
||||
Util.deleteFile(mapFile);
|
||||
System.err.println(builds + " has been restored to its original format");
|
||||
}
|
||||
|
||||
/**
|
||||
* Expose unmigration instruction to the user.
|
||||
*/
|
||||
@Extension
|
||||
public static class UnmigrationInstruction implements RootAction, StaplerProxy {
|
||||
@Override
|
||||
public String getIconFileName() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUrlName() {
|
||||
return "JENKINS-24380";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getTarget() {
|
||||
Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getCommand() {
|
||||
return RunIdMigrator.getUnmigrationCommandLine(Jenkins.getInstance().getRootDir());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,6 @@ import hudson.model.Job;
|
||||
import hudson.model.Run;
|
||||
import hudson.model.RunMap;
|
||||
import java.io.File;
|
||||
import java.io.FilenameFilter;
|
||||
import java.io.IOException;
|
||||
import java.util.AbstractMap;
|
||||
import java.util.ArrayList;
|
||||
@@ -44,8 +43,6 @@ import java.util.logging.Logger;
|
||||
import javax.annotation.CheckForNull;
|
||||
|
||||
import static jenkins.model.lazy.AbstractLazyLoadRunMap.Direction.*;
|
||||
import static jenkins.model.lazy.Boundary.*;
|
||||
import org.apache.commons.collections.keyvalue.DefaultMapEntry;
|
||||
import org.kohsuke.accmod.Restricted;
|
||||
import org.kohsuke.accmod.restrictions.NoExternalUse;
|
||||
|
||||
@@ -60,26 +57,16 @@ import org.kohsuke.accmod.restrictions.NoExternalUse;
|
||||
* requested, this class {@link #retrieve(File) retrieves them} on demand, one by one.
|
||||
*
|
||||
* <p>
|
||||
* The lookup is primarily done by using the build number as the key (hence the key type is {@link Integer}),
|
||||
* but this class also provides look up based on {@linkplain #getIdOf(Object) the build ID}.
|
||||
* The lookup is done by using the build number as the key (hence the key type is {@link Integer}).
|
||||
*
|
||||
* <p>
|
||||
* This class makes the following assumption about the on-disk layout of the data:
|
||||
*
|
||||
* <ul>
|
||||
* <li>Every build is stored in a directory, named after its ID.
|
||||
* <li>ID and build number are in the consistent order. That is,
|
||||
* if there are two builds #M and #N, {@code M>N <=> M.id > N.id}.
|
||||
* <li>Every build is stored in a directory, named after its number.
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* On certain platforms, there are symbolic links named after build numbers that link to the build ID.
|
||||
* If these are available, they are used as a hint to speed up the lookup. Otherwise
|
||||
* we rely on the assumption above and perform a binary search to locate the build.
|
||||
* (notice that we'll have to do linear search if we don't have the consistent ordering assumption,
|
||||
* which robs the whole point of doing lazy loading.)
|
||||
*
|
||||
* <p>
|
||||
* Some of the {@link SortedMap} operations are weakly implemented. For example,
|
||||
* {@link #size()} may be inaccurate because we only count the number of directories that look like
|
||||
* build records, without checking if they are loadable. But these weaknesses aren't distinguishable
|
||||
@@ -93,7 +80,7 @@ import org.kohsuke.accmod.restrictions.NoExternalUse;
|
||||
* <p>
|
||||
* Object lock of {@code this} is used to make sure mutation occurs sequentially.
|
||||
* That is, ensure that only one thread is actually calling {@link #retrieve(File)} and
|
||||
* updating {@link jenkins.model.lazy.AbstractLazyLoadRunMap.Index#byNumber} and {@link jenkins.model.lazy.AbstractLazyLoadRunMap.Index#byId}.
|
||||
* updating {@link jenkins.model.lazy.AbstractLazyLoadRunMap.Index#byNumber}.
|
||||
*
|
||||
* @author Kohsuke Kawaguchi
|
||||
* @since 1.485
|
||||
@@ -112,8 +99,10 @@ public abstract class AbstractLazyLoadRunMap<R> extends AbstractMap<Integer,R> i
|
||||
private LazyLoadRunMapEntrySet<R> entrySet = new LazyLoadRunMapEntrySet<R>(this);
|
||||
|
||||
/**
|
||||
* Pair of two maps into a single class, so that the changes can be made visible atomically,
|
||||
* and updates can happen concurrently to read.
|
||||
* Historical holder for map.
|
||||
*
|
||||
* TODO all this mess including {@link #numberOnDisk} could probably be simplified to a single {@code TreeMap<Integer,BuildReference<R>>}
|
||||
* where a null value means not yet loaded and a broken entry just uses {@code NoHolder}.
|
||||
*
|
||||
* The idiom is that you put yourself in a synchronized block, {@linkplain #copy() make a copy of this},
|
||||
* update the copy, then set it to {@link #index}.
|
||||
@@ -121,63 +110,24 @@ public abstract class AbstractLazyLoadRunMap<R> extends AbstractMap<Integer,R> i
|
||||
private class Index {
|
||||
/**
|
||||
* Stores the mapping from build number to build, for builds that are already loaded.
|
||||
*/
|
||||
private final TreeMap<Integer,BuildReference<R>> byNumber;
|
||||
|
||||
/**
|
||||
* Stores the build ID to build number for builds that we already know.
|
||||
*
|
||||
* If we have known load failure of the given ID, we record that in the map
|
||||
* by using the null value (not to be confused with a non-null {@link BuildReference}
|
||||
* with null referent, which just means the record was GCed.)
|
||||
*/
|
||||
private final TreeMap<String,BuildReference<R>> byId;
|
||||
private final TreeMap<Integer,BuildReference<R>> byNumber;
|
||||
|
||||
private Index() {
|
||||
byId = new TreeMap<String,BuildReference<R>>();
|
||||
byNumber = new TreeMap<Integer,BuildReference<R>>(COMPARATOR);
|
||||
}
|
||||
|
||||
private Index(Index rhs) {
|
||||
byId = new TreeMap<String, BuildReference<R>>(rhs.byId);
|
||||
byNumber = new TreeMap<Integer,BuildReference<R>>(rhs.byNumber);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the build record #M (<=n)
|
||||
*/
|
||||
private Map.Entry<Integer,BuildReference<R>> ceilingEntry(int n) {
|
||||
// switch to this once we depend on JDK6
|
||||
// return byNumber.ceilingEntry(n);
|
||||
|
||||
Set<Entry<Integer, BuildReference<R>>> s = byNumber.tailMap(n).entrySet();
|
||||
if (s.isEmpty()) return null;
|
||||
else return s.iterator().next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the build record #M (>=n)
|
||||
*/
|
||||
// >= and not <= because byNumber is in the descending order
|
||||
private Map.Entry<Integer,BuildReference<R>> floorEntry(int n) {
|
||||
// switch to this once we depend on JDK6
|
||||
// return byNumber.floorEntry(n);
|
||||
|
||||
SortedMap<Integer, BuildReference<R>> sub = byNumber.headMap(n);
|
||||
if (sub.isEmpty()) return null;
|
||||
Integer k = sub.lastKey();
|
||||
return new DefaultMapEntry(k,sub.get(k));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build IDs found as directories, in the ascending order.
|
||||
*/
|
||||
// copy on write
|
||||
private volatile SortedList<String> idOnDisk = new SortedList<String>(Collections.<String>emptyList());
|
||||
|
||||
/**
|
||||
* Build number shortcuts found on disk, in the ascending order.
|
||||
* Build numbers found on disk, in the ascending order.
|
||||
*/
|
||||
// copy on write
|
||||
private volatile SortedIntList numberOnDisk = new SortedIntList(0);
|
||||
@@ -188,8 +138,9 @@ public abstract class AbstractLazyLoadRunMap<R> extends AbstractMap<Integer,R> i
|
||||
* because the compatibility requires that we make it settable
|
||||
* in the first call after the constructor.
|
||||
*/
|
||||
private File dir;
|
||||
protected File dir;
|
||||
|
||||
@Restricted(NoExternalUse.class) // subclassing other than by RunMap does not guarantee compatibility
|
||||
protected AbstractLazyLoadRunMap(File dir) {
|
||||
initBaseDir(dir);
|
||||
}
|
||||
@@ -199,7 +150,7 @@ public abstract class AbstractLazyLoadRunMap<R> extends AbstractMap<Integer,R> i
|
||||
assert this.dir==null;
|
||||
this.dir = dir;
|
||||
if (dir!=null)
|
||||
loadIdOnDisk();
|
||||
loadNumberOnDisk();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -230,31 +181,23 @@ public abstract class AbstractLazyLoadRunMap<R> extends AbstractMap<Integer,R> i
|
||||
public synchronized void purgeCache() {
|
||||
index = new Index();
|
||||
fullyLoaded = false;
|
||||
loadIdOnDisk();
|
||||
loadNumberOnDisk();
|
||||
}
|
||||
|
||||
private void loadIdOnDisk() {
|
||||
private void loadNumberOnDisk() {
|
||||
String[] kids = dir.list();
|
||||
if (kids == null) {
|
||||
// the job may have just been created
|
||||
kids = EMPTY_STRING_ARRAY;
|
||||
}
|
||||
List<String> buildDirs = new ArrayList<String>();
|
||||
FilenameFilter buildDirFilter = createDirectoryFilter();
|
||||
SortedIntList list = new SortedIntList(kids.length / 2);
|
||||
for (String s : kids) {
|
||||
if (buildDirFilter.accept(dir, s)) {
|
||||
buildDirs.add(s);
|
||||
} else {
|
||||
try {
|
||||
list.add(Integer.parseInt(s));
|
||||
} catch (NumberFormatException e) {
|
||||
// this isn't a shortcut
|
||||
}
|
||||
try {
|
||||
list.add(Integer.parseInt(s));
|
||||
} catch (NumberFormatException e) {
|
||||
// this isn't a build dir
|
||||
}
|
||||
}
|
||||
Collections.sort(buildDirs);
|
||||
idOnDisk = new SortedList<String>(buildDirs);
|
||||
list.sort();
|
||||
numberOnDisk = list;
|
||||
}
|
||||
@@ -263,13 +206,9 @@ public abstract class AbstractLazyLoadRunMap<R> extends AbstractMap<Integer,R> i
|
||||
return COMPARATOR;
|
||||
}
|
||||
|
||||
/**
|
||||
* If we have non-zero R in memory, we can return false right away.
|
||||
* If we have zero R in memory, try loading one and see if we can find something.
|
||||
*/
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return index.byId.isEmpty() && search(Integer.MAX_VALUE, DESC)==null;
|
||||
return search(Integer.MAX_VALUE, DESC)==null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -349,7 +288,7 @@ public abstract class AbstractLazyLoadRunMap<R> extends AbstractMap<Integer,R> i
|
||||
}
|
||||
|
||||
public R get(int n) {
|
||||
return search(n, Direction.EXACT);
|
||||
return getByNumber(n);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -367,184 +306,60 @@ public abstract class AbstractLazyLoadRunMap<R> extends AbstractMap<Integer,R> i
|
||||
* If DESC, finds the closest #M that satisfies M<=N.
|
||||
*/
|
||||
public @CheckForNull R search(final int n, final Direction d) {
|
||||
Entry<Integer, BuildReference<R>> c = index.ceilingEntry(n);
|
||||
if (c!=null && c.getKey()== n) {
|
||||
R r = c.getValue().get();
|
||||
if (r!=null)
|
||||
return r; // found the exact #n
|
||||
}
|
||||
|
||||
// at this point we know that we don't have #n loaded yet
|
||||
|
||||
{// check numberOnDisk as a cache to see if we can find it there
|
||||
int npos = numberOnDisk.find(n);
|
||||
if (npos>=0) {// found exact match
|
||||
R r = load(numberOnDisk.get(npos), null);
|
||||
if (r!=null)
|
||||
return r;
|
||||
}
|
||||
|
||||
switch (d) {
|
||||
case ASC:
|
||||
case DESC:
|
||||
// didn't find the exact match, but what's the nearest ascending value in the cache?
|
||||
int neighbor = (d==ASC?HIGHER:LOWER).apply(npos);
|
||||
if (numberOnDisk.isInRange(neighbor)) {
|
||||
R r = getByNumber(numberOnDisk.get(neighbor));
|
||||
if (r!=null) {
|
||||
// make sure that the cache is accurate by looking at the previous ID
|
||||
// and it actually satisfies the constraint
|
||||
int prev = (d==ASC?LOWER:HIGHER).apply(idOnDisk.find(getIdOf(r)));
|
||||
if (idOnDisk.isInRange(prev)) {
|
||||
R pr = getById(idOnDisk.get(prev));
|
||||
// sign*sign is making sure that #pr and #r sandwiches #n.
|
||||
if (pr!=null && signOfCompare(getNumberOf(pr),n)*signOfCompare(n,getNumberOf(r))>0)
|
||||
return r;
|
||||
else {
|
||||
// cache is lying. there's something fishy.
|
||||
// ignore the cache and do the slow search
|
||||
}
|
||||
} else {
|
||||
// r is the build with youngest ID
|
||||
return r;
|
||||
}
|
||||
} else {
|
||||
// cache says we should have a build but we didn't.
|
||||
// ignore the cache and do the slow search
|
||||
}
|
||||
}
|
||||
break;
|
||||
case EXACT:
|
||||
// fall through
|
||||
}
|
||||
|
||||
// didn't find it in the cache, but don't give up yet
|
||||
// maybe the cache just doesn't exist.
|
||||
// so fall back to the slow search
|
||||
}
|
||||
|
||||
// capture the snapshot and work off with it since it can be overwritten by other threads
|
||||
SortedList<String> idOnDisk = this.idOnDisk;
|
||||
boolean clonedIdOnDisk = false; // if we modify idOnDisk we need to write it back. this flag is set to true when we overwrit idOnDisk local var
|
||||
|
||||
// slow path: we have to find the build from idOnDisk by guessing ID of the build.
|
||||
// first, narrow down the candidate IDs to try by using two known number-to-ID mapping
|
||||
if (idOnDisk.isEmpty()) return null;
|
||||
|
||||
Entry<Integer, BuildReference<R>> f = index.floorEntry(n);
|
||||
|
||||
// if bound is null, use a sentinel value
|
||||
String cid = c==null ? "\u0000" : c.getValue().id;
|
||||
String fid = f==null ? "\uFFFF" : f.getValue().id;
|
||||
// at this point, #n must be in (cid,fid)
|
||||
|
||||
// We know that the build we are looking for exists in [lo,hi) --- it's "hi)" and not "hi]" because we do +1.
|
||||
// we will narrow this down via binary search
|
||||
final int initialSize = idOnDisk.size();
|
||||
int lo = idOnDisk.higher(cid);
|
||||
int hi = idOnDisk.lower(fid)+1;
|
||||
|
||||
final int initialLo = lo, initialHi = hi;
|
||||
|
||||
if (!(0<=lo && lo<=hi && hi<=idOnDisk.size())) {
|
||||
// assertion error, but we are so far unable to get to the bottom of this bug.
|
||||
// but don't let this kill the loading the hard way
|
||||
String msg = String.format(
|
||||
"JENKINS-15652 Assertion error #1: failing to load %s #%d %s: lo=%d,hi=%d,size=%d,size2=%d",
|
||||
dir, n, d, lo, hi, idOnDisk.size(), initialSize);
|
||||
LOGGER.log(Level.WARNING, msg);
|
||||
return null;
|
||||
}
|
||||
|
||||
while (lo<hi) {
|
||||
final int pivot = (lo+hi)/2;
|
||||
if (!(0<=lo && lo<=pivot && pivot<hi && hi<=idOnDisk.size())) {
|
||||
// assertion error, but we are so far unable to get to the bottom of this bug.
|
||||
// but don't let this kill the loading the hard way
|
||||
String msg = String.format(
|
||||
"JENKINS-15652 Assertion error #2: failing to load %s #%d %s: lo=%d,hi=%d,pivot=%d,size=%d (initial:lo=%d,hi=%d,size=%d)",
|
||||
dir, n, d, lo, hi, pivot, idOnDisk.size(), initialLo, initialHi, initialSize);
|
||||
LOGGER.log(Level.WARNING, msg);
|
||||
return null;
|
||||
}
|
||||
R r = load(idOnDisk.get(pivot), null);
|
||||
if (r==null) {
|
||||
// this ID isn't valid. get rid of that and retry pivot
|
||||
hi--;
|
||||
if (!clonedIdOnDisk) {// if we are making an edit, we need to own a copy
|
||||
idOnDisk = new SortedList<String>(idOnDisk);
|
||||
clonedIdOnDisk = true;
|
||||
}
|
||||
idOnDisk.remove(pivot);
|
||||
continue;
|
||||
}
|
||||
|
||||
int found = getNumberOf(r);
|
||||
if (found==n)
|
||||
return r; // exact match
|
||||
|
||||
if (found<n) lo = pivot+1; // the pivot was too small. look in the upper half
|
||||
else hi = pivot; // the pivot was too big. look in the lower half
|
||||
}
|
||||
|
||||
if (clonedIdOnDisk)
|
||||
this.idOnDisk = idOnDisk; // feedback the modified result atomically
|
||||
|
||||
assert lo==hi;
|
||||
// didn't find the exact match
|
||||
// both lo and hi point to the insertion point on idOnDisk
|
||||
switch (d) {
|
||||
case ASC:
|
||||
if (hi==idOnDisk.size()) return null;
|
||||
return getById(idOnDisk.get(hi));
|
||||
case DESC:
|
||||
if (lo<=0) return null;
|
||||
if (lo-1>=idOnDisk.size()) {
|
||||
// assertion error, but we are so far unable to get to the bottom of this bug.
|
||||
// but don't let this kill the loading the hard way
|
||||
LOGGER.log(Level.WARNING, String.format(
|
||||
"JENKINS-15652 Assertion error #3: failing to load %s #%d %s: lo=%d,hi=%d,size=%d (initial:lo=%d,hi=%d,size=%d)",
|
||||
dir, n,d,lo,hi,idOnDisk.size(), initialLo,initialHi,initialSize));
|
||||
return null;
|
||||
}
|
||||
return getById(idOnDisk.get(lo-1));
|
||||
case EXACT:
|
||||
if (hi<=0) return null;
|
||||
R r = getById(idOnDisk.get(hi-1));
|
||||
if (r==null) return null;
|
||||
|
||||
int found = getNumberOf(r);
|
||||
if (found==n)
|
||||
return r; // exact match
|
||||
return getByNumber(n);
|
||||
case ASC:
|
||||
for (int m : numberOnDisk) {
|
||||
if (m < n) {
|
||||
// TODO could be made more efficient with numberOnDisk.find
|
||||
continue;
|
||||
}
|
||||
R r = getByNumber(m);
|
||||
if (r != null) {
|
||||
return r;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
case DESC:
|
||||
// TODO again could be made more efficient
|
||||
List<Integer> reversed = new ArrayList<Integer>(numberOnDisk);
|
||||
Collections.reverse(reversed);
|
||||
for (int m : reversed) {
|
||||
if (m > n) {
|
||||
continue;
|
||||
}
|
||||
R r = getByNumber(m);
|
||||
if (r != null) {
|
||||
return r;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
default:
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* sign of (a-b).
|
||||
*/
|
||||
private static int signOfCompare(int a, int b) {
|
||||
if (a>b) return 1;
|
||||
if (a<b) return -1;
|
||||
return 0;
|
||||
public R getById(String id) {
|
||||
return getByNumber(Integer.parseInt(id));
|
||||
}
|
||||
|
||||
public R getById(String id) {
|
||||
public R getByNumber(int n) {
|
||||
Index snapshot = index;
|
||||
if (snapshot.byId.containsKey(id)) {
|
||||
BuildReference<R> ref = snapshot.byId.get(id);
|
||||
if (snapshot.byNumber.containsKey(n)) {
|
||||
BuildReference<R> ref = snapshot.byNumber.get(n);
|
||||
if (ref==null) return null; // known failure
|
||||
R v = unwrap(ref);
|
||||
if (v!=null) return v; // already in memory
|
||||
// otherwise fall through to load
|
||||
}
|
||||
return load(id,null);
|
||||
return load(n, null);
|
||||
}
|
||||
|
||||
public R getByNumber(int n) {
|
||||
return search(n, Direction.EXACT);
|
||||
protected final synchronized void proposeNewNumber(int number) throws IllegalStateException {
|
||||
if (numberOnDisk.isInRange(numberOnDisk.ceil(number))) {
|
||||
throw new IllegalStateException("cannot create a build with number " + number + " since that (or higher) is already in use among " + numberOnDisk);
|
||||
}
|
||||
}
|
||||
|
||||
public R put(R value) {
|
||||
@@ -557,27 +372,13 @@ public abstract class AbstractLazyLoadRunMap<R> extends AbstractMap<Integer,R> i
|
||||
|
||||
@Override
|
||||
public synchronized R put(Integer key, R r) {
|
||||
String id = getIdOf(r);
|
||||
int n = getNumberOf(r);
|
||||
|
||||
Index copy = copy();
|
||||
BuildReference<R> ref = createReference(r);
|
||||
BuildReference<R> old = copy.byId.put(id,ref);
|
||||
copy.byNumber.put(n,ref);
|
||||
BuildReference<R> old = copy.byNumber.put(n,ref);
|
||||
index = copy;
|
||||
|
||||
/*
|
||||
search relies on the fact that every object added via
|
||||
put() method be available in the xyzOnDisk index, so I'm adding them here
|
||||
however, this is awfully inefficient. I wonder if there's any better way to do this?
|
||||
*/
|
||||
if (!idOnDisk.contains(id)) {
|
||||
ArrayList<String> a = new ArrayList<String>(idOnDisk);
|
||||
a.add(id);
|
||||
Collections.sort(a);
|
||||
idOnDisk = new SortedList<String>(a);
|
||||
}
|
||||
|
||||
if (!numberOnDisk.contains(n)) {
|
||||
SortedIntList a = new SortedIntList(numberOnDisk);
|
||||
a.add(n);
|
||||
@@ -598,9 +399,7 @@ public abstract class AbstractLazyLoadRunMap<R> extends AbstractMap<Integer,R> i
|
||||
public synchronized void putAll(Map<? extends Integer,? extends R> rhs) {
|
||||
Index copy = copy();
|
||||
for (R r : rhs.values()) {
|
||||
String id = getIdOf(r);
|
||||
BuildReference<R> ref = createReference(r);
|
||||
copy.byId.put(id,ref);
|
||||
copy.byNumber.put(getNumberOf(r),ref);
|
||||
}
|
||||
index = copy;
|
||||
@@ -620,9 +419,9 @@ public abstract class AbstractLazyLoadRunMap<R> extends AbstractMap<Integer,R> i
|
||||
synchronized (this) {
|
||||
if (!fullyLoaded) {
|
||||
Index copy = copy();
|
||||
for (String id : idOnDisk) {
|
||||
if (!copy.byId.containsKey(id))
|
||||
load(id,copy);
|
||||
for (Integer number : numberOnDisk) {
|
||||
if (!copy.byNumber.containsKey(number))
|
||||
load(number, copy);
|
||||
}
|
||||
index = copy;
|
||||
fullyLoaded = true;
|
||||
@@ -640,42 +439,18 @@ public abstract class AbstractLazyLoadRunMap<R> extends AbstractMap<Integer,R> i
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to load the record #N by using the shortcut.
|
||||
* Tries to load the record #N.
|
||||
*
|
||||
* @return null if the data failed to load.
|
||||
*/
|
||||
protected R load(int n, Index editInPlace) {
|
||||
R r = null;
|
||||
File shortcut = new File(dir,String.valueOf(n));
|
||||
if (shortcut.isDirectory()) {
|
||||
synchronized (this) {
|
||||
r = load(shortcut,editInPlace);
|
||||
|
||||
// make sure what we actually loaded is #n,
|
||||
// because the shortcuts can lie.
|
||||
if (r!=null && getNumberOf(r)!=n)
|
||||
r = null;
|
||||
|
||||
if (r==null) {
|
||||
// if failed to locate, record that fact
|
||||
SortedIntList update = new SortedIntList(numberOnDisk);
|
||||
update.removeValue(n);
|
||||
numberOnDisk = update;
|
||||
}
|
||||
}
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
|
||||
protected R load(String id, Index editInPlace) {
|
||||
assert dir != null;
|
||||
R v = load(new File(dir, id), editInPlace);
|
||||
R v = load(new File(dir, String.valueOf(n)), editInPlace);
|
||||
if (v==null && editInPlace!=null) {
|
||||
// remember the failure.
|
||||
// if editInPlace==null, we can create a new copy for this, but not sure if it's worth doing,
|
||||
// given that we also update idOnDisk anyway.
|
||||
editInPlace.byId.put(id,null);
|
||||
// TODO should we also update numberOnDisk?
|
||||
editInPlace.byNumber.put(n, null);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
@@ -692,9 +467,7 @@ public abstract class AbstractLazyLoadRunMap<R> extends AbstractMap<Integer,R> i
|
||||
|
||||
Index copy = editInPlace!=null ? editInPlace : new Index(index);
|
||||
|
||||
String id = getIdOf(r);
|
||||
BuildReference<R> ref = createReference(r);
|
||||
copy.byId.put(id,ref);
|
||||
copy.byNumber.put(getNumberOf(r),ref);
|
||||
|
||||
if (editInPlace==null) index = copy;
|
||||
@@ -710,10 +483,13 @@ public abstract class AbstractLazyLoadRunMap<R> extends AbstractMap<Integer,R> i
|
||||
* Subtype to provide {@link Run#getNumber()} so that this class doesn't have to depend on it.
|
||||
*/
|
||||
protected abstract int getNumberOf(R r);
|
||||
|
||||
/**
|
||||
* Subtype to provide {@link Run#getId()} so that this class doesn't have to depend on it.
|
||||
*/
|
||||
protected abstract String getIdOf(R r);
|
||||
protected String getIdOf(R r) {
|
||||
return String.valueOf(getNumberOf(r));
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow subtype to capture a reference.
|
||||
@@ -737,11 +513,10 @@ public abstract class AbstractLazyLoadRunMap<R> extends AbstractMap<Integer,R> i
|
||||
public synchronized boolean removeValue(R run) {
|
||||
Index copy = copy();
|
||||
int n = getNumberOf(run);
|
||||
copy.byNumber.remove(n);
|
||||
BuildReference<R> old = copy.byNumber.remove(n);
|
||||
SortedIntList a = new SortedIntList(numberOnDisk);
|
||||
a.removeValue(n);
|
||||
numberOnDisk = a;
|
||||
BuildReference<R> old = copy.byId.remove(getIdOf(run));
|
||||
this.index = copy;
|
||||
|
||||
entrySet.clearCache();
|
||||
@@ -755,9 +530,7 @@ public abstract class AbstractLazyLoadRunMap<R> extends AbstractMap<Integer,R> i
|
||||
public synchronized void reset(TreeMap<Integer,R> builds) {
|
||||
Index index = new Index();
|
||||
for (R r : builds.values()) {
|
||||
String id = getIdOf(r);
|
||||
BuildReference<R> ref = createReference(r);
|
||||
index.byId.put(id,ref);
|
||||
index.byNumber.put(getNumberOf(r),ref);
|
||||
}
|
||||
|
||||
@@ -774,14 +547,9 @@ public abstract class AbstractLazyLoadRunMap<R> extends AbstractMap<Integer,R> i
|
||||
return o==this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists the actual data directory
|
||||
*/
|
||||
protected abstract FilenameFilter createDirectoryFilter();
|
||||
|
||||
private static final Comparator<Comparable> COMPARATOR = new Comparator<Comparable>() {
|
||||
public int compare(Comparable o1, Comparable o2) {
|
||||
return -o1.compareTo(o2);
|
||||
private static final Comparator<Integer> COMPARATOR = new Comparator<Integer>() {
|
||||
@Override public int compare(Integer o1, Integer o2) {
|
||||
return o2 - o1;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ 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.
|
||||
@@ -63,9 +64,6 @@ public abstract class LazyBuildMixIn<JobT extends Job<JobT,RunT> & Queue.Task &
|
||||
@SuppressWarnings("deprecation") // [JENKINS-15156] builds accessed before onLoad or onCreatedFromScratch called
|
||||
private @Nonnull RunMap<RunT> builds = new RunMap<RunT>();
|
||||
|
||||
// keep track of the previous time we started a build
|
||||
private long lastBuildStartTime;
|
||||
|
||||
/**
|
||||
* Initializes this mixin.
|
||||
* Call this from a constructor and {@link AbstractItem#onLoad} to make sure it is always initialized.
|
||||
@@ -92,14 +90,14 @@ public abstract class LazyBuildMixIn<JobT extends Job<JobT,RunT> & Queue.Task &
|
||||
}
|
||||
|
||||
/**
|
||||
* Something to be called from {@link AbstractItem#onCreatedFromScratch}.
|
||||
* Something to be called from {@link Job#onCreatedFromScratch}.
|
||||
*/
|
||||
public final void onCreatedFromScratch() {
|
||||
builds = createBuildRunMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* Something to be called from {@link AbstractItem#onLoad}.
|
||||
* Something to be called from {@link Job#onLoad}.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public void onLoad(ItemGroup<? extends Item> parent, String name) throws IOException {
|
||||
@@ -123,7 +121,8 @@ public abstract class LazyBuildMixIn<JobT extends Job<JobT,RunT> & Queue.Task &
|
||||
// if we are reloading, keep all those that are still building intact
|
||||
for (RunT r : currentBuilds.getLoadedBuilds().values()) {
|
||||
if (r.isBuilding()) {
|
||||
_builds.put(r);
|
||||
// Do not use RunMap.put(Run):
|
||||
_builds.put(r.getNumber(), r);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -131,11 +130,15 @@ public abstract class LazyBuildMixIn<JobT extends Job<JobT,RunT> & Queue.Task &
|
||||
}
|
||||
|
||||
private RunMap<RunT> createBuildRunMap() {
|
||||
return new RunMap<RunT>(asJob().getBuildDir(), new RunMap.Constructor<RunT>() {
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -169,19 +172,7 @@ public abstract class LazyBuildMixIn<JobT extends Job<JobT,RunT> & Queue.Task &
|
||||
* Calls the ({@link Job}) constructor of {@link #getBuildClass}.
|
||||
* Suitable for {@link SubTask#createExecutable}.
|
||||
*/
|
||||
@SuppressWarnings("SleepWhileHoldingLock")
|
||||
@edu.umd.cs.findbugs.annotations.SuppressWarnings("SWL_SLEEP_WITH_LOCK_HELD")
|
||||
public final synchronized RunT 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 {
|
||||
RunT lastBuild = getBuildClass().getConstructor(asJob().getClass()).newInstance(asJob());
|
||||
builds.put(lastBuild);
|
||||
|
||||
Reference in new issue
Block a user