diff --git a/changelog.html b/changelog.html index 0258cd434882036534174dcf1a97b296bd880d2f..fc6b0caa8df76cabb0519844e7b97511677aa843 100644 --- a/changelog.html +++ b/changelog.html @@ -55,7 +55,9 @@ Upcoming changes 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();