diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ConfigTest.java index d98a7d433..aebbafeff 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ConfigTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/ConfigTest.java @@ -788,10 +788,31 @@ public class ConfigTest { @Test public void testIncludeValuePathNotFound() throws ConfigInvalidException { + // we do not expect an exception, included path not found are ignored String notFound = "/not/found"; - expectedEx.expect(ConfigInvalidException.class); - expectedEx.expectMessage(notFound); - parse("[include]\npath=" + notFound + "\n"); + Config parsed = parse("[include]\npath=" + notFound + "\n"); + assertEquals(1, parsed.getSections().size()); + assertEquals(notFound, parsed.getString("include", null, "path")); + } + + @Test + public void testIncludeValuePathWithTilde() throws ConfigInvalidException { + // we do not expect an exception, included path not supported are + // ignored + String notSupported = "~/someFile"; + Config parsed = parse("[include]\npath=" + notSupported + "\n"); + assertEquals(1, parsed.getSections().size()); + assertEquals(notSupported, parsed.getString("include", null, "path")); + } + + @Test + public void testIncludeValuePathRelative() throws ConfigInvalidException { + // we do not expect an exception, included path not supported are + // ignored + String notSupported = "someRelativeFile"; + Config parsed = parse("[include]\npath=" + notSupported + "\n"); + assertEquals(1, parsed.getSections().size()); + assertEquals(notSupported, parsed.getString("include", null, "path")); } @Test diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RepositoryCacheConfigTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RepositoryCacheConfigTest.java new file mode 100644 index 000000000..52cc9fb03 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RepositoryCacheConfigTest.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2016 Ericsson + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.lib; + +import static org.eclipse.jgit.lib.RepositoryCacheConfig.AUTO_CLEANUP_DELAY; +import static org.eclipse.jgit.lib.RepositoryCacheConfig.NO_CLEANUP; +import static org.junit.Assert.assertEquals; + +import java.util.concurrent.TimeUnit; + +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.junit.Before; +import org.junit.Test; + +public class RepositoryCacheConfigTest { + + private RepositoryCacheConfig config; + + @Before + public void setUp() { + config = new RepositoryCacheConfig(); + } + + @Test + public void testDefaultValues() { + assertEquals(TimeUnit.HOURS.toMillis(1), config.getExpireAfter()); + assertEquals(config.getExpireAfter() / 10, config.getCleanupDelay()); + } + + @Test + public void testCleanupDelay() { + config.setCleanupDelay(TimeUnit.HOURS.toMillis(1)); + assertEquals(TimeUnit.HOURS.toMillis(1), config.getCleanupDelay()); + } + + @Test + public void testAutoCleanupDelay() { + config.setExpireAfter(TimeUnit.MINUTES.toMillis(20)); + config.setCleanupDelay(AUTO_CLEANUP_DELAY); + assertEquals(TimeUnit.MINUTES.toMillis(20), config.getExpireAfter()); + assertEquals(config.getExpireAfter() / 10, config.getCleanupDelay()); + } + + @Test + public void testAutoCleanupDelayShouldBeMax10minutes() { + config.setExpireAfter(TimeUnit.HOURS.toMillis(10)); + assertEquals(TimeUnit.HOURS.toMillis(10), config.getExpireAfter()); + assertEquals(TimeUnit.MINUTES.toMillis(10), config.getCleanupDelay()); + } + + @Test + public void testDisabledCleanupDelay() { + config.setCleanupDelay(NO_CLEANUP); + assertEquals(NO_CLEANUP, config.getCleanupDelay()); + } + + @Test + public void testFromConfig() throws ConfigInvalidException { + Config otherConfig = new Config(); + otherConfig.fromText("[core]\nrepositoryCacheExpireAfter=1000\n" + + "repositoryCacheCleanupDelay=500"); + config.fromConfig(otherConfig); + assertEquals(1000, config.getExpireAfter()); + assertEquals(500, config.getCleanupDelay()); + } +} diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RepositoryCacheTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RepositoryCacheTest.java index dd14e64dd..6bea32012 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RepositoryCacheTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/RepositoryCacheTest.java @@ -196,17 +196,80 @@ public class RepositoryCacheTest extends RepositoryTestCase { } @Test - public void testRepositoryUnregisteringWhenClosing() throws Exception { + public void testRepositoryNotUnregisteringWhenClosing() throws Exception { FileKey loc = FileKey.exact(db.getDirectory(), db.getFS()); Repository d2 = RepositoryCache.open(loc); assertEquals(1, d2.useCnt.get()); assertThat(RepositoryCache.getRegisteredKeys(), hasItem(FileKey.exact(db.getDirectory(), db.getFS()))); assertEquals(1, RepositoryCache.getRegisteredKeys().size()); - d2.close(); - assertEquals(0, d2.useCnt.get()); - assertEquals(0, RepositoryCache.getRegisteredKeys().size()); + assertEquals(1, RepositoryCache.getRegisteredKeys().size()); + assertTrue(RepositoryCache.isCached(d2)); + } + + @Test + public void testRepositoryUnregisteringWhenExpired() throws Exception { + Repository repoA = createBareRepository(); + Repository repoB = createBareRepository(); + Repository repoC = createBareRepository(); + RepositoryCache.register(repoA); + RepositoryCache.register(repoB); + RepositoryCache.register(repoC); + + assertEquals(3, RepositoryCache.getRegisteredKeys().size()); + assertTrue(RepositoryCache.isCached(repoA)); + assertTrue(RepositoryCache.isCached(repoB)); + assertTrue(RepositoryCache.isCached(repoC)); + + // fake that repoA was closed more than 1 hour ago (default expiration + // time) + repoA.close(); + repoA.closedAt.set(System.currentTimeMillis() - 65 * 60 * 1000); + // close repoB but this one will not be expired + repoB.close(); + + assertEquals(3, RepositoryCache.getRegisteredKeys().size()); + assertTrue(RepositoryCache.isCached(repoA)); + assertTrue(RepositoryCache.isCached(repoB)); + assertTrue(RepositoryCache.isCached(repoC)); + + RepositoryCache.clearExpired(); + + assertEquals(2, RepositoryCache.getRegisteredKeys().size()); + assertFalse(RepositoryCache.isCached(repoA)); + assertTrue(RepositoryCache.isCached(repoB)); + assertTrue(RepositoryCache.isCached(repoC)); + } + + @Test + public void testReconfigure() throws InterruptedException { + RepositoryCache.register(db); + assertTrue(RepositoryCache.isCached(db)); + db.close(); + assertTrue(RepositoryCache.isCached(db)); + + // Actually, we would only need to validate that + // WorkQueue.getExecutor().scheduleWithFixedDelay is called with proper + // values but since we do not have a mock library, we test + // reconfiguration from a black box perspective. I.e. reconfigure + // expireAfter and cleanupDelay to 1 ms and wait until the Repository + // is evicted to prove that reconfiguration worked. + RepositoryCacheConfig config = new RepositoryCacheConfig(); + config.setExpireAfter(1); + config.setCleanupDelay(1); + config.install(); + + // Instead of using a fixed waiting time, start with small and increase: + // sleep 1, 2, 4, 8, 16, ..., 1024 ms + // This wait will time out after 2048 ms + for (int i = 0; i <= 10; i++) { + Thread.sleep(1 << i); + if (!RepositoryCache.isCached(db)) { + return; + } + } + fail("Repository should have been evicted from cache"); } } 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 909488ced..e68bca032 100644 --- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties +++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties @@ -185,6 +185,7 @@ corruptObjectTruncatedInMode=truncated in mode corruptObjectTruncatedInName=truncated in name corruptObjectTruncatedInObjectId=truncated in object id corruptObjectZeroId=entry points to null SHA-1 +corruptUseCnt=close() called when useCnt is already zero couldNotCheckOutBecauseOfConflicts=Could not check out because of conflicts couldNotDeleteLockFileShouldNotHappen=Could not delete lock file. Should not happen couldNotDeleteTemporaryIndexFileShouldNotHappen=Could not delete temporary index file. Should not happen @@ -339,7 +340,6 @@ invalidId0=Invalid id invalidIdLength=Invalid id length {0}; should be {1} invalidIgnoreParamSubmodule=Found invalid ignore param for submodule {0}. invalidIgnoreRule=Exception caught while parsing ignore rule ''{0}''. -invalidIncludedPathInConfigFile=Invalid included path in config file: {0} invalidIntegerValue=Invalid integer value: {0}.{1}={2} invalidKey=Invalid key: {0} invalidLineInConfigFile=Invalid line in config file 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 928cc8605..b7ef0854c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java @@ -245,6 +245,7 @@ public class JGitText extends TranslationBundle { /***/ public String corruptObjectTruncatedInObjectId; /***/ public String corruptObjectZeroId; /***/ public String corruptPack; + /***/ public String corruptUseCnt; /***/ public String couldNotCheckOutBecauseOfConflicts; /***/ public String couldNotDeleteLockFileShouldNotHappen; /***/ public String couldNotDeleteTemporaryIndexFileShouldNotHappen; @@ -398,7 +399,6 @@ public class JGitText extends TranslationBundle { /***/ public String invalidIdLength; /***/ public String invalidIgnoreParamSubmodule; /***/ public String invalidIgnoreRule; - /***/ public String invalidIncludedPathInConfigFile; /***/ public String invalidIntegerValue; /***/ public String invalidKey; /***/ public String invalidLineInConfigFile; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BatchingProgressMonitor.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BatchingProgressMonitor.java index 39856c0c9..a3859abb2 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/BatchingProgressMonitor.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/BatchingProgressMonitor.java @@ -43,55 +43,11 @@ package org.eclipse.jgit.lib; -import java.util.concurrent.Executors; import java.util.concurrent.Future; -import java.util.concurrent.ScheduledThreadPoolExecutor; -import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; /** ProgressMonitor that batches update events. */ public abstract class BatchingProgressMonitor implements ProgressMonitor { - private static final ScheduledThreadPoolExecutor alarmQueue; - - static final Object alarmQueueKiller; - - static { - // To support garbage collection, start our thread but - // swap out the thread factory. When our class is GC'd - // the alarmQueueKiller will finalize and ask the executor - // to shutdown, ending the worker. - // - int threads = 1; - alarmQueue = new ScheduledThreadPoolExecutor(threads, - new ThreadFactory() { - private final ThreadFactory baseFactory = Executors - .defaultThreadFactory(); - - public Thread newThread(Runnable taskBody) { - Thread thr = baseFactory.newThread(taskBody); - thr.setName("JGit-AlarmQueue"); //$NON-NLS-1$ - thr.setDaemon(true); - return thr; - } - }); - alarmQueue.setContinueExistingPeriodicTasksAfterShutdownPolicy(false); - alarmQueue.setExecuteExistingDelayedTasksAfterShutdownPolicy(false); - alarmQueue.prestartAllCoreThreads(); - - // Now that the threads are running, its critical to swap out - // our own thread factory for one that isn't in the ClassLoader. - // This allows the class to GC. - // - alarmQueue.setThreadFactory(Executors.defaultThreadFactory()); - - alarmQueueKiller = new Object() { - @Override - protected void finalize() { - alarmQueue.shutdownNow(); - } - }; - } - private long delayStartTime; private TimeUnit delayStartUnit = TimeUnit.MILLISECONDS; @@ -219,7 +175,7 @@ public abstract class BatchingProgressMonitor implements ProgressMonitor { void delay(long time, TimeUnit unit) { display = false; - timerFuture = alarmQueue.schedule(this, time, unit); + timerFuture = WorkQueue.getExecutor().schedule(this, time, unit); } public void run() { @@ -254,7 +210,8 @@ public abstract class BatchingProgressMonitor implements ProgressMonitor { private void restartTimer() { display = false; - timerFuture = alarmQueue.schedule(this, 1, TimeUnit.SECONDS); + timerFuture = WorkQueue.getExecutor().schedule(this, 1, + TimeUnit.SECONDS); } void end(BatchingProgressMonitor pm) { diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java index 40829f4f6..a041bb70c 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Config.java @@ -52,6 +52,7 @@ package org.eclipse.jgit.lib; import java.io.File; +import java.io.FileNotFoundException; import java.io.IOException; import java.text.MessageFormat; import java.util.ArrayList; @@ -82,7 +83,7 @@ public class Config { private static final long KiB = 1024; private static final long MiB = 1024 * KiB; private static final long GiB = 1024 * MiB; - private static final int MAX_DEPTH = 999; + private static final int MAX_DEPTH = 10; /** the change listeners */ private final ListenerList listeners = new ListenerList(); @@ -1247,9 +1248,15 @@ public class Config { decoded = RawParseUtils.decode(bytes); } newEntries.addAll(fromTextRecurse(decoded, depth + 1)); + } catch (FileNotFoundException fnfe) { + if (path.exists()) { + throw new ConfigInvalidException(MessageFormat + .format(JGitText.get().cannotReadFile, path), fnfe); + } } catch (IOException ioe) { - throw new ConfigInvalidException(MessageFormat.format( - JGitText.get().invalidIncludedPathInConfigFile, path)); + throw new ConfigInvalidException( + MessageFormat.format(JGitText.get().cannotReadFile, path), + ioe); } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java index 5703dddf1..7ec24998b 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/Repository.java @@ -63,6 +63,7 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.annotations.Nullable; @@ -93,6 +94,8 @@ import org.eclipse.jgit.util.IO; import org.eclipse.jgit.util.RawParseUtils; import org.eclipse.jgit.util.SystemReader; import org.eclipse.jgit.util.io.SafeBufferedOutputStream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Represents a Git repository. @@ -103,6 +106,8 @@ import org.eclipse.jgit.util.io.SafeBufferedOutputStream; * This class is thread-safe. */ public abstract class Repository implements AutoCloseable { + private static Logger LOG = LoggerFactory.getLogger(Repository.class); + private static final ListenerList globalListeners = new ListenerList(); /** @return the global listener list observing all events in this JVM. */ @@ -113,6 +118,8 @@ public abstract class Repository implements AutoCloseable { /** Use counter */ final AtomicInteger useCnt = new AtomicInteger(1); + final AtomicLong closedAt = new AtomicLong(); + /** Metadata directory holding the repository's critical files. */ private final File gitDir; @@ -863,9 +870,24 @@ public abstract class Repository implements AutoCloseable { /** Decrement the use count, and maybe close resources. */ public void close() { - if (useCnt.decrementAndGet() == 0) { - doClose(); - RepositoryCache.unregister(this); + int newCount = useCnt.decrementAndGet(); + if (newCount == 0) { + if (RepositoryCache.isCached(this)) { + closedAt.set(System.currentTimeMillis()); + } else { + doClose(); + } + } else if (newCount == -1) { + // should not happen, only log when useCnt became negative to + // minimize number of log entries + LOG.warn(JGitText.get().corruptUseCnt); + if (LOG.isDebugEnabled()) { + IllegalStateException e = new IllegalStateException(); + LOG.debug("", e); //$NON-NLS-1$ + } + if (RepositoryCache.isCached(this)) { + closedAt.set(System.currentTimeMillis()); + } } } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryCache.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryCache.java index 22b5fcd11..29ef08483 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryCache.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryCache.java @@ -52,17 +52,26 @@ import java.util.Collection; import java.util.Iterator; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.errors.RepositoryNotFoundException; import org.eclipse.jgit.internal.storage.file.FileRepository; import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.IO; import org.eclipse.jgit.util.RawParseUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** Cache of active {@link Repository} instances. */ public class RepositoryCache { private static final RepositoryCache cache = new RepositoryCache(); + private final static Logger LOG = LoggerFactory + .getLogger(RepositoryCache.class); + /** * Open an existing repository, reusing a cached instance if possible. *
@@ -138,10 +147,10 @@ public class RepositoryCache {
* @param db
* repository to unregister.
*/
- public static void close(final Repository db) {
+ public static void close(@NonNull final Repository db) {
if (db.getDirectory() != null) {
FileKey key = FileKey.exact(db.getDirectory(), db.getFS());
- cache.unregisterAndCloseRepository(key);
+ cache.unregisterAndCloseRepository(key, db);
}
}
@@ -187,20 +196,69 @@ public class RepositoryCache {
return cache.getKeys();
}
+ static boolean isCached(@NonNull Repository repo) {
+ File gitDir = repo.getDirectory();
+ if (gitDir == null) {
+ return false;
+ }
+ FileKey key = new FileKey(gitDir, repo.getFS());
+ Reference
+ * Set it to {@link #NO_CLEANUP} in order to switch off cache
+ * expiration.
+ *
+ * If cache expiration is switched off the JVM still can evict
+ * cache entries when the JVM is running low on available heap
+ * memory.
+ */
+ public void setCleanupDelay(long cleanupDelayMillis) {
+ this.cleanupDelayMillis = cleanupDelayMillis;
+ }
+
+ /**
+ * Update properties by setting fields from the configuration.
+ *
+ * If a property is not defined in the configuration, then it is left
+ * unmodified.
+ *
+ * @param config
+ * configuration to read properties from.
+ * @return {@code this}.
+ */
+ public RepositoryCacheConfig fromConfig(Config config) {
+ setExpireAfter(
+ config.getTimeUnit("core", null, "repositoryCacheExpireAfter", //$NON-NLS-1$//$NON-NLS-2$
+ getExpireAfter(), TimeUnit.MILLISECONDS));
+ setCleanupDelay(
+ config.getTimeUnit("core", null, "repositoryCacheCleanupDelay", //$NON-NLS-1$ //$NON-NLS-2$
+ AUTO_CLEANUP_DELAY, TimeUnit.MILLISECONDS));
+ return this;
+ }
+
+ /**
+ * Install this configuration as the live settings.
+ *
+ * The new configuration is applied immediately.
+ */
+ public void install() {
+ RepositoryCache.reconfigure(this);
+ }
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/WorkQueue.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/WorkQueue.java
new file mode 100644
index 000000000..9735d19e5
--- /dev/null
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/WorkQueue.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2008-2016, Google Inc.
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.eclipse.jgit.lib;
+
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ThreadFactory;
+
+/**
+ * Simple work queue to run tasks in the background
+ */
+class WorkQueue {
+ private static final ScheduledThreadPoolExecutor executor;
+
+ static final Object executorKiller;
+
+ static {
+ // To support garbage collection, start our thread but
+ // swap out the thread factory. When our class is GC'd
+ // the executorKiller will finalize and ask the executor
+ // to shutdown, ending the worker.
+ //
+ int threads = 1;
+ executor = new ScheduledThreadPoolExecutor(threads,
+ new ThreadFactory() {
+ private final ThreadFactory baseFactory = Executors
+ .defaultThreadFactory();
+
+ public Thread newThread(Runnable taskBody) {
+ Thread thr = baseFactory.newThread(taskBody);
+ thr.setName("JGit-WorkQueue"); //$NON-NLS-1$
+ thr.setDaemon(true);
+ return thr;
+ }
+ });
+ executor.setRemoveOnCancelPolicy(true);
+ executor.setContinueExistingPeriodicTasksAfterShutdownPolicy(false);
+ executor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
+ executor.prestartAllCoreThreads();
+
+ // Now that the threads are running, its critical to swap out
+ // our own thread factory for one that isn't in the ClassLoader.
+ // This allows the class to GC.
+ //
+ executor.setThreadFactory(Executors.defaultThreadFactory());
+
+ executorKiller = new Object() {
+ @Override
+ protected void finalize() {
+ executor.shutdownNow();
+ }
+ };
+ }
+
+ static ScheduledThreadPoolExecutor getExecutor() {
+ return executor;
+ }
+}
diff --git a/pom.xml b/pom.xml
index 59bce4370..d256bbb7b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -188,9 +188,10 @@