From f66e89662c091e082bd1d2feb6ac91513ccff273 Mon Sep 17 00:00:00 2001 From: Rafael Cavazin <rafaelcavazin@gmail.com> Date: Sun, 21 Jul 2013 09:59:00 -0400 Subject: [PATCH] Merge branch 'master' of https://github.com/gitblit/gitblit --- src/main/java/com/gitblit/GCExecutor.java | 239 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 files changed, 239 insertions(+), 0 deletions(-) diff --git a/src/main/java/com/gitblit/GCExecutor.java b/src/main/java/com/gitblit/GCExecutor.java new file mode 100644 index 0000000..0a0c8ad --- /dev/null +++ b/src/main/java/com/gitblit/GCExecutor.java @@ -0,0 +1,239 @@ +/* + * Copyright 2012 gitblit.com. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.gitblit; + +import java.lang.reflect.Field; +import java.text.MessageFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import org.eclipse.jgit.api.GarbageCollectCommand; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.lib.Repository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.gitblit.models.RepositoryModel; +import com.gitblit.utils.FileUtils; + +/** + * The GC executor handles periodic garbage collection in repositories. + * + * @author James Moger + * + */ +public class GCExecutor implements Runnable { + + public static enum GCStatus { + READY, COLLECTING; + + public boolean exceeds(GCStatus s) { + return ordinal() > s.ordinal(); + } + } + private final Logger logger = LoggerFactory.getLogger(GCExecutor.class); + + private final IStoredSettings settings; + + private AtomicBoolean running = new AtomicBoolean(false); + + private AtomicBoolean forceClose = new AtomicBoolean(false); + + private final Map<String, GCStatus> gcCache = new ConcurrentHashMap<String, GCStatus>(); + + public GCExecutor(IStoredSettings settings) { + this.settings = settings; + } + + /** + * Indicates if the GC executor is ready to process repositories. + * + * @return true if the GC executor is ready to process repositories + */ + public boolean isReady() { + return settings.getBoolean(Keys.git.enableGarbageCollection, false); + } + + public boolean isRunning() { + return running.get(); + } + + public boolean lock(String repositoryName) { + return setGCStatus(repositoryName, GCStatus.COLLECTING); + } + + /** + * Tries to set a GCStatus for the specified repository. + * + * @param repositoryName + * @return true if the status has been set + */ + private boolean setGCStatus(String repositoryName, GCStatus status) { + String key = repositoryName.toLowerCase(); + if (gcCache.containsKey(key)) { + if (gcCache.get(key).exceeds(GCStatus.READY)) { + // already collecting or blocked + return false; + } + } + gcCache.put(key, status); + return true; + } + + /** + * Returns true if Gitblit is actively collecting garbage in this repository. + * + * @param repositoryName + * @return true if actively collecting garbage + */ + public boolean isCollectingGarbage(String repositoryName) { + String key = repositoryName.toLowerCase(); + return gcCache.containsKey(key) && GCStatus.COLLECTING.equals(gcCache.get(key)); + } + + /** + * Resets the GC status to ready. + * + * @param repositoryName + */ + public void releaseLock(String repositoryName) { + gcCache.put(repositoryName.toLowerCase(), GCStatus.READY); + } + + public void close() { + forceClose.set(true); + } + + @Override + public void run() { + if (!isReady()) { + return; + } + + running.set(true); + Date now = new Date(); + + for (String repositoryName : GitBlit.self().getRepositoryList()) { + if (forceClose.get()) { + break; + } + if (isCollectingGarbage(repositoryName)) { + logger.warn(MessageFormat.format("Already collecting garbage from {0}?!?", repositoryName)); + continue; + } + boolean garbageCollected = false; + RepositoryModel model = null; + Repository repository = null; + try { + model = GitBlit.self().getRepositoryModel(repositoryName); + repository = GitBlit.self().getRepository(repositoryName); + if (repository == null) { + logger.warn(MessageFormat.format("GCExecutor is missing repository {0}?!?", repositoryName)); + continue; + } + + if (!isRepositoryIdle(repository)) { + logger.debug(MessageFormat.format("GCExecutor is skipping {0} because it is not idle", repositoryName)); + continue; + } + + // By setting the GCStatus to COLLECTING we are + // disabling *all* access to this repository from Gitblit. + // Think of this as a clutch in a manual transmission vehicle. + if (!setGCStatus(repositoryName, GCStatus.COLLECTING)) { + logger.warn(MessageFormat.format("Can not acquire GC lock for {0}, skipping", repositoryName)); + continue; + } + + logger.debug(MessageFormat.format("GCExecutor locked idle repository {0}", repositoryName)); + + Git git = new Git(repository); + GarbageCollectCommand gc = git.gc(); + Properties stats = gc.getStatistics(); + + // determine if this is a scheduled GC + Calendar cal = Calendar.getInstance(); + cal.setTime(model.lastGC); + cal.set(Calendar.HOUR_OF_DAY, 0); + cal.set(Calendar.MINUTE, 0); + cal.set(Calendar.SECOND, 0); + cal.set(Calendar.MILLISECOND, 0); + cal.add(Calendar.DATE, model.gcPeriod); + Date gcDate = cal.getTime(); + boolean shouldCollectGarbage = now.after(gcDate); + + // determine if filesize triggered GC + long gcThreshold = FileUtils.convertSizeToLong(model.gcThreshold, 500*1024L); + long sizeOfLooseObjects = (Long) stats.get("sizeOfLooseObjects"); + boolean hasEnoughGarbage = sizeOfLooseObjects >= gcThreshold; + + // if we satisfy one of the requirements, GC + boolean hasGarbage = sizeOfLooseObjects > 0; + if (hasGarbage && (hasEnoughGarbage || shouldCollectGarbage)) { + long looseKB = sizeOfLooseObjects/1024L; + logger.info(MessageFormat.format("Collecting {1} KB of loose objects from {0}", repositoryName, looseKB)); + + // do the deed + gc.call(); + + garbageCollected = true; + } + } catch (Exception e) { + logger.error("Error collecting garbage in " + repositoryName, e); + } finally { + // cleanup + if (repository != null) { + if (garbageCollected) { + // update the last GC date + model.lastGC = new Date(); + GitBlit.self().updateConfiguration(repository, model); + } + + repository.close(); + } + + // reset the GC lock + releaseLock(repositoryName); + logger.debug(MessageFormat.format("GCExecutor released GC lock for {0}", repositoryName)); + } + } + + running.set(false); + } + + private boolean isRepositoryIdle(Repository repository) { + try { + // Read the use count. + // An idle use count is 2: + // +1 for being in the cache + // +1 for the repository parameter in this method + Field useCnt = Repository.class.getDeclaredField("useCnt"); + useCnt.setAccessible(true); + int useCount = ((AtomicInteger) useCnt.get(repository)).get(); + return useCount == 2; + } catch (Exception e) { + logger.warn(MessageFormat + .format("Failed to reflectively determine use count for repository {0}", + repository.getDirectory().getPath()), e); + } + return false; + } +} -- Gitblit v1.9.1