From b623dd3ec4e54210b92f0f726175f2eeaa06b59c Mon Sep 17 00:00:00 2001 From: Sam Van Oort Date: Thu, 5 Apr 2018 12:44:08 -0400 Subject: [PATCH 1/3] Jump dirty-restart support back to manual implementation of directory copying for more robust symlink handling --- .../hudson/test/RestartableJenkinsRule.java | 64 ++++++++++++++----- 1 file changed, 49 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/jvnet/hudson/test/RestartableJenkinsRule.java b/src/main/java/org/jvnet/hudson/test/RestartableJenkinsRule.java index 98f5c9ddb..3e8d32151 100644 --- a/src/main/java/org/jvnet/hudson/test/RestartableJenkinsRule.java +++ b/src/main/java/org/jvnet/hudson/test/RestartableJenkinsRule.java @@ -1,7 +1,6 @@ package org.jvnet.hudson.test; import groovy.lang.Closure; -import hudson.FilePath; import org.junit.Assert; import org.junit.rules.MethodRule; import org.junit.rules.TemporaryFolder; @@ -11,10 +10,16 @@ import java.io.File; import java.io.IOException; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.LinkOption; import java.nio.file.NoSuchFileException; -import java.util.ArrayList; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.StandardCopyOption; +import java.nio.file.attribute.BasicFileAttributes; import java.util.LinkedHashMap; -import java.util.List; +import java.util.Collections; import java.util.Map; import java.util.concurrent.Callable; @@ -86,6 +91,46 @@ public void evaluate() throws Throwable { }); } + /** Approach adapted from https://stackoverflow.com/questions/6214703/copy-entire-directory-contents-to-another-directory */ + static class CopyFileVisitor extends SimpleFileVisitor { + private final Path targetPath; + private Path sourcePath = null; + public CopyFileVisitor(Path targetPath) { + this.targetPath = targetPath; + } + + @Override + public FileVisitResult preVisitDirectory(final Path dir, + final BasicFileAttributes attrs) throws IOException { + if (sourcePath == null) { + sourcePath = dir; + } else { + Files.createDirectories(targetPath.resolve(sourcePath + .relativize(dir))); + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(final Path file, + final BasicFileAttributes attrs) throws IOException { + try { + if (!Files.isSymbolicLink(file)) { + // Needed because Jenkins includes invalid lastSuccessful symlinks and otherwise we get a NoSuchFileException + Files.copy(file, + targetPath.resolve(sourcePath.relativize(file)), StandardCopyOption.COPY_ATTRIBUTES); + } else if (Files.isSymbolicLink(file) && Files.exists(Files.readSymbolicLink(file))) { + Files.copy(file, + targetPath.resolve(sourcePath.relativize(file)), LinkOption.NOFOLLOW_LINKS, StandardCopyOption.COPY_ATTRIBUTES); + } + } catch (NoSuchFileException nsfe) { + // File removed in between scan beginning and when we try to copy it, ignore it + } + + return FileVisitResult.CONTINUE; + } + } + /** * Simulate an abrupt failure of Jenkins to see if it appropriately handles inconsistent states when * shutdown cleanup is not performed or data is not written fully to disk. @@ -104,18 +149,7 @@ void simulateAbruptShutdown() throws IOException { File newHome = temp.newFolder(); // Copy efficiently - try { - try { - new FilePath(homeDir).copyRecursiveTo(new FilePath(newHome)); - } catch (NoSuchFileException nsfe) { - // Retry in case of tempfile deletion while copying. - newHome.delete(); - newHome = temp.newFolder(); - new FilePath(homeDir).copyRecursiveTo(new FilePath(newHome)); - } - } catch (InterruptedException ie) { - throw new IOException(ie); - } + Files.walkFileTree(homeDir.toPath(), Collections.EMPTY_SET, 99, new CopyFileVisitor(newHome.toPath())); home = newHome; } From 5591d89c558823361e7fd9b499b4c5ec59e4cb37 Mon Sep 17 00:00:00 2001 From: Sam Van Oort Date: Thu, 5 Apr 2018 14:28:45 -0400 Subject: [PATCH 2/3] Better bulletproofing for errors with simulated abrupt shutdown --- .../hudson/test/RestartableJenkinsRule.java | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/jvnet/hudson/test/RestartableJenkinsRule.java b/src/main/java/org/jvnet/hudson/test/RestartableJenkinsRule.java index 3e8d32151..dc408c0e9 100644 --- a/src/main/java/org/jvnet/hudson/test/RestartableJenkinsRule.java +++ b/src/main/java/org/jvnet/hudson/test/RestartableJenkinsRule.java @@ -9,6 +9,7 @@ import org.junit.runners.model.Statement; import java.io.File; +import java.io.FileNotFoundException; import java.io.IOException; import java.nio.file.FileVisitResult; import java.nio.file.Files; @@ -22,6 +23,8 @@ import java.util.Collections; import java.util.Map; import java.util.concurrent.Callable; +import java.util.logging.Level; +import java.util.logging.Logger; /** * Provides a pattern for executing a sequence of steps. @@ -60,6 +63,7 @@ public class RestartableJenkinsRule implements MethodRule { */ public File home; + private static final Logger LOGGER = Logger.getLogger(HudsonTestCase.class.getName()); @Override public Statement apply(final Statement base, FrameworkMethod method, Object target) { @@ -108,6 +112,7 @@ public FileVisitResult preVisitDirectory(final Path dir, Files.createDirectories(targetPath.resolve(sourcePath .relativize(dir))); } + return FileVisitResult.CONTINUE; } @@ -129,6 +134,15 @@ public FileVisitResult visitFile(final Path file, return FileVisitResult.CONTINUE; } + + @Override + public FileVisitResult visitFileFailed(Path file, IOException exc) { + if (exc instanceof FileNotFoundException) { + LOGGER.log(Level.FINE, "File disappeared while trying to copy to new home: "+file.toString()); + return FileVisitResult.CONTINUE; + } + return FileVisitResult.CONTINUE; + } } /** @@ -143,14 +157,16 @@ public FileVisitResult visitFile(final Path file, * @throws IOException */ void simulateAbruptShutdown() throws IOException { - File homeDir = this.home; - TemporaryFolder temp = new TemporaryFolder(); - temp.create(); - File newHome = temp.newFolder(); + LOGGER.log(Level.INFO, "Beginning snapshot of JENKINS_HOME so we can simulate abrupt shutdown. Disk writes MAY be lost if they happen after this."); + File homeDir = this.home; + TemporaryFolder temp = new TemporaryFolder(); + temp.create(); + File newHome = temp.newFolder(); - // Copy efficiently - Files.walkFileTree(homeDir.toPath(), Collections.EMPTY_SET, 99, new CopyFileVisitor(newHome.toPath())); - home = newHome; + // Copy efficiently + Files.walkFileTree(homeDir.toPath(), Collections.EMPTY_SET, 99, new CopyFileVisitor(newHome.toPath())); + LOGGER.log(Level.INFO, "Finished snapshot of JENKINS_HOME, any disk writes by Jenkins after this are lost as we will simulate suddenly killing the Jenkins process and switch to the snapshot."); + home = newHome; } /** From 263bb62ad0bef06e41b3f6ba3466c0323aae85fa Mon Sep 17 00:00:00 2001 From: Sam Van Oort Date: Thu, 5 Apr 2018 17:37:22 -0400 Subject: [PATCH 3/3] Better logging of errors --- .../java/org/jvnet/hudson/test/RestartableJenkinsRule.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/jvnet/hudson/test/RestartableJenkinsRule.java b/src/main/java/org/jvnet/hudson/test/RestartableJenkinsRule.java index dc408c0e9..44834d4c8 100644 --- a/src/main/java/org/jvnet/hudson/test/RestartableJenkinsRule.java +++ b/src/main/java/org/jvnet/hudson/test/RestartableJenkinsRule.java @@ -130,6 +130,7 @@ public FileVisitResult visitFile(final Path file, } } catch (NoSuchFileException nsfe) { // File removed in between scan beginning and when we try to copy it, ignore it + LOGGER.log(Level.FINE, "File disappeared while trying to copy to new home, continuing anyway: "+file.toString()); } return FileVisitResult.CONTINUE; @@ -138,10 +139,12 @@ public FileVisitResult visitFile(final Path file, @Override public FileVisitResult visitFileFailed(Path file, IOException exc) { if (exc instanceof FileNotFoundException) { - LOGGER.log(Level.FINE, "File disappeared while trying to copy to new home: "+file.toString()); + LOGGER.log(Level.FINE, "File not found while trying to copy to new home, continuing anyway: "+file.toString()); return FileVisitResult.CONTINUE; + } else { + LOGGER.log(Level.WARNING, "Error copying file", exc); + return FileVisitResult.TERMINATE; } - return FileVisitResult.CONTINUE; } }