diff options
author | Matthias Sohn <matthias.sohn@sap.com> | 2023-02-10 23:39:20 +0100 |
---|---|---|
committer | Matthias Sohn <matthias.sohn@sap.com> | 2023-02-21 00:18:33 +0100 |
commit | 8eee800fb1853a9b73067fa875737bb3a117ec2c (patch) | |
tree | 5392136ad09aa6a8dfa3b4197043990d77937e20 /org.eclipse.jgit/src | |
parent | 380f091fa5fa71f44368c0e8e58bb520ebc34ed7 (diff) | |
download | jgit-8eee800fb1853a9b73067fa875737bb3a117ec2c.tar.gz jgit-8eee800fb1853a9b73067fa875737bb3a117ec2c.zip |
Acquire file lock "gc.pid" before running gc
Git guards gc by locking a lock file "gc.pid" before starting execution.
The lock file contains the pid and hostname of the process holding the
lock. Git tries to kill the process holding that lock if the lock file
wasn't modified in the last 12 hours and was started from the same host.
Teach JGit to acquire this lock before running gc but skip execution if
another process already holds the lock. Killing the other process could
be undesired if it's a long running application.
If the lock file wasn't modified in the last 12 hours try to lock it and
run gc if locking succeeds.
Register a shutdown hook for the lock file to ensure it is cleaned up if
the process is gracefully killed.
Change-Id: I00b838dcbf4fb0d03863bf7a2cd86b743c6c6971
Diffstat (limited to 'org.eclipse.jgit/src')
-rw-r--r-- | org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java | 4 | ||||
-rw-r--r-- | org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java | 181 |
2 files changed, 177 insertions, 8 deletions
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java index 2b48bf5a1b..446d35078f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java @@ -165,6 +165,7 @@ public class JGitText extends TranslationBundle { /***/ public String cloneNonEmptyDirectory; /***/ public String closeLockTokenFailed; /***/ public String closed; + /***/ public String closePidLockFailed; /***/ public String collisionOn; /***/ public String commandClosedStderrButDidntExit; /***/ public String commandRejectedByHook; @@ -334,6 +335,7 @@ public class JGitText extends TranslationBundle { /***/ public String expectedReportForRefNotReceived; /***/ public String failedAtomicFileCreation; /***/ public String failedCreateLockFile; + /***/ public String failedPidLock; /***/ public String failedReadHttpsProtocols; /***/ public String failedToDetermineFilterDefinition; /***/ public String failedToConvert; @@ -353,6 +355,7 @@ public class JGitText extends TranslationBundle { /***/ public String flagNotFromThis; /***/ public String flagsAlreadyCreated; /***/ public String funnyRefname; + /***/ public String gcAlreadyRunning; /***/ public String gcFailed; /***/ public String gcTooManyUnpruned; /***/ public String headRequiredToStash; @@ -703,6 +706,7 @@ public class JGitText extends TranslationBundle { /***/ public String sslTrustForRepo; /***/ public String sslTrustNow; /***/ public String sslVerifyCannotSave; + /***/ public String stalePidLock; /***/ public String staleRevFlagsOn; /***/ public String startingReadStageWithoutWrittenRequestDataPendingIsNotSupported; /***/ public String stashApplyConflict; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java index 9e97659499..3f281a5d6c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/GC.java @@ -12,17 +12,24 @@ package org.eclipse.jgit.internal.storage.file; import static org.eclipse.jgit.internal.storage.pack.PackExt.BITMAP_INDEX; import static org.eclipse.jgit.internal.storage.pack.PackExt.INDEX; -import static org.eclipse.jgit.internal.storage.pack.PackExt.PACK; import static org.eclipse.jgit.internal.storage.pack.PackExt.KEEP; +import static org.eclipse.jgit.internal.storage.pack.PackExt.PACK; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.PrintWriter; +import java.io.RandomAccessFile; import java.io.StringWriter; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.ByteBuffer; import java.nio.channels.Channels; import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +import java.nio.channels.OverlappingFileLockException; +import java.nio.charset.StandardCharsets; import java.nio.file.DirectoryNotEmptyException; import java.nio.file.DirectoryStream; import java.nio.file.Files; @@ -44,6 +51,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.TreeMap; import java.util.concurrent.Callable; @@ -83,8 +91,11 @@ import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.storage.pack.PackConfig; import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.filter.TreeFilter; +import org.eclipse.jgit.util.FS; +import org.eclipse.jgit.util.FS.LockToken; import org.eclipse.jgit.util.FileUtils; import org.eclipse.jgit.util.GitDateParser; +import org.eclipse.jgit.util.StringUtils; import org.eclipse.jgit.util.SystemReader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -264,13 +275,18 @@ public class GC { if (automatic && !needGc()) { return Collections.emptyList(); } - pm.start(6 /* tasks */); - packRefs(); - // TODO: implement reflog_expire(pm, repo); - Collection<Pack> newPacks = repack(); - prune(Collections.emptySet()); - // TODO: implement rerere_gc(pm); - return newPacks; + try (PidLock lock = new PidLock()) { + if (!lock.lock()) { + return Collections.emptyList(); + } + pm.start(6 /* tasks */); + packRefs(); + // TODO: implement reflog_expire(pm, repo); + Collection<Pack> newPacks = repack(); + prune(Collections.emptySet()); + // TODO: implement rerere_gc(pm); + return newPacks; + } } /** @@ -1596,4 +1612,153 @@ public class GC { return repo.getConfig().getInt(ConfigConstants.CONFIG_GC_SECTION, ConfigConstants.CONFIG_KEY_AUTO, DEFAULT_AUTOLIMIT); } + + private class PidLock implements AutoCloseable { + + private static final String GC_PID = "gc.pid"; //$NON-NLS-1$ + + private final Path pidFile; + + private LockToken token; + + private FileLock lock; + + private RandomAccessFile f; + + private FileChannel channel; + + PidLock() { + pidFile = repo.getDirectory().toPath().resolve(GC_PID); + } + + boolean lock() { + if (Files.exists(pidFile)) { + Instant mtime = FS.DETECTED + .lastModifiedInstant(pidFile.toFile()); + Instant twelveHoursAgo = Instant.now().minus(12, + ChronoUnit.HOURS); + if (mtime.compareTo(twelveHoursAgo) > 0) { + gcAlreadyRunning(); + return false; + } + LOG.warn(MessageFormat.format(JGitText.get().stalePidLock, + pidFile, mtime)); + } + try { + token = FS.DETECTED.createNewFileAtomic(pidFile.toFile()); + f = new RandomAccessFile(pidFile.toFile(), "rw"); //$NON-NLS-1$ + channel = f.getChannel(); + lock = channel.tryLock(); + if (lock == null) { + failedToLock(); + return false; + } + channel.write(ByteBuffer + .wrap(getProcDesc().getBytes(StandardCharsets.UTF_8))); + Thread cleanupHook = new Thread(() -> close()); + try { + Runtime.getRuntime().addShutdownHook(cleanupHook); + } catch (IllegalStateException e) { + // ignore - the VM is already shutting down + } + } catch (IOException | OverlappingFileLockException e) { + try { + failedToLock(); + } catch (Exception e1) { + LOG.error( + MessageFormat.format( + JGitText.get().closePidLockFailed, pidFile), + e1); + } + return false; + } + return true; + } + + private void failedToLock() { + close(); + LOG.error(MessageFormat.format(JGitText.get().failedPidLock, + pidFile)); + } + + private void gcAlreadyRunning() { + close(); + try { + Optional<String> s = Files.lines(pidFile).findFirst(); + String machine = null; + String pid = null; + if (s.isPresent()) { + String[] c = s.get().split("\\s+"); //$NON-NLS-1$ + pid = c[0]; + machine = c[1]; + } + if (!StringUtils.isEmptyOrNull(machine) + && !StringUtils.isEmptyOrNull(pid)) { + LOG.error(MessageFormat.format( + JGitText.get().gcAlreadyRunning, machine, pid)); + return; + } + } catch (IOException e) { + // ignore + } + LOG.error(MessageFormat.format(JGitText.get().failedPidLock, + pidFile)); + } + + private String getProcDesc() { + StringBuffer s = new StringBuffer(Long.toString(getPID())); + s.append(' '); + s.append(getHostName()); + return s.toString(); + } + + private long getPID() { + String processName = java.lang.management.ManagementFactory + .getRuntimeMXBean().getName(); + if (processName != null && processName.length() > 0) { + try { + return Long.parseLong(processName.split("@")[0]); //$NON-NLS-1$ + } catch (Exception e) { + return 0; + } + } + + return 0; + } + + private String getHostName() { + try { + return InetAddress.getLocalHost().getHostName(); + } catch (UnknownHostException e) { + return ""; //$NON-NLS-1$ + } + } + + @Override + public void close() { + boolean wasLocked = false; + try { + if (lock != null) { + lock.release(); + wasLocked = true; + } + if (channel != null) { + channel.close(); + } + if (f != null) { + f.close(); + } + if (token != null) { + token.close(); + } + if (wasLocked) { + FileUtils.delete(pidFile.toFile(), FileUtils.RETRY); + } + } catch (IOException e) { + LOG.error(MessageFormat + .format(JGitText.get().closePidLockFailed, pidFile), e); + } + } + + } } |