| /* | |
| * 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; | |
| } | |
| } |