diff --git a/changelog.html b/changelog.html
index 0258cd434882036534174dcf1a97b296bd880d2f..fc6b0caa8df76cabb0519844e7b97511677aa843 100644
--- a/changelog.html
+++ b/changelog.html
@@ -55,7 +55,9 @@ Upcoming changes
- -
+
-
+ Clean up fingerprint records that correspond to the deleted build recods
+ (issue 18417)
diff --git a/core/src/main/java/hudson/model/Fingerprint.java b/core/src/main/java/hudson/model/Fingerprint.java
index bf3b14cf8cd49567127511543b979c94fe777d54..4de1899d00f7ed49bd197375e095a3d9e2e1286f 100644
--- a/core/src/main/java/hudson/model/Fingerprint.java
+++ b/core/src/main/java/hudson/model/Fingerprint.java
@@ -249,6 +249,13 @@ public class Fingerprint implements ModelObject, Saveable {
return this.end intersection = new ArrayList();
+
+ int lhs=0,rhs=0;
+ while(lhs sub = new ArrayList();
+
+ int lhs=0,rhs=0;
+ while(lhs e : new Hashtable(usages).entrySet()) {// copy because we mutate
+ Job j = Jenkins.getInstance().getItemByFullName(e.getKey(),Job.class);
+ if(j==null) {// no such job any more. recycle the record
+ modified = true;
+ usages.remove(e.getKey());
+ continue;
+ }
+
+ Run firstBuild = j.getFirstBuild();
+ if(firstBuild==null) {// no builds. recycle the whole record
+ modified = true;
+ usages.remove(e.getKey());
+ continue;
+ }
+
+ RangeSet cur = e.getValue();
+
+ // builds that are around without the keepLog flag on are normally clustered together (in terms of build #)
+ // so our basic strategy is to discard everything up to the first ephemeral build, except those builds
+ // that are marked as kept
+ RangeSet kept = new RangeSet();
+ Run r = firstBuild;
+ while (r!=null && r.isKeepLog()) {
+ kept.add(r.getNumber());
+ r = r.getNextBuild();
+ }
+
+ if (r==null) {
+ // all the build records are permanently kept ones, so we'll just have to keep 'kept' out of whatever currently in 'cur'
+ modified |= cur.retainAll(kept);
+ } else {
+ // otherwise we are ready to discard [0,r.number) except those marked as 'kept'
+ RangeSet discarding = new RangeSet(new Range(-1,r.getNumber()));
+ discarding.removeAll(kept);
+ modified |= cur.removeAll(discarding);
+ }
+
+ if (cur.isEmpty()) {
+ usages.remove(e.getKey());
+ modified = true;
+ }
+ }
+
+ if (modified)
+ save();
+
+ return modified;
+ }
+
+
/**
* Gets the associated {@link FingerprintFacet}s.
*
diff --git a/core/src/main/java/hudson/model/FingerprintCleanupThread.java b/core/src/main/java/hudson/model/FingerprintCleanupThread.java
index e70037ab57e31b087b5942808920263da5c436f6..1b0dc37278e7121a7c6bf8314edaf3deed60daa7 100644
--- a/core/src/main/java/hudson/model/FingerprintCleanupThread.java
+++ b/core/src/main/java/hudson/model/FingerprintCleanupThread.java
@@ -61,7 +61,7 @@ public final class FingerprintCleanupThread extends AsyncPeriodicWork {
return Jenkins.getInstance().getExtensionList(AsyncPeriodicWork.class).get(FingerprintCleanupThread.class);
}
- protected void execute(TaskListener listener) {
+ public void execute(TaskListener listener) {
int numFiles = 0;
File root = new File(Jenkins.getInstance().getRootDir(),"fingerprints");
@@ -103,11 +103,16 @@ public final class FingerprintCleanupThread extends AsyncPeriodicWork {
if(!fp.isAlive()) {
fingerprintFile.delete();
return true;
+ } else {
+ // get the fingerprint in the official map so have the changes visible to Jenkins
+ // otherwise the mutation made in FingerprintMap can override our trimming.
+ fp = Jenkins.getInstance()._getFingerprint(fp.getHashString());
+ return fp.trim();
}
} catch (IOException e) {
logger.log(Level.WARNING, "Failed to process "+fingerprintFile, e);
+ return false;
}
- return false;
}
private static final FileFilter LENGTH2DIR_FILTER = new FileFilter() {
diff --git a/core/src/main/java/hudson/tasks/Fingerprinter.java b/core/src/main/java/hudson/tasks/Fingerprinter.java
index d62e500fff7f6fc17c255f50e2086d7e14d0cee4..21317f5f05923eb67200ce91df7c454fcf662d85 100644
--- a/core/src/main/java/hudson/tasks/Fingerprinter.java
+++ b/core/src/main/java/hudson/tasks/Fingerprinter.java
@@ -158,10 +158,8 @@ public class Fingerprinter extends Recorder implements Serializable, DependencyD
for ( ListIterator iter = builds.listIterator(); iter.hasNext(); ) {
Run build = (Run) iter.next();
- List fingerprints = build.getActions(FingerprintAction.class);
- for (FingerprintAction action : fingerprints) {
- Map deps = action.getDependencies();
- for (AbstractProject key : deps.keySet()) {
+ for (FingerprintAction action : build.getActions(FingerprintAction.class)) {
+ for (AbstractProject key : action.getDependencies().keySet()) {
if (key == owner) {
continue; // Avoid self references
}
diff --git a/core/src/test/java/hudson/model/FingerprintTest.java b/core/src/test/java/hudson/model/FingerprintTest.java
index 296cb77fdf5362c70f6e08c37d617a1f4ec682ff..da1ef4e9fb3fbe44cd3e9850e82c3ffa81e8dadb 100644
--- a/core/src/test/java/hudson/model/FingerprintTest.java
+++ b/core/src/test/java/hudson/model/FingerprintTest.java
@@ -123,6 +123,91 @@ public class FingerprintTest {
assertEquals("[1,2),[3,4),[5,6),[7,8)",x.toString());
}
+ @Test
+ public void retainAll1() {
+ RangeSet x = new RangeSet();
+ RangeSet y = new RangeSet();
+
+ x.addAll(1,2,3, 10,11, 20);
+ y.addAll( 2, 11,12, 19,20,21);
+
+ assertTrue(x.retainAll(y));
+
+ RangeSet z = new RangeSet();
+ z.addAll(2,11,20);
+
+ assertEquals(x,z);
+ }
+
+ @Test
+ public void retainAll2() {
+ RangeSet x = new RangeSet();
+ RangeSet y = new RangeSet();
+
+ x.addAll(1,2,3,4,5,6,7,8,9,10, 13,14,15,16,17,18,19,20);
+ y.addAll( 2,3, 5,6, 9,10,11,12,13, 15,16, 18,19);
+
+ assertTrue(x.retainAll(y));
+
+ RangeSet z = new RangeSet();
+ z.addAll(2,3,5,6,9,10,13,15,16,18,19);
+
+ assertEquals(x,z);
+ }
+
+ @Test
+ public void retainAll3() {
+ RangeSet x = new RangeSet();
+ RangeSet y = new RangeSet();
+
+ x.addAll(1,2,3,4,5);
+
+ assertTrue(x.retainAll(y));
+ assertTrue(x.isEmpty());
+ }
+
+ @Test
+ public void removeAll1() {
+ RangeSet x = new RangeSet();
+ RangeSet y = new RangeSet();
+
+ x.addAll(1,2,3, 10,11, 20);
+ y.addAll( 2, 11,12, 19,20,21);
+
+ assertTrue(x.removeAll(y));
+
+ RangeSet z = new RangeSet();
+ z.addAll(1,3,10);
+
+ assertEquals(x,z);
+ }
+
+ @Test
+ public void removeAll2() {
+ RangeSet x = new RangeSet();
+ RangeSet y = new RangeSet();
+
+ x.addAll(1,2,3,4,5,6,7,8,9,10, 13,14,15,16,17,18,19,20);
+ y.addAll( 2,3, 5,6, 9,10,11,12,13, 15,16, 18,19);
+
+ assertTrue(x.removeAll(y));
+
+ RangeSet z = new RangeSet();
+ z.addAll(1,4,7,8,14,17,20);
+
+ assertEquals(x,z);
+ }
+
+ @Test
+ public void removeAll3() {
+ RangeSet x = new RangeSet();
+ RangeSet y = new RangeSet();
+
+ x.addAll(1,2,3,4,5);
+
+ assertFalse(x.removeAll(y));
+ }
+
@Test public void deserialize() throws Exception {
assertEquals("Fingerprint["
+ "original=stapler/org.kohsuke.stapler:stapler-jelly #123,"
diff --git a/test/src/test/java/hudson/tasks/FingerprinterTest.java b/test/src/test/java/hudson/tasks/FingerprinterTest.java
index d2f0b2eb233e92e1eb0e4acf9806d62347f6b637..f508d4131b972fa65e5d64377ea22fd5ac609110 100644
--- a/test/src/test/java/hudson/tasks/FingerprinterTest.java
+++ b/test/src/test/java/hudson/tasks/FingerprinterTest.java
@@ -24,12 +24,14 @@
package hudson.tasks;
+import hudson.Util;
import hudson.XmlFile;
import hudson.matrix.Axis;
import hudson.matrix.AxisList;
import hudson.matrix.MatrixProject;
import hudson.model.AbstractProject;
import hudson.model.Fingerprint;
+import hudson.model.FingerprintCleanupThread;
import hudson.model.FreeStyleBuild;
import hudson.model.FreeStyleProject;
import hudson.model.Hudson;
@@ -38,9 +40,13 @@ import hudson.util.RunList;
import java.io.File;
import java.io.IOException;
+import java.util.Arrays;
import java.util.Collection;
+import java.util.HashSet;
import java.util.List;
import static org.junit.Assert.*;
+
+import hudson.util.StreamTaskListener;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
@@ -275,6 +281,50 @@ public class FingerprinterTest {
assertEquals(build, action.getBuild());
assertEquals("{a=f31efcf9afe30617d6c46b919e702822}", action.getRecords().toString());
}
+
+ @SuppressWarnings("unchecked")
+ @Bug(18417)
+ @Test
+ public void fingerprintCleanup() throws Exception {
+ // file names shouldn't matter
+ FreeStyleProject p1 = createFreeStyleProjectWithFingerprints(singleContents, singleFiles);
+ FreeStyleProject p2 = createFreeStyleProjectWithFingerprints(singleContents, singleFiles2);
+ FreeStyleProject p3 = createFreeStyleProjectWithFingerprints(singleContents, singleFiles);
+
+ j.assertBuildStatusSuccess(p1.scheduleBuild2(0));
+ j.assertBuildStatusSuccess(p2.scheduleBuild2(0));
+ j.assertBuildStatusSuccess(p3.scheduleBuild2(0));
+
+ Fingerprint f = j.jenkins._getFingerprint(Util.getDigestOf(singleContents[0]+"\n"));
+ assertEquals(3,f.getUsages().size());
+
+ assertEquals(Arrays.asList(p1), p2.getUpstreamProjects());
+ assertEquals(Arrays.asList(p1), p3.getUpstreamProjects());
+ assertEquals(new HashSet(Arrays.asList(p2,p3)), new HashSet(p1.getDownstreamProjects()));
+
+ // discard the p3 records
+ p3.delete();
+ new FingerprintCleanupThread().execute(StreamTaskListener.fromStdout());
+
+ // records for p3 should have been deleted now
+ assertEquals(2,f.getUsages().size());
+ assertEquals(Arrays.asList(p1), p2.getUpstreamProjects());
+ assertEquals(Arrays.asList(p2), p1.getDownstreamProjects());
+
+
+ // do a new build in p2 #2 that points to a separate fingerprints
+ p2.getBuildersList().clear();
+ p2.getPublishersList().clear();
+ addFingerprinterToProject(p2,singleContents2,singleFiles2);
+ j.assertBuildStatusSuccess(p2.scheduleBuild2(0));
+
+ // another garbage collection that gets rid of p2 records from the fingerprint
+ p2.getBuildByNumber(1).delete();
+ new FingerprintCleanupThread().execute(StreamTaskListener.fromStdout());
+
+ assertEquals(1,f.getUsages().size());
+ }
+
private FreeStyleProject createFreeStyleProjectWithFingerprints(String[] contents, String[] files) throws IOException, Exception {
FreeStyleProject project = j.createFreeStyleProject();