From 380f091fa5fa71f44368c0e8e58bb520ebc34ed7 Mon Sep 17 00:00:00 2001 From: Matthias Sohn Date: Mon, 20 Feb 2023 22:32:37 +0100 Subject: Silence API errors introduced by 9424052f Change-Id: Ia9e619a8fa06648086b583c994e4b107ae06c44d --- org.eclipse.jgit/.settings/.api_filters | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/org.eclipse.jgit/.settings/.api_filters b/org.eclipse.jgit/.settings/.api_filters index cc4916cf8b..01366d259a 100644 --- a/org.eclipse.jgit/.settings/.api_filters +++ b/org.eclipse.jgit/.settings/.api_filters @@ -7,6 +7,18 @@ + + + + + + + + + + + + -- cgit v1.2.3 From 8eee800fb1853a9b73067fa875737bb3a117ec2c Mon Sep 17 00:00:00 2001 From: Matthias Sohn Date: Fri, 10 Feb 2023 23:39:20 +0100 Subject: 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 --- .../org/eclipse/jgit/internal/JGitText.properties | 4 + .../src/org/eclipse/jgit/internal/JGitText.java | 4 + .../org/eclipse/jgit/internal/storage/file/GC.java | 181 ++++++++++++++++++++- 3 files changed, 181 insertions(+), 8 deletions(-) diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties index 3c0f75710e..f9b0595080 100644 --- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties +++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties @@ -137,6 +137,7 @@ classCastNotA=Not a {0} cloneNonEmptyDirectory=Destination path "{0}" already exists and is not an empty directory closed=closed closeLockTokenFailed=Closing LockToken ''{0}'' failed +closePidLockFailed=Closing lock file ''{0}'' failed collisionOn=Collision on {0} commandClosedStderrButDidntExit=Command {0} closed stderr stream but didn''t exit within timeout {1} seconds commandRejectedByHook=Rejected by "{0}" hook.\n{1} @@ -306,6 +307,7 @@ expectedReceivedContentType=expected Content-Type {0}; received Content-Type {1} expectedReportForRefNotReceived={0}: expected report for ref {1} not received failedAtomicFileCreation=Atomic file creation failed, number of hard links to file {0} was not 2 but {1} failedCreateLockFile=Creating lock file {} failed +failedPidLock=Failed to lock ''{0}'' guarding git gc failedReadHttpsProtocols=Failed to read system property https.protocols, assuming it is not set failedToConvert=Failed to convert rest: %s failedToDetermineFilterDefinition=An exception occurred while determining filter definitions @@ -325,6 +327,7 @@ flagIsDisposed={0} is disposed. flagNotFromThis={0} not from this. flagsAlreadyCreated={0} flags already created. funnyRefname=funny refname +gcAlreadyRunning=fatal: gc is already running on machine ''{0}'' pid {1} gcFailed=Garbage collection failed. gcTooManyUnpruned=Too many loose, unpruneable objects after garbage collection. Consider adjusting gc.auto or gc.pruneExpire. headRequiredToStash=HEAD required to stash local changes @@ -675,6 +678,7 @@ sslTrustAlways=Always skip SSL verification for this server from now on sslTrustForRepo=Skip SSL verification for git operations for repository {0} sslTrustNow=Skip SSL verification for this single git operation sslVerifyCannotSave=Could not save setting for http.sslVerify +stalePidLock=Lock file ''{0}'' is older than 12 hours and seems to be stale, lastModified: {1}, trying to lock it staleRevFlagsOn=Stale RevFlags on {0} startingReadStageWithoutWrittenRequestDataPendingIsNotSupported=Starting read stage without written request data pending is not supported stashApplyConflict=Applying stashed changes resulted in a conflict 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 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 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 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); + } + } + + } } -- cgit v1.2.3 From 2a2a208fa1651e0558755c4804a2a9db1433a345 Mon Sep 17 00:00:00 2001 From: Matthias Sohn Date: Wed, 22 Feb 2023 01:06:06 +0100 Subject: Use Java 11 ProcessHandle to get pid of the current process Change-Id: I790f218601c1d5e1b39c4101e3b2708e76b9d782 --- .../src/org/eclipse/jgit/internal/storage/file/GC.java | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) 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 4e8164c77f..080af97cbc 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 @@ -1713,17 +1713,7 @@ public class GC { } 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; + return ProcessHandle.current().pid(); } private String getHostName() { -- cgit v1.2.3