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 repoRef = cache.cacheMap.get(key); + return repoRef != null && repoRef.get() == repo; + } + /** Unregister all repositories from the cache. */ public static void clear() { cache.clearAll(); } + static void clearExpired() { + cache.clearAllExpired(); + } + + static void reconfigure(RepositoryCacheConfig repositoryCacheConfig) { + cache.configureEviction(repositoryCacheConfig); + } + private final ConcurrentHashMap> cacheMap; private final Lock[] openLocks; + private ScheduledFuture cleanupTask; + + private volatile long expireAfter; + private RepositoryCache() { cacheMap = new ConcurrentHashMap>(); openLocks = new Lock[4]; - for (int i = 0; i < openLocks.length; i++) + for (int i = 0; i < openLocks.length; i++) { openLocks[i] = new Lock(); + } + configureEviction(new RepositoryCacheConfig()); + } + + private void configureEviction( + RepositoryCacheConfig repositoryCacheConfig) { + expireAfter = repositoryCacheConfig.getExpireAfter(); + ScheduledThreadPoolExecutor scheduler = WorkQueue.getExecutor(); + synchronized (scheduler) { + if (cleanupTask != null) { + cleanupTask.cancel(false); + } + long delay = repositoryCacheConfig.getCleanupDelay(); + if (delay == RepositoryCacheConfig.NO_CLEANUP) { + return; + } + cleanupTask = scheduler.scheduleWithFixedDelay(new Runnable() { + @Override + public void run() { + try { + cache.clearAllExpired(); + } catch (Throwable e) { + LOG.error(e.getMessage(), e); + } + } + }, delay, delay, TimeUnit.MILLISECONDS); + } } @SuppressWarnings("resource") @@ -239,10 +297,20 @@ public class RepositoryCache { return oldRef != null ? oldRef.get() : null; } - private void unregisterAndCloseRepository(final Key location) { - Repository oldDb = unregisterRepository(location); - if (oldDb != null) { - oldDb.close(); + private boolean isExpired(Repository db) { + return db != null && db.useCnt.get() == 0 + && (System.currentTimeMillis() - db.closedAt.get() > expireAfter); + } + + private void unregisterAndCloseRepository(final Key location, + Repository db) { + synchronized (lockFor(location)) { + if (isExpired(db)) { + Repository oldDb = unregisterRepository(location); + if (oldDb != null) { + oldDb.close(); + } + } } } @@ -250,6 +318,15 @@ public class RepositoryCache { return new ArrayList(cacheMap.keySet()); } + private void clearAllExpired() { + for (Reference ref : cacheMap.values()) { + Repository db = ref.get(); + if (isExpired(db)) { + RepositoryCache.close(db); + } + } + } + private void clearAll() { for (int stage = 0; stage < 2; stage++) { for (Iterator>> i = cacheMap diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryCacheConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryCacheConfig.java new file mode 100644 index 000000000..428dea3e6 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/RepositoryCacheConfig.java @@ -0,0 +1,152 @@ +/* + * 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 java.util.concurrent.TimeUnit; + +/** + * Configuration parameters for JVM-wide repository cache used by JGit. + * + * @since 4.4 + */ +public class RepositoryCacheConfig { + + /** + * Set cleanupDelayMillis to this value in order to switch off time-based + * cache eviction. The JVM can still expire cache entries when heap memory + * runs low. + */ + public static final long NO_CLEANUP = 0; + + /** + * Set cleanupDelayMillis to this value in order to auto-set it to minimum + * of 1/10 of expireAfterMillis and 10 minutes + */ + public static final long AUTO_CLEANUP_DELAY = -1; + + private long expireAfterMillis; + + private long cleanupDelayMillis; + + /** Create a default configuration. */ + public RepositoryCacheConfig() { + expireAfterMillis = TimeUnit.HOURS.toMillis(1); + cleanupDelayMillis = AUTO_CLEANUP_DELAY; + } + + /** + * @return the time an unused repository should expired and be evicted from + * the RepositoryCache in milliseconds. Default is 1 hour. + */ + public long getExpireAfter() { + return expireAfterMillis; + } + + /** + * @param expireAfterMillis + * the time an unused repository should expired and be evicted + * from the RepositoryCache in milliseconds. + */ + public void setExpireAfter(long expireAfterMillis) { + this.expireAfterMillis = expireAfterMillis; + } + + /** + * @return the delay between the periodic cleanup of expired repository in + * milliseconds. Default is minimum of 1/10 of expireAfterMillis + * and 10 minutes + */ + public long getCleanupDelay() { + if (cleanupDelayMillis < 0) { + return Math.min(expireAfterMillis / 10, + TimeUnit.MINUTES.toMillis(10)); + } + return cleanupDelayMillis; + } + + /** + * @param cleanupDelayMillis + * the delay between the periodic cleanup of expired repository + * in milliseconds. Set it to {@link #AUTO_CLEANUP_DELAY} to + * automatically derive cleanup delay from expireAfterMillis. + *

+ * 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 @@ UTF-8 UTF-8 - yyyyMMddHHmm ${project.build.directory}/META-INF/MANIFEST.MF + + ${JAVA_HOME} 4.2.0.201601211800-r 0.1.53 @@ -264,6 +265,9 @@ UTF-8 1.7 1.7 + + ${JDK_HOME}${file.separator}jre${file.separator}lib${file.separator}rt.jar${path.separator}${JDK_HOME}${file.separator}jre${file.separator}lib${file.separator}jsse.jar${path.separator}${JDK_HOME}${file.separator}jre${file.separator}lib${file.separator}jce.jar +