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