diff --git a/core/src/main/java/hudson/model/PermalinkProjectAction.java b/core/src/main/java/hudson/model/PermalinkProjectAction.java index 7909040f654d09bd49cc0f9087a723b6a0de22b3..26273b38d6191c3269e296eba434b66917e70a58 100644 --- a/core/src/main/java/hudson/model/PermalinkProjectAction.java +++ b/core/src/main/java/hudson/model/PermalinkProjectAction.java @@ -27,6 +27,7 @@ import jenkins.model.PeepholePermalink; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; +import javax.annotation.CheckForNull; /** * Optional interface for {@link Action}s that are attached @@ -86,7 +87,7 @@ public interface PermalinkProjectAction extends Action { * @return null * if the target of the permalink doesn't exist. */ - public abstract Run resolve(Job job); + public abstract @CheckForNull Run resolve(Job job); /** * List of {@link Permalink}s that are built into Jenkins. diff --git a/core/src/main/java/hudson/model/Run.java b/core/src/main/java/hudson/model/Run.java index 24243946b0ff9f7f85b9dfe03fee8ece395af303..a7d85f673561d1f7fe2e854ed762f1d4e770a004 100644 --- a/core/src/main/java/hudson/model/Run.java +++ b/core/src/main/java/hudson/model/Run.java @@ -107,7 +107,6 @@ import jenkins.model.ArtifactManagerFactory; import jenkins.model.BuildDiscarder; import jenkins.model.Jenkins; import jenkins.model.JenkinsLocationConfiguration; -import jenkins.model.PeepholePermalink; import jenkins.model.RunAction2; import jenkins.model.StandardArtifactManager; import jenkins.model.lazy.BuildReference; @@ -1813,8 +1812,6 @@ public abstract class Run ,RunT extends Run,RunT extends Run * This base class provides a file-based caching mechanism that avoids - * walking the long build history. The cache is a symlink to the build directory - * where symlinks are supported, and text file that contains the build number otherwise. + * walking the long build history. * *

* The implementation transparently tolerates G(B) that goes from true to false over time @@ -56,13 +54,17 @@ import org.apache.commons.io.FileUtils; * from false to true, then call {@link #resolve(Job)} to check the current permalink target * is up to date, then call {@link #updateCache(Job, Run)} if it needs updating. * - * @author Kohsuke Kawaguchi * @since 1.507 */ public abstract class PeepholePermalink extends Permalink implements Predicate> { - /** JENKINS-22822: avoids rereading symlinks */ - static final Map symlinks = new HashMap<>(); + /** + * JENKINS-22822: avoids rereading caches. + * Top map keys are {@link builds} directories. + * Inner maps are from permalink name to build number. + * Synchronization is first on the outer map, then on the inner. + */ + private static final Map> caches = new HashMap<>(); /** * Checks if the given build satisfies the peep-hole criteria. @@ -71,9 +73,8 @@ public abstract class PeepholePermalink extends Permalink implements Predicate run); - /** - * The file in which the permalink target gets recorded. - */ + /** @deprecated No longer used. */ + @Deprecated protected File getPermalinkFile(Job job) { return new File(job.getBuildDir(),getId()); } @@ -83,32 +84,27 @@ public abstract class PeepholePermalink extends Permalink implements Predicate resolve(Job job) { - File f = getPermalinkFile(job); - Run b=null; - - try { - String target = readSymlink(f); - if (target!=null) { - int n = Integer.parseInt(Util.getFileName(target)); - if (n==RESOLVES_TO_NONE) return null; - - b = job.getBuildByNumber(n); - if (b!=null && apply(b)) - return b; // found it (in the most efficient way possible) - - // the cache is stale. start the search - if (b==null) - b=job.getNearestOldBuild(n); + Map cache = cacheFor(job.getBuildDir()); + int n; + synchronized (cache) { + n = cache.getOrDefault(getId(), 0); + } + if (n == RESOLVES_TO_NONE) { + return null; + } + Run b; + if (n > 0) { + b = job.getBuildByNumber(n); + if (b != null && apply(b)) { + return b; // found it (in the most efficient way possible) } - } catch (InterruptedException e) { - LOGGER.log(Level.WARNING, "Failed to read permalink cache:" + f, e); - // if we fail to read the cache, fall back to the re-computation - } catch (NumberFormatException e) { - LOGGER.log(Level.WARNING, "Failed to parse the build number in the permalink cache:" + f, e); - // if we fail to read the cache, fall back to the re-computation - } catch (IOException e) { - // this happens when the symlink doesn't exist - // (and it cannot be distinguished from the case when the actual I/O error happened + } else { + b = null; + } + + // the cache is stale. start the search + if (b == null) { + b = job.getNearestOldBuild(n); } if (b==null) { @@ -133,72 +129,71 @@ public abstract class PeepholePermalink extends Permalink implements Predicate job, @Nullable Run b) { - final int n = b==null ? RESOLVES_TO_NONE : b.getNumber(); - - File cache = getPermalinkFile(job); - cache.getParentFile().mkdirs(); - - try { - String target = String.valueOf(n); - if (b != null && !new File(job.getBuildDir(), target).exists()) { - // (re)create the build Number->Id symlink - Util.createSymlink(job.getBuildDir(),b.getId(),target,TaskListener.NULL); + private static @Nonnull Map cacheFor(@Nonnull File buildDir) { + synchronized (caches) { + Map cache = caches.get(buildDir); + if (cache == null) { + cache = load(buildDir); + caches.put(buildDir, cache); } - writeSymlink(cache, target); - } catch (IOException | InterruptedException e) { - LOGGER.log(Level.WARNING, "Failed to update "+job+" "+getId()+" permalink for " + b, e); - cache.delete(); + return cache; } } - // File.exists returns false for a link with a missing target, so for Java 6 compatibility we have to use this circuitous method to see if it was created. - private static boolean exists(File link) { - File[] kids = link.getParentFile().listFiles(); - return kids != null && Arrays.asList(kids).contains(link); - } - - static String readSymlink(File cache) throws IOException, InterruptedException { - synchronized (symlinks) { - String target = symlinks.get(cache); - if (target != null) { - LOGGER.log(Level.FINE, "readSymlink cached {0} → {1}", new Object[] {cache, target}); - return target; + private static @Nonnull Map load(@Nonnull File buildDir) { + Map cache = new TreeMap<>(); + File storage = storageFor(buildDir); + if (storage.isFile()) { + try { + Files.lines(storage.toPath(), StandardCharsets.UTF_8).forEach(line -> { + int idx = line.indexOf(' '); + if (idx == -1) { + return; + } + try { + cache.put(line.substring(0, idx), Integer.parseInt(line.substring(idx + 1))); + } catch (NumberFormatException x) { + LOGGER.log(Level.WARNING, "failed to read " + storage, x); + } + }); + } catch (IOException x) { + LOGGER.log(Level.WARNING, "failed to read " + storage, x); } + LOGGER.fine(() -> "loading from " + storage + ": " + cache); } - String target = Util.resolveSymlink(cache); - if (target==null && cache.exists()) { - // if this file isn't a symlink, it must be a regular file - target = FileUtils.readFileToString(cache,"UTF-8").trim(); - } - LOGGER.log(Level.FINE, "readSymlink {0} → {1}", new Object[] {cache, target}); - synchronized (symlinks) { - symlinks.put(cache, target); - } - return target; + return cache; } - static void writeSymlink(File cache, String target) throws IOException, InterruptedException { - LOGGER.log(Level.FINE, "writeSymlink {0} → {1}", new Object[] {cache, target}); - synchronized (symlinks) { - symlinks.put(cache, target); - } - StringWriter w = new StringWriter(); - StreamTaskListener listener = new StreamTaskListener(w); - Util.createSymlink(cache.getParentFile(),target,cache.getName(),listener); - // Avoid calling resolveSymlink on a nonexistent file as it will probably throw an IOException: - if (!exists(cache) || Util.resolveSymlink(cache)==null) { - // symlink not supported. use a regular file - AtomicFileWriter cw = new AtomicFileWriter(cache); - try { - cw.write(target); - cw.commit(); - } finally { - cw.abort(); - } + static @Nonnull File storageFor(@Nonnull File buildDir) { + return new File(buildDir, "permalinks"); + } + + /** + * Remembers the value 'n' in the cache for future {@link #resolve(Job)}. + */ + protected void updateCache(@Nonnull Job job, @CheckForNull Run b) { + File buildDir = job.getBuildDir(); + Map cache = cacheFor(buildDir); + synchronized (cache) { + cache.put(getId(), b == null ? RESOLVES_TO_NONE : b.getNumber()); + File storage = storageFor(buildDir); + LOGGER.fine(() -> "saving to " + storage + ": " + cache); + try { + AtomicFileWriter cw = new AtomicFileWriter(storage); + try { + for (Map.Entry entry : cache.entrySet()) { + cw.write(entry.getKey()); + cw.write(' '); + cw.write(Integer.toString(entry.getValue())); + cw.write('\n'); + } + cw.commit(); + } finally { + cw.abort(); + } + } catch (IOException x) { + LOGGER.log(Level.WARNING, "failed to update " + storage, x); + } } } @@ -213,8 +208,7 @@ public abstract class PeepholePermalink extends Permalink implements Predicate r = pp.find(run.getPreviousBuild()); - if (LOGGER.isLoggable(Level.FINE)) - LOGGER.fine("Updating "+pp.getPermalinkFile(j).getName()+" permalink from deleted "+run.getNumber()+" to "+(r == null ? -1 : r.getNumber())); + LOGGER.fine(() -> "Updating " + pp.getId() + " permalink from deleted " + run + " to " + (r == null ? -1 : r.getNumber())); pp.updateCache(j,r); } } @@ -230,8 +224,7 @@ public abstract class PeepholePermalink extends Permalink implements Predicate cur = pp.resolve(j); if (cur==null || cur.getNumber() "Updating " + pp.getId() + " permalink to completed " + run); pp.updateCache(j,run); } } diff --git a/core/src/test/java/jenkins/model/PeepholePermalinkTest.java b/core/src/test/java/jenkins/model/PeepholePermalinkTest.java deleted file mode 100644 index fa992e0bb144dc27dccac692721f582cd1d605b9..0000000000000000000000000000000000000000 --- a/core/src/test/java/jenkins/model/PeepholePermalinkTest.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * The MIT License - * - * Copyright 2013 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 java.io.File; -import org.junit.Test; -import static org.junit.Assert.*; -import org.junit.Rule; -import org.junit.rules.TemporaryFolder; -import org.jvnet.hudson.test.Issue; - -public class PeepholePermalinkTest { - - @Rule public TemporaryFolder tmp = new TemporaryFolder(); - - @Issue("JENKINS-17681") - @Test public void symlinks() throws Exception { - File link = new File(tmp.getRoot(), "link"); - PeepholePermalink.writeSymlink(link, "stuff"); - PeepholePermalink.symlinks.clear(); // so we actually test the filesystem - assertEquals("stuff", PeepholePermalink.readSymlink(link)); - } - -} diff --git a/test/src/test/java/hudson/model/AbstractProjectTest.java b/test/src/test/java/hudson/model/AbstractProjectTest.java index a9318228f99ef5f1b2f5ce30bc7cbec249c390d7..c9c54eaf7d8e9e0b5f85686d4d0f21e0cee40dc5 100644 --- a/test/src/test/java/hudson/model/AbstractProjectTest.java +++ b/test/src/test/java/hudson/model/AbstractProjectTest.java @@ -34,13 +34,11 @@ import com.gargoylesoftware.htmlunit.html.HtmlPage; import hudson.FilePath; import hudson.Functions; import hudson.Launcher; -import hudson.Util; import hudson.maven.MavenModuleSet; import hudson.scm.NullSCM; import hudson.scm.SCM; import hudson.scm.SCMDescriptor; import hudson.security.GlobalMatrixAuthorizationStrategy; -import hudson.tasks.ArtifactArchiver; import hudson.tasks.BatchFile; import hudson.tasks.BuildTrigger; import hudson.tasks.Shell; @@ -50,8 +48,6 @@ import hudson.triggers.Trigger; import hudson.triggers.TriggerDescriptor; import hudson.util.OneShotEvent; import hudson.util.StreamTaskListener; -import java.io.File; -import java.io.IOException; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL; @@ -60,11 +56,9 @@ import java.util.ResourceBundle; import java.util.Vector; import java.util.concurrent.Future; import jenkins.model.Jenkins; -import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; -import org.junit.Assume; import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.Issue; @@ -258,65 +252,6 @@ public class AbstractProjectTest { } } - @Test - @Issue("JENKINS-1986") - public void buildSymlinks() throws Exception { - Assume.assumeFalse("If we're on Windows, don't bother doing this", Functions.isWindows()); - - FreeStyleProject job = j.createFreeStyleProject(); - job.getBuildersList().add(new Shell("echo \"Build #$BUILD_NUMBER\"\n")); - FreeStyleBuild build = job.scheduleBuild2(0, new Cause.UserCause()).get(); - File lastSuccessful = new File(job.getRootDir(), "lastSuccessful"), - lastStable = new File(job.getRootDir(), "lastStable"); - // First build creates links - assertSymlinkForBuild(lastSuccessful, 1); - assertSymlinkForBuild(lastStable, 1); - FreeStyleBuild build2 = job.scheduleBuild2(0, new Cause.UserCause()).get(); - // Another build updates links - assertSymlinkForBuild(lastSuccessful, 2); - assertSymlinkForBuild(lastStable, 2); - // Delete latest build should update links - build2.delete(); - assertSymlinkForBuild(lastSuccessful, 1); - assertSymlinkForBuild(lastStable, 1); - // Delete all builds should remove links - build.delete(); - assertFalse("lastSuccessful link should be removed", lastSuccessful.exists()); - assertFalse("lastStable link should be removed", lastStable.exists()); - } - - private static void assertSymlinkForBuild(File file, int buildNumber) - throws IOException, InterruptedException { - assert file.exists() : "should exist and point to something that exists"; - assert Util.isSymlink(file) : "should be symlink"; - String s = FileUtils.readFileToString(new File(file, "log")); - assert s.contains("Build #" + buildNumber + "\n") : "link should point to build #$buildNumber, but link was: ${Util.resolveSymlink(file, TaskListener.NULL)}\nand log was:\n$s"; - } - - @Test - @Issue("JENKINS-2543") - public void symlinkForPostBuildFailure() throws Exception { - Assume.assumeFalse("If we're on Windows, don't bother doing this", Functions.isWindows()); - - // Links should be updated after post-build actions when final build result is known - FreeStyleProject job = j.createFreeStyleProject(); - job.getBuildersList().add(new Shell("echo \"Build #$BUILD_NUMBER\"\n")); - FreeStyleBuild build = job.scheduleBuild2(0, new Cause.UserCause()).get(); - assert Result.SUCCESS == build.getResult(); - File lastSuccessful = new File(job.getRootDir(), "lastSuccessful"), - lastStable = new File(job.getRootDir(), "lastStable"); - // First build creates links - assertSymlinkForBuild(lastSuccessful, 1); - assertSymlinkForBuild(lastStable, 1); - // Archive artifacts that don't exist to create failure in post-build action - job.getPublishersList().add(new ArtifactArchiver("*.foo", "", false, false)); - build = job.scheduleBuild2(0, new Cause.UserCause()).get(); - assert Result.FAILURE == build.getResult(); - // Links should not be updated since build failed - assertSymlinkForBuild(lastSuccessful, 1); - assertSymlinkForBuild(lastStable, 1); - } - /* TODO too slow, seems capable of causing testWorkspaceLock to time out: @Test @Issue("JENKINS-15156") diff --git a/test/src/test/java/jenkins/model/JenkinsBuildsAndWorkspacesDirectoriesTest.java b/test/src/test/java/jenkins/model/JenkinsBuildsAndWorkspacesDirectoriesTest.java index 6d67c7317c153384e8656d7fd66cece71da4aa8d..a89b9a0d84a5b961cc559adf4a0e344c4e583c0b 100644 --- a/test/src/test/java/jenkins/model/JenkinsBuildsAndWorkspacesDirectoriesTest.java +++ b/test/src/test/java/jenkins/model/JenkinsBuildsAndWorkspacesDirectoriesTest.java @@ -1,7 +1,6 @@ package jenkins.model; import hudson.Functions; -import hudson.Util; import hudson.init.InitMilestone; import hudson.maven.MavenModuleSet; import hudson.maven.MavenModuleSetBuild; @@ -21,12 +20,10 @@ import org.jvnet.hudson.test.RestartableJenkinsRule; import org.jvnet.hudson.test.recipes.LocalData; import java.io.File; -import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.logging.Level; -import java.util.logging.LogRecord; import java.util.stream.Stream; import java.util.stream.Collectors; @@ -353,45 +350,4 @@ public class JenkinsBuildsAndWorkspacesDirectoriesTest { }); } - @Test - @Issue("JENKINS-17137") - public void externalBuildDirectorySymlinks() throws Exception { - assumeFalse(Functions.isWindows()); // symlinks may not be available - - // Hack to get String builds usable in lambda below - final List builds = new ArrayList<>(); - - story.then(steps -> { - builds.add(story.j.createTmpDir().toString()); - setBuildsDirProperty(builds.get(0) + "/${ITEM_FULL_NAME}"); - }); - - story.then(steps -> { - - assertEquals(builds.get(0) + "/${ITEM_FULL_NAME}", story.j.jenkins.getRawBuildsDir()); - FreeStyleProject p = story.j.jenkins.createProject(MockFolder.class, "d").createProject(FreeStyleProject.class, "p"); - FreeStyleBuild b1 = p.scheduleBuild2(0).get(); - File link = new File(p.getRootDir(), "lastStable"); - assertTrue(link.exists()); - assertEquals(resolveAll(link).getAbsolutePath(), b1.getRootDir().getAbsolutePath()); - FreeStyleBuild b2 = p.scheduleBuild2(0).get(); - assertTrue(link.exists()); - assertEquals(resolveAll(link).getAbsolutePath(), b2.getRootDir().getAbsolutePath()); - b2.delete(); - assertTrue(link.exists()); - assertEquals(resolveAll(link).getAbsolutePath(), b1.getRootDir().getAbsolutePath()); - b1.delete(); - assertFalse(link.exists()); - }); - } - - private File resolveAll(File link) throws InterruptedException, IOException { - while (true) { - File f = Util.resolveSymlinkToFile(link); - if (f == null) { - return link; - } - link = f; - } - } } diff --git a/test/src/test/java/jenkins/model/PeepholePermalinkTest.java b/test/src/test/java/jenkins/model/PeepholePermalinkTest.java index c25e276b4ede2538d30a207f45207643422bbf2f..d37b01971e85a69786f0e0ad33b38635134eec6a 100644 --- a/test/src/test/java/jenkins/model/PeepholePermalinkTest.java +++ b/test/src/test/java/jenkins/model/PeepholePermalinkTest.java @@ -1,17 +1,15 @@ package jenkins.model; -import hudson.Functions; -import hudson.Util; import hudson.model.FreeStyleBuild; import hudson.model.FreeStyleProject; +import hudson.model.Job; import hudson.model.Run; -import java.io.File; +import java.nio.file.Files; +import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; -import org.junit.Assume; import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.FailureBuilder; -import org.jvnet.hudson.test.Issue; import org.jvnet.hudson.test.JenkinsRule; public class PeepholePermalinkTest { @@ -24,76 +22,45 @@ public class PeepholePermalinkTest { */ @Test public void basics() throws Exception { - Assume.assumeFalse("can't run on windows because we rely on symlinks", Functions.isWindows()); - FreeStyleProject p = j.createFreeStyleProject(); FreeStyleBuild b1 = j.assertBuildStatusSuccess(p.scheduleBuild2(0)); - File lsb = new File(p.getBuildDir(), "lastSuccessfulBuild"); - File lfb = new File(p.getBuildDir(), "lastFailedBuild"); + String lsb = "lastSuccessfulBuild"; + String lfb = "lastFailedBuild"; - assertLink(lsb, b1); + assertStorage(lsb, p, b1); // now another build that fails p.getBuildersList().add(new FailureBuilder()); FreeStyleBuild b2 = p.scheduleBuild2(0).get(); - assertLink(lsb, b1); - assertLink(lfb, b2); + assertStorage(lsb, p, b1); + assertStorage(lfb, p, b2); // one more build and this time it succeeds p.getBuildersList().clear(); FreeStyleBuild b3 = j.assertBuildStatusSuccess(p.scheduleBuild2(0)); - assertLink(lsb, b3); - assertLink(lfb, b2); + assertStorage(lsb, p, b3); + assertStorage(lfb, p, b2); - // delete b3 and symlinks should update properly + // delete b3 and links should update properly b3.delete(); - assertLink(lsb, b1); - assertLink(lfb, b2); + assertStorage(lsb, p, b1); + assertStorage(lfb, p, b2); b1.delete(); - assertLink(lsb, null); - assertLink(lfb, b2); + assertStorage(lsb, p, null); + assertStorage(lfb, p, b2); b2.delete(); - assertLink(lsb, null); - assertLink(lfb, null); - } - - private void assertLink(File symlink, Run build) throws Exception { - assertEquals(build == null ? "-1" : Integer.toString(build.getNumber()), Util.resolveSymlink(symlink)); - } - - /** - * job/JOBNAME/lastStable and job/JOBNAME/lastSuccessful symlinks that we - * used to generate should still work - */ - @Test - public void legacyCompatibility() throws Exception { - Assume.assumeFalse("can't run on windows because we rely on symlinks", Functions.isWindows()); - - FreeStyleProject p = j.createFreeStyleProject(); - FreeStyleBuild b1 = j.assertBuildStatusSuccess(p.scheduleBuild2(0)); - - for (String n : new String[] {"lastStable", "lastSuccessful"}) { - // test if they both point to b1 - assertEquals(new File(p.getRootDir(), n + "/build.xml").length(), new File(b1.getRootDir(), "build.xml").length()); - } + assertStorage(lsb, p, null); + assertStorage(lfb, p, null); } - @Test - @Issue("JENKINS-19034") - public void rebuildBuildNumberPermalinks() throws Exception { - FreeStyleProject p = j.createFreeStyleProject(); - FreeStyleBuild b = j.assertBuildStatusSuccess(p.scheduleBuild2(0)); - File f = new File(p.getBuildDir(), "1"); - // assertTrue(Util.isSymlink(f)) - f.delete(); - PeepholePermalink link = (PeepholePermalink) p.getPermalinks().stream().filter(l -> l instanceof PeepholePermalink).findAny().get(); - link.updateCache(p, b); - assertTrue("build symlink hasn't been restored", f.exists()); + private void assertStorage(String id, Job job, Run build) throws Exception { + assertThat(Files.readAllLines(PeepholePermalink.storageFor(job.getBuildDir()).toPath()), + hasItem(id + " " + (build == null ? -1 : build.getNumber()))); } }