/* * 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.manager.IRepositoryManager; 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 gcCache = new ConcurrentHashMap(); 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(); IRepositoryManager repositoryManager = GitBlit.getManager(IRepositoryManager.class); for (String repositoryName : repositoryManager.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 = repositoryManager.getRepositoryModel(repositoryName); repository = repositoryManager.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(); repositoryManager.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; } }