summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMatthias Sohn <matthias.sohn@sap.com>2023-09-08 02:14:20 +0200
committerMatthias Sohn <matthias.sohn@sap.com>2023-09-12 22:43:15 +0200
commitd4d6c2b5af9b984ae824fb0073e1b368b39b1aa4 (patch)
tree9c9b1b4495c499e8aa89874c3dc5d0a7d3b014f6
parentf94be665f12d85304db056c6cb8934870de3e45a (diff)
downloadjgit-d4d6c2b5af9b984ae824fb0073e1b368b39b1aa4.tar.gz
jgit-d4d6c2b5af9b984ae824fb0073e1b368b39b1aa4.zip
Add ShutdownHook to cleanup FileLocks on graceful JVM shutdown
This should avoid stale lock files if the JVM is terminated gracefully. Implement a ShutdownHook which can register/unregister listeners which need to do some cleanup during graceful JVM shutdown. This hook is registered as a Java shutdown hook and when the JVM shuts down calls #onShutdown of registered listeners using a parallel stream to let them run concurrently. See https://docs.oracle.com/javase/8/docs/technotes/guides/lang/hook-design.html Bug: 582379 Change-Id: I1621dc5f7d9a8c832b6d1b74cbc47578b1c2f0b8
-rw-r--r--org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties3
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java3
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LockFile.java6
-rw-r--r--org.eclipse.jgit/src/org/eclipse/jgit/internal/util/ShutdownHook.java149
4 files changed, 161 insertions, 0 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 80a65b2263..141ea8e201 100644
--- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
+++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
@@ -707,6 +707,9 @@ shortCompressedStreamAt=Short compressed stream at {0}
shortReadOfBlock=Short read of block.
shortReadOfOptionalDIRCExtensionExpectedAnotherBytes=Short read of optional DIRC extension {0}; expected another {1} bytes within the section.
shortSkipOfBlock=Short skip of block.
+shutdownCleanup=Cleanup {} during JVM shutdown
+shutdownCleanupFailed=Cleanup during JVM shutdown failed
+shutdownCleanupListenerFailed=Cleanup of {0} during JVM shutdown failed
signatureVerificationError=Signature verification failed
signatureVerificationUnavailable=No signature verifier registered
signedTagMessageNoLf=A non-empty message of a signed tag must end in LF.
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 1da1de4418..f0d6657336 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
@@ -735,6 +735,9 @@ public class JGitText extends TranslationBundle {
/***/ public String shortReadOfBlock;
/***/ public String shortReadOfOptionalDIRCExtensionExpectedAnotherBytes;
/***/ public String shortSkipOfBlock;
+ /***/ public String shutdownCleanup;
+ /***/ public String shutdownCleanupFailed;
+ /***/ public String shutdownCleanupListenerFailed;
/***/ public String signatureVerificationError;
/***/ public String signatureVerificationUnavailable;
/***/ public String signedTagMessageNoLf;
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LockFile.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LockFile.java
index 8ada0a90e1..a2d8bd0140 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LockFile.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/storage/file/LockFile.java
@@ -31,6 +31,7 @@ import java.time.Instant;
import java.util.concurrent.TimeUnit;
import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.internal.util.ShutdownHook;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.util.FS;
@@ -113,6 +114,8 @@ public class LockFile {
private LockToken token;
+ private ShutdownHook.Listener shutdownListener = this::unlock;
+
/**
* Create a new lock for any file.
*
@@ -147,6 +150,7 @@ public class LockFile {
}
boolean obtainedLock = token.isCreated();
if (obtainedLock) {
+ ShutdownHook.INSTANCE.register(shutdownListener);
haveLck = true;
isAppend = false;
written = false;
@@ -471,6 +475,7 @@ public class LockFile {
* the lock is not held.
*/
public boolean commit() {
+ ShutdownHook.INSTANCE.unregister(shutdownListener);
if (os != null) {
unlock();
throw new IllegalStateException(MessageFormat.format(JGitText.get().lockOnNotClosed, ref));
@@ -551,6 +556,7 @@ public class LockFile {
* The temporary file (if created) is deleted before returning.
*/
public void unlock() {
+ ShutdownHook.INSTANCE.unregister(shutdownListener);
if (os != null) {
try {
os.close();
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/util/ShutdownHook.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/util/ShutdownHook.java
new file mode 100644
index 0000000000..9a299bc260
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/util/ShutdownHook.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2023, SAP SE and others
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Distribution License v. 1.0 which is available at
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.internal.util;
+
+import java.text.MessageFormat;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import org.eclipse.jgit.internal.JGitText;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A hook registered as a JVM shutdown hook managing a set of objects needing
+ * cleanup during JVM shutdown. See {@link Runtime#addShutdownHook}.
+ */
+public enum ShutdownHook {
+ /**
+ * Singleton
+ */
+ INSTANCE;
+
+ /**
+ * Object that needs to cleanup on JVM shutdown.
+ */
+ public interface Listener {
+ /**
+ * Cleanup resources when JVM shuts down, called from JVM shutdown hook.
+ * <p>
+ * Implementations should be coded defensively
+ * <ul>
+ * <li>they should finish their work quickly
+ * <li>they should be written to be thread-safe and to avoid deadlocks
+ * insofar as possible
+ * <li>they should not rely blindly upon services that may have
+ * registered their own shutdown hooks and therefore may themselves be
+ * in the process of shutting down
+ * <li>attempts to use other thread-based services may lead to
+ * deadlocks.
+ * </ul>
+ * See {@link Runtime#addShutdownHook} for more details.
+ */
+ public void onShutdown();
+ }
+
+ private static final Logger LOG = LoggerFactory
+ .getLogger(ShutdownHook.class);
+
+ private final Set<Listener> listeners = ConcurrentHashMap.newKeySet();
+
+ private volatile boolean shutdownInProgress;
+
+ private ShutdownHook() {
+ try {
+ Runtime.getRuntime().addShutdownHook(new Thread(this::cleanup));
+ } catch (IllegalStateException e) {
+ // ignore - the VM is already shutting down
+ }
+ }
+
+ private void cleanup() {
+ shutdownInProgress = true;
+ ExecutorService runner = Executors.newWorkStealingPool();
+ try {
+ runner.submit(() -> {
+ this.doCleanup();
+ return null;
+ }).get(30L, TimeUnit.SECONDS);
+ } catch (RejectedExecutionException | InterruptedException
+ | ExecutionException | TimeoutException e) {
+ LOG.error(JGitText.get().shutdownCleanupFailed, e);
+ }
+ runner.shutdownNow();
+ }
+
+ private void doCleanup() {
+ listeners.parallelStream().forEach(this::notify);
+ }
+
+ private void notify(Listener l) {
+ LOG.warn(JGitText.get().shutdownCleanup, l);
+ try {
+ l.onShutdown();
+ } catch (RuntimeException e) {
+ LOG.error(MessageFormat.format(
+ JGitText.get().shutdownCleanupListenerFailed, l), e);
+ }
+ }
+
+ /**
+ * Register object that needs cleanup during JVM shutdown if it is not
+ * already registered. Registration is disabled when JVM shutdown is already
+ * in progress.
+ *
+ * @param l
+ * the object to call {@link Listener#onShutdown} on when JVM
+ * shuts down
+ * @return {@code true} if this object has been registered
+ */
+ public boolean register(Listener l) {
+ if (shutdownInProgress) {
+ return listeners.contains(l);
+ }
+ LOG.debug("register {} with shutdown hook", l); //$NON-NLS-1$
+ listeners.add(l);
+ return true;
+ }
+
+ /**
+ * Unregister object that no longer needs cleanup during JVM shutdown if it
+ * is still registered. Unregistration is disabled when JVM shutdown is
+ * already in progress.
+ *
+ * @param l
+ * the object registered to be notified for cleanup when the JVM
+ * shuts down
+ * @return {@code true} if this object is no longer registered
+ */
+ public boolean unregister(Listener l) {
+ if (shutdownInProgress) {
+ return !listeners.contains(l);
+ }
+ LOG.debug("unregister {} from shutdown hook", l); //$NON-NLS-1$
+ listeners.remove(l);
+ return true;
+ }
+
+ /**
+ * Whether a JVM shutdown is in progress
+ *
+ * @return {@code true} if a JVM shutdown is in progress
+ */
+ public boolean isShutdownInProgress() {
+ return shutdownInProgress;
+ }
+}