package hudson.plugins.mercurial; import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.EnvVars; import hudson.FilePath; import hudson.Launcher; import hudson.model.Hudson; import hudson.model.Node; import hudson.model.TaskListener; import java.io.IOException; import java.math.BigInteger; import java.security.MessageDigest; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.concurrent.locks.ReentrantLock; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Mercurial repository that serves as a cache to hg operations in the Hudson cluster. * * <p> * This substantially improves the performance by reducing the amount of data that needs to be transferred. * One cache will be built on the Hudson master, then per-slave cache is cloned from there. * * @see HUDSON-4794: manages repository caches. * @author Jesse Glick */ class Cache { /** * The remote source repository that this repository is caching. */ private final String remote; /** * Hashed value of {@link #remote} that only contains characters that are safe as a directory name. */ private final String hash; /** * Mutual exclusion to the access to the cache. */ private final ReentrantLock lock = new ReentrantLock(true); private Cache(String remote, String hash) { this.remote = remote; this.hash = hash; } private static final Map<String, Cache> CACHES = new HashMap<String, Cache>(); public synchronized static @NonNull Cache fromURL(String remote) { String h = hashSource(remote); Cache cache = CACHES.get(h); if (cache == null) { CACHES.put(h, cache = new Cache(remote, h)); } return cache; } /** * Returns a local hg repository cache of the remote repository specified in the given {@link MercurialSCM} * on the given {@link Node}, fully updated to the tip of the current remote repository. * * @param node * The node that gets a local cached repository. * * @return * The file path on the {@code node} to the local repository cache, cloned off from the master cache. */ @CheckForNull FilePath repositoryCache(MercurialSCM config, Node node, Launcher launcher, TaskListener listener, boolean fromPolling) throws IOException, InterruptedException { boolean wasLocked = lock.isLocked(); if (wasLocked) { listener.getLogger().println("Waiting for lock on hgcache/" + hash + "..."); } lock.lockInterruptibly(); try { if (wasLocked) { listener.getLogger().println("...acquired cache lock."); } // Always update master cache first. Node master = Hudson.getInstance(); FilePath masterCaches = master.getRootPath().child("hgcache"); FilePath masterCache = masterCaches.child(hash); Launcher masterLauncher = node == master ? launcher : master.createLauncher(listener); // hg invocation on master // do we need to pass in EnvVars from a build too? HgExe masterHg = new HgExe(config,masterLauncher,master,listener,new EnvVars()); if (masterCache.isDirectory()) { if (MercurialSCM.joinWithPossibleTimeout(masterHg.pull().pwd(masterCache), fromPolling, listener) != 0) { listener.error("Failed to update " + masterCache); return null; } } else { masterCaches.mkdirs(); if (MercurialSCM.joinWithPossibleTimeout(masterHg.clone("--noupdate", remote, masterCache.getRemote()), fromPolling, listener) != 0) { listener.error("Failed to clone " + remote); return null; } } if (node == master) { return masterCache; } // Not on master, so need to create/update local cache as well. FilePath localCaches = node.getRootPath().child("hgcache"); FilePath localCache = localCaches.child(hash); FilePath masterTransfer = masterCache.child("xfer.hg"); FilePath localTransfer = localCache.child("xfer.hg"); try { // hg invocation on the slave HgExe slaveHg = new HgExe(config,launcher,node,listener,new EnvVars()); if (localCache.isDirectory()) { // Need to transfer just newly available changesets. Set<String> masterHeads = masterHg.heads(masterCache, fromPolling); Set<String> localHeads = slaveHg.heads(localCache, fromPolling); if (localHeads.equals(masterHeads)) { listener.getLogger().println("Local cache is up to date."); } else { // If there are some local heads not in master, they must be ancestors of new heads. // If there are some master heads not in local, they could be descendants of old heads, // or they could be new branches. // Issue1910: in Hg 1.4.3 and earlier, passing --base $h for h in localHeads will fail // to actually exclude those head sets, but not a big deal. (Hg 1.5 fixes that but leaves // a major bug that if no csets are selected, the whole repo will be bundled; fortunately // this case should be caught by equality check above.) if (MercurialSCM.joinWithPossibleTimeout(masterHg.bundle(localHeads,"xfer.hg"). pwd(masterCache), fromPolling, listener) != 0) { listener.error("Failed to send outgoing changes"); return null; } } } else { // Need to transfer entire repo. if (MercurialSCM.joinWithPossibleTimeout(masterHg.bundleAll("xfer.hg").pwd(masterCache), fromPolling, listener) != 0) { listener.error("Failed to bundle repo"); return null; } localCaches.mkdirs(); if (MercurialSCM.joinWithPossibleTimeout(slaveHg.init(localCache), fromPolling, listener) != 0) { listener.error("Failed to create local cache"); return null; } } if (masterTransfer.exists()) { masterTransfer.copyTo(localTransfer); if (MercurialSCM.joinWithPossibleTimeout(slaveHg.unbundle("xfer.hg").pwd(localCache), fromPolling, listener) != 0) { listener.error("Failed to unbundle " + localTransfer); return null; } } } finally { masterTransfer.delete(); localTransfer.delete(); } return localCache; } finally { lock.unlock(); } } /** * Hash a URL into a string that only contains characters that are safe as directory names. */ static String hashSource(String source) { if (!source.endsWith("/")) { source += "/"; } Matcher m = Pattern.compile(".+[/]([^/]+)[/]?").matcher(source); BigInteger hash; try { hash = new BigInteger(1, MessageDigest.getInstance("SHA-1").digest(source.getBytes("UTF-8"))); } catch (Exception x) { throw new AssertionError(x); } return String.format("%040X%s", hash, m.matches() ? "-" + m.group(1) : ""); } }