diff options
author | James Moger <james.moger@gitblit.com> | 2012-10-28 13:27:26 -0400 |
---|---|---|
committer | James Moger <james.moger@gitblit.com> | 2012-10-28 13:27:26 -0400 |
commit | e92c6d230b3a350749fdb9fa2150bb1773260b8c (patch) | |
tree | 135b01ea4c8174f5e79725dd2b99d72b01cc50f1 /src/com/gitblit | |
parent | 5b831a0981491cabbbdbb02dba237308947a5e7d (diff) | |
download | gitblit-e92c6d230b3a350749fdb9fa2150bb1773260b8c.tar.gz gitblit-e92c6d230b3a350749fdb9fa2150bb1773260b8c.zip |
Experimental JGit-based GC Executor
Diffstat (limited to 'src/com/gitblit')
-rw-r--r-- | src/com/gitblit/AccessRestrictionFilter.java | 6 | ||||
-rw-r--r-- | src/com/gitblit/Constants.java | 10 | ||||
-rw-r--r-- | src/com/gitblit/DownloadZipServlet.java | 11 | ||||
-rw-r--r-- | src/com/gitblit/FederationPullExecutor.java | 8 | ||||
-rw-r--r-- | src/com/gitblit/GCExecutor.java | 229 | ||||
-rw-r--r-- | src/com/gitblit/GitBlit.java | 116 | ||||
-rw-r--r-- | src/com/gitblit/LuceneExecutor.java | 6 | ||||
-rw-r--r-- | src/com/gitblit/RpcServlet.java | 5 | ||||
-rw-r--r-- | src/com/gitblit/SyndicationServlet.java | 8 | ||||
-rw-r--r-- | src/com/gitblit/models/RepositoryModel.java | 5 | ||||
-rw-r--r-- | src/com/gitblit/utils/ActivityUtils.java | 3 | ||||
-rw-r--r-- | src/com/gitblit/wicket/GitBlitWebApp.properties | 8 | ||||
-rw-r--r-- | src/com/gitblit/wicket/pages/EditRepositoryPage.html | 34 | ||||
-rw-r--r-- | src/com/gitblit/wicket/pages/EditRepositoryPage.java | 4 | ||||
-rw-r--r-- | src/com/gitblit/wicket/pages/RepositoryPage.java | 21 | ||||
-rw-r--r-- | src/com/gitblit/wicket/panels/BranchesPanel.java | 8 |
16 files changed, 442 insertions, 40 deletions
diff --git a/src/com/gitblit/AccessRestrictionFilter.java b/src/com/gitblit/AccessRestrictionFilter.java index 78d33d21..495d3438 100644 --- a/src/com/gitblit/AccessRestrictionFilter.java +++ b/src/com/gitblit/AccessRestrictionFilter.java @@ -125,6 +125,12 @@ public abstract class AccessRestrictionFilter extends AuthenticationFilter { String fullUrl = getFullUrl(httpRequest);
String repository = extractRepositoryName(fullUrl);
+
+ if (GitBlit.self().isCollectingGarbage(repository)) {
+ logger.info(MessageFormat.format("ARF: Rejecting request for {0}, busy collecting garbage!", repository));
+ httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN);
+ return;
+ }
// Determine if the request URL is restricted
String fullSuffix = fullUrl.substring(repository.length());
diff --git a/src/com/gitblit/Constants.java b/src/com/gitblit/Constants.java index 33cf2873..f74317ea 100644 --- a/src/com/gitblit/Constants.java +++ b/src/com/gitblit/Constants.java @@ -86,6 +86,8 @@ public class Constants { public static final String CONFIG_CUSTOM_FIELDS = "customFields";
+ public static final String ISO8601 = "yyyy-MM-dd'T'HH:mm:ssZ";
+
public static String getGitBlitVersion() {
return NAME + " v" + VERSION;
}
@@ -384,6 +386,14 @@ public class Constants { REPOSITORY, USER, TEAM;
}
+ public static enum GCStatus {
+ READY, COLLECTING;
+
+ public boolean exceeds(GCStatus s) {
+ return ordinal() > s.ordinal();
+ }
+ }
+
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface Unused {
diff --git a/src/com/gitblit/DownloadZipServlet.java b/src/com/gitblit/DownloadZipServlet.java index 209272f2..26559344 100644 --- a/src/com/gitblit/DownloadZipServlet.java +++ b/src/com/gitblit/DownloadZipServlet.java @@ -101,11 +101,16 @@ public class DownloadZipServlet extends HttpServlet { if (!StringUtils.isEmpty(objectId)) {
name += "-" + objectId;
}
-
+
Repository r = GitBlit.self().getRepository(repository);
if (r == null) {
- error(response, MessageFormat.format("# Error\nFailed to find repository {0}", repository));
- return;
+ if (GitBlit.self().isCollectingGarbage(repository)) {
+ error(response, MessageFormat.format("# Error\nGitblit is busy collecting garbage in {0}", repository));
+ return;
+ } else {
+ error(response, MessageFormat.format("# Error\nFailed to find repository {0}", repository));
+ return;
+ }
}
RevCommit commit = JGitUtils.getCommit(r, objectId);
if (commit == null) {
diff --git a/src/com/gitblit/FederationPullExecutor.java b/src/com/gitblit/FederationPullExecutor.java index 19afacc7..ad1022cf 100644 --- a/src/com/gitblit/FederationPullExecutor.java +++ b/src/com/gitblit/FederationPullExecutor.java @@ -189,11 +189,17 @@ public class FederationPullExecutor implements Runnable { repositoryName.indexOf(DOT_GIT_EXT));
}
}
-
+
// confirm that the origin of any pre-existing repository matches
// the clone url
String fetchHead = null;
Repository existingRepository = GitBlit.self().getRepository(repositoryName);
+
+ if (existingRepository == null && GitBlit.self().isCollectingGarbage(repositoryName)) {
+ logger.warn(MessageFormat.format("Skipping local repository {0}, busy collecting garbage", repositoryName));
+ continue;
+ }
+
if (existingRepository != null) {
StoredConfig config = existingRepository.getConfig();
config.load();
diff --git a/src/com/gitblit/GCExecutor.java b/src/com/gitblit/GCExecutor.java new file mode 100644 index 00000000..c5fe43b4 --- /dev/null +++ b/src/com/gitblit/GCExecutor.java @@ -0,0 +1,229 @@ +/*
+ * 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.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.storage.file.FileRepository;
+import org.eclipse.jgit.storage.file.GC;
+import org.eclipse.jgit.storage.file.GC.RepoStatistics;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.gitblit.models.RepositoryModel;
+import com.gitblit.utils.FileUtils;
+import com.gitblit.utils.TimeUtils;
+
+/**
+ * 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 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 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;
+ }
+ 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;
+ FileRepository repository = null;
+ try {
+ model = GitBlit.self().getRepositoryModel(repositoryName);
+ repository = (FileRepository) 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));
+
+ GC gc = new GC(repository);
+ RepoStatistics stats = gc.getStatistics();
+
+ // determine if this is a scheduled GC
+ int gcPeriodInDays = TimeUtils.convertFrequencyToMinutes(model.gcPeriod)/(60*24);
+ 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, gcPeriodInDays);
+ Date gcDate = cal.getTime();
+ boolean shouldCollectGarbage = now.after(gcDate);
+
+ // determine if filesize triggered GC
+ long gcThreshold = FileUtils.convertSizeToLong(model.gcThreshold, 500*1024L);
+ boolean hasEnoughGarbage = stats.sizeOfLooseObjects >= gcThreshold;
+
+ // if we satisfy one of the requirements, GC
+ boolean hasGarbage = stats.sizeOfLooseObjects > 0;
+ if (hasGarbage && (hasEnoughGarbage || shouldCollectGarbage)) {
+ long looseKB = stats.sizeOfLooseObjects/1024L;
+ logger.info(MessageFormat.format("Collecting {1} KB of loose objects from {0}", repositoryName, looseKB));
+
+ // do the deed
+ gc.gc();
+
+ 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));
+ }
+ }
+ }
+
+ private boolean isRepositoryIdle(FileRepository 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;
+ }
+}
diff --git a/src/com/gitblit/GitBlit.java b/src/com/gitblit/GitBlit.java index e83da933..402f600d 100644 --- a/src/com/gitblit/GitBlit.java +++ b/src/com/gitblit/GitBlit.java @@ -28,6 +28,7 @@ import java.text.MessageFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
@@ -103,6 +104,7 @@ import com.gitblit.utils.JsonUtils; import com.gitblit.utils.MetricUtils;
import com.gitblit.utils.ObjectCache;
import com.gitblit.utils.StringUtils;
+import com.gitblit.utils.TimeUtils;
import com.gitblit.wicket.WicketUtils;
/**
@@ -160,6 +162,8 @@ public class GitBlit implements ServletContextListener { private LuceneExecutor luceneExecutor;
+ private GCExecutor gcExecutor;
+
private TimeZone timezone;
private FileBasedConfig projectConfigs;
@@ -251,6 +255,34 @@ public class GitBlit implements ServletContextListener { public static int getInteger(String key, int defaultValue) {
return self().settings.getInteger(key, defaultValue);
}
+
+ /**
+ * Returns the value in bytes for the specified key. If the key does not
+ * exist or the value for the key can not be interpreted as an integer, the
+ * defaultValue is returned.
+ *
+ * @see IStoredSettings.getFilesize(String key, int defaultValue)
+ * @param key
+ * @param defaultValue
+ * @return key value or defaultValue
+ */
+ public static int getFilesize(String key, int defaultValue) {
+ return self().settings.getFilesize(key, defaultValue);
+ }
+
+ /**
+ * Returns the value in bytes for the specified key. If the key does not
+ * exist or the value for the key can not be interpreted as a long, the
+ * defaultValue is returned.
+ *
+ * @see IStoredSettings.getFilesize(String key, long defaultValue)
+ * @param key
+ * @param defaultValue
+ * @return key value or defaultValue
+ */
+ public static long getFilesize(String key, long defaultValue) {
+ return self().settings.getFilesize(key, defaultValue);
+ }
/**
* Returns the char value for the specified key. If the key does not exist
@@ -1018,10 +1050,15 @@ public class GitBlit implements ServletContextListener { * @return repository or null
*/
public Repository getRepository(String repositoryName, boolean logError) {
+ if (isCollectingGarbage(repositoryName)) {
+ logger.warn(MessageFormat.format("Rejecting request for {0}, busy collecting garbage!", repositoryName));
+ return null;
+ }
+
File dir = FileKey.resolve(new File(repositoriesFolder, repositoryName), FS.DETECTED);
if (dir == null)
return null;
-
+
Repository r = null;
try {
FileKey key = FileKey.exact(dir, FS.DETECTED);
@@ -1115,7 +1152,14 @@ public class GitBlit implements ServletContextListener { // cached model
RepositoryModel model = repositoryListCache.get(repositoryName);
-
+
+ if (gcExecutor.isCollectingGarbage(model.name)) {
+ // Gitblit is busy collecting garbage, use our cached model
+ RepositoryModel rm = DeepCopier.copy(model);
+ rm.isCollectingGarbage = true;
+ return rm;
+ }
+
// check for updates
Repository r = getRepository(repositoryName);
if (r == null) {
@@ -1180,12 +1224,6 @@ public class GitBlit implements ServletContextListener { }
project.title = projectConfigs.getString("project", name, "title");
project.description = projectConfigs.getString("project", name, "description");
- // TODO add more interesting metadata
- // project manager?
- // commit message regex?
- // RW+
- // RW
- // R
configs.put(name.toLowerCase(), project);
}
projectCache.clear();
@@ -1379,6 +1417,13 @@ public class GitBlit implements ServletContextListener { model.federationSets = new ArrayList<String>(Arrays.asList(config.getStringList(
Constants.CONFIG_GITBLIT, null, "federationSets")));
model.isFederated = getConfig(config, "isFederated", false);
+ model.gcThreshold = getConfig(config, "gcThreshold", settings.getString(Keys.git.defaultGarbageCollectionThreshold, "500KB"));
+ model.gcPeriod = getConfig(config, "gcPeriod", settings.getString(Keys.git.defaultGarbageCollectionPeriod, "7 days"));
+ try {
+ model.lastGC = new SimpleDateFormat(Constants.ISO8601).parse(getConfig(config, "lastGC", "1970-01-01'T'00:00:00Z"));
+ } catch (Exception e) {
+ model.lastGC = new Date(0);
+ }
model.origin = config.getString("remote", "origin", "url");
if (model.origin != null) {
model.origin = model.origin.replace('\\', '/');
@@ -1675,6 +1720,10 @@ public class GitBlit implements ServletContextListener { */
public void updateRepositoryModel(String repositoryName, RepositoryModel repository,
boolean isCreate) throws GitBlitException {
+ if (gcExecutor.isCollectingGarbage(repositoryName)) {
+ throw new GitBlitException(MessageFormat.format("sorry, Gitblit is busy collecting garbage in {0}",
+ repositoryName));
+ }
Repository r = null;
String projectPath = StringUtils.getFirstPathElement(repository.name);
if (!StringUtils.isEmpty(projectPath)) {
@@ -1819,6 +1868,9 @@ public class GitBlit implements ServletContextListener { config.setString(Constants.CONFIG_GITBLIT, null, "federationStrategy",
repository.federationStrategy.name());
config.setBoolean(Constants.CONFIG_GITBLIT, null, "isFederated", repository.isFederated);
+ config.setString(Constants.CONFIG_GITBLIT, null, "gcThreshold", repository.gcThreshold);
+ config.setString(Constants.CONFIG_GITBLIT, null, "gcPeriod", repository.gcPeriod);
+ config.setString(Constants.CONFIG_GITBLIT, null, "lastGC", new SimpleDateFormat(Constants.ISO8601).format(repository.lastGC));
updateList(config, "federationSets", repository.federationSets);
updateList(config, "preReceiveScript", repository.preReceiveScripts);
@@ -2614,6 +2666,12 @@ public class GitBlit implements ServletContextListener { public void configureContext(IStoredSettings settings, boolean startFederation) {
logger.info("Reading configuration from " + settings.toString());
this.settings = settings;
+
+ // prepare service executors
+ mailExecutor = new MailExecutor(settings);
+ luceneExecutor = new LuceneExecutor(settings, repositoriesFolder);
+ gcExecutor = new GCExecutor(settings);
+
repositoriesFolder = getRepositoriesFolder();
logger.info("Git repositories folder " + repositoriesFolder.getAbsolutePath());
@@ -2647,16 +2705,43 @@ public class GitBlit implements ServletContextListener { // load and cache the project metadata
projectConfigs = new FileBasedConfig(getFileOrFolder(Keys.web.projectsFile, "projects.conf"), FS.detect());
getProjectConfigs();
- mailExecutor = new MailExecutor(settings);
+
+ // schedule mail engine
if (mailExecutor.isReady()) {
logger.info("Mail executor is scheduled to process the message queue every 2 minutes.");
scheduledExecutor.scheduleAtFixedRate(mailExecutor, 1, 2, TimeUnit.MINUTES);
} else {
logger.warn("Mail server is not properly configured. Mail services disabled.");
}
- luceneExecutor = new LuceneExecutor(settings, repositoriesFolder);
+
+ // schedule lucene engine
logger.info("Lucene executor is scheduled to process indexed branches every 2 minutes.");
scheduledExecutor.scheduleAtFixedRate(luceneExecutor, 1, 2, TimeUnit.MINUTES);
+
+ // schedule gc engine
+ if (gcExecutor.isReady()) {
+ logger.info("GC executor is scheduled to scan repositories every 24 hours.");
+ Calendar c = Calendar.getInstance();
+ c.set(Calendar.HOUR_OF_DAY, settings.getInteger(Keys.git.garbageCollectionHour, 0));
+ c.set(Calendar.MINUTE, 0);
+ c.set(Calendar.SECOND, 0);
+ c.set(Calendar.MILLISECOND, 0);
+ Date cd = c.getTime();
+ Date now = new Date();
+ int delay = 0;
+ if (cd.before(now)) {
+ c.add(Calendar.DATE, 1);
+ cd = c.getTime();
+ }
+ delay = (int) ((cd.getTime() - now.getTime())/TimeUtils.MIN);
+ String when = delay + " mins";
+ if (delay > 60) {
+ when = MessageFormat.format("{0,number,0.0} hours", ((float)delay)/60f);
+ }
+ logger.info(MessageFormat.format("Next scheculed GC scan is in {0}", when));
+ scheduledExecutor.scheduleAtFixedRate(gcExecutor, delay, 60*24, TimeUnit.MINUTES);
+ }
+
if (startFederation) {
configureFederation();
}
@@ -2758,9 +2843,20 @@ public class GitBlit implements ServletContextListener { logger.info("Gitblit context destroyed by servlet container.");
scheduledExecutor.shutdownNow();
luceneExecutor.close();
+ gcExecutor.close();
}
/**
+ * Returns true if Gitblit is actively collecting garbage in this repository.
+ *
+ * @param repositoryName
+ * @return true if actively collecting garbage
+ */
+ public boolean isCollectingGarbage(String repositoryName) {
+ return gcExecutor.isCollectingGarbage(repositoryName);
+ }
+
+ /**
* Creates a personal fork of the specified repository. The clone is view
* restricted by default and the owner of the source repository is given
* access to the clone.
diff --git a/src/com/gitblit/LuceneExecutor.java b/src/com/gitblit/LuceneExecutor.java index 42155f42..dffd4788 100644 --- a/src/com/gitblit/LuceneExecutor.java +++ b/src/com/gitblit/LuceneExecutor.java @@ -171,6 +171,12 @@ public class LuceneExecutor implements Runnable { RepositoryModel model = GitBlit.self().getRepositoryModel(repositoryName);
if (model.hasCommits && !ArrayUtils.isEmpty(model.indexedBranches)) {
Repository repository = GitBlit.self().getRepository(model.name);
+ if (repository == null) {
+ if (GitBlit.self().isCollectingGarbage(model.name)) {
+ logger.info(MessageFormat.format("Skipping Lucene index of {0}, busy garbage collecting", repositoryName));
+ }
+ continue;
+ }
index(model, repository);
repository.close();
System.gc();
diff --git a/src/com/gitblit/RpcServlet.java b/src/com/gitblit/RpcServlet.java index 0c9ac379..f6368dd0 100644 --- a/src/com/gitblit/RpcServlet.java +++ b/src/com/gitblit/RpcServlet.java @@ -110,6 +110,11 @@ public class RpcServlet extends JsonServlet { // skip empty repository
continue;
}
+ if (model.isCollectingGarbage) {
+ // skip garbage collecting repository
+ logger.warn(MessageFormat.format("Temporarily excluding {0} from RPC, busy collecting garbage", model.name));
+ continue;
+ }
// get local branches
Repository repository = GitBlit.self().getRepository(model.name);
List<RefModel> refs = JGitUtils.getLocalBranches(repository, false, -1);
diff --git a/src/com/gitblit/SyndicationServlet.java b/src/com/gitblit/SyndicationServlet.java index a36f5839..baaf7eb7 100644 --- a/src/com/gitblit/SyndicationServlet.java +++ b/src/com/gitblit/SyndicationServlet.java @@ -210,7 +210,13 @@ public class SyndicationServlet extends HttpServlet { for (String name : repositories) {
Repository repository = GitBlit.self().getRepository(name);
RepositoryModel model = GitBlit.self().getRepositoryModel(name);
-
+
+ if (repository == null) {
+ if (model.isCollectingGarbage) {
+ logger.warn(MessageFormat.format("Temporarily excluding {0} from feed, busy collecting garbage", name));
+ }
+ continue;
+ }
if (!isProjectFeed) {
// single-repository feed
feedName = model.name;
diff --git a/src/com/gitblit/models/RepositoryModel.java b/src/com/gitblit/models/RepositoryModel.java index 502f886e..23ce9e3b 100644 --- a/src/com/gitblit/models/RepositoryModel.java +++ b/src/com/gitblit/models/RepositoryModel.java @@ -76,6 +76,11 @@ public class RepositoryModel implements Serializable, Comparable<RepositoryModel public Set<String> forks;
public String originRepository;
public boolean verifyCommitter;
+ public String gcThreshold;
+ public String gcPeriod;
+
+ public transient boolean isCollectingGarbage;
+ public Date lastGC;
public RepositoryModel() {
this("", "", "", new Date(0));
diff --git a/src/com/gitblit/utils/ActivityUtils.java b/src/com/gitblit/utils/ActivityUtils.java index e5159942..e389e642 100644 --- a/src/com/gitblit/utils/ActivityUtils.java +++ b/src/com/gitblit/utils/ActivityUtils.java @@ -82,6 +82,9 @@ public class ActivityUtils { Map<String, Activity> activity = new HashMap<String, Activity>();
for (RepositoryModel model : models) {
if (model.hasCommits && model.lastChange.after(thresholdDate)) {
+ if (model.isCollectingGarbage) {
+ continue;
+ }
Repository repository = GitBlit.self()
.getRepository(model.name);
List<String> branches = new ArrayList<String>();
diff --git a/src/com/gitblit/wicket/GitBlitWebApp.properties b/src/com/gitblit/wicket/GitBlitWebApp.properties index 09ee929b..62e4817f 100644 --- a/src/com/gitblit/wicket/GitBlitWebApp.properties +++ b/src/com/gitblit/wicket/GitBlitWebApp.properties @@ -357,4 +357,10 @@ gb.createPermission = {0} (push, ref creation) gb.deletePermission = {0} (push, ref creation+deletion)
gb.rewindPermission = {0} (push, ref creation+deletion+rewind)
gb.permission = permission
-gb.regexPermission = this permission is set from a regular expression
\ No newline at end of file +gb.regexPermission = this permission is set from a regular expression
+gb.accessDenied = access denied
+gb.busyCollectingGarbage = sorry, Gitblit is busy collecting garbage in {0}
+gb.gcPeriod = GC period
+gb.gcPeriodDescription = duration between garbage collections
+gb.gcThreshold = GC threshold
+gb.gcThresholdDescription = minimum total size of loose objects to trigger early garbage collection
diff --git a/src/com/gitblit/wicket/pages/EditRepositoryPage.html b/src/com/gitblit/wicket/pages/EditRepositoryPage.html index 23e2312f..7bd896cf 100644 --- a/src/com/gitblit/wicket/pages/EditRepositoryPage.html +++ b/src/com/gitblit/wicket/pages/EditRepositoryPage.html @@ -28,15 +28,17 @@ <tr><th><wicket:message key="gb.name"></wicket:message></th><td class="edit"><input class="span4" type="text" wicket:id="name" id="name" size="40" tabindex="1" /> <span class="help-inline"><wicket:message key="gb.nameDescription"></wicket:message></span></td></tr>
<tr><th><wicket:message key="gb.description"></wicket:message></th><td class="edit"><input class="span4" type="text" wicket:id="description" size="40" tabindex="2" /></td></tr>
<tr><th><wicket:message key="gb.origin"></wicket:message></th><td class="edit"><input class="span5" type="text" wicket:id="origin" size="80" tabindex="3" /></td></tr>
- <tr><th><wicket:message key="gb.headRef"></wicket:message></th><td class="edit"><select wicket:id="HEAD" tabindex="4" /> <span class="help-inline"><wicket:message key="gb.headRefDescription"></wicket:message></span></td></tr>
- <tr><th><wicket:message key="gb.owner"></wicket:message></th><td class="edit"><select wicket:id="owner" tabindex="5" /> <span class="help-inline"><wicket:message key="gb.ownerDescription"></wicket:message></span></td></tr>
- <tr><th><wicket:message key="gb.enableTickets"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="useTickets" tabindex="6" /> <span class="help-inline"><wicket:message key="gb.useTicketsDescription"></wicket:message></span></label></td></tr>
- <tr><th><wicket:message key="gb.enableDocs"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="useDocs" tabindex="7" /> <span class="help-inline"><wicket:message key="gb.useDocsDescription"></wicket:message></span></label></td></tr>
- <tr><th><wicket:message key="gb.showRemoteBranches"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="showRemoteBranches" tabindex="8" /> <span class="help-inline"><wicket:message key="gb.showRemoteBranchesDescription"></wicket:message></span></label></td></tr>
- <tr><th><wicket:message key="gb.showReadme"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="showReadme" tabindex="9" /> <span class="help-inline"><wicket:message key="gb.showReadmeDescription"></wicket:message></span></label></td></tr>
- <tr><th><wicket:message key="gb.skipSizeCalculation"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="skipSizeCalculation" tabindex="10" /> <span class="help-inline"><wicket:message key="gb.skipSizeCalculationDescription"></wicket:message></span></label></td></tr>
- <tr><th><wicket:message key="gb.skipSummaryMetrics"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="skipSummaryMetrics" tabindex="11" /> <span class="help-inline"><wicket:message key="gb.skipSummaryMetricsDescription"></wicket:message></span></label></td></tr>
- <tr><th><wicket:message key="gb.mailingLists"></wicket:message></th><td class="edit"><input class="span8" type="text" wicket:id="mailingLists" size="40" tabindex="13" /></td></tr>
+ <tr><th><wicket:message key="gb.headRef"></wicket:message></th><td class="edit"><select class="span3" wicket:id="HEAD" tabindex="4" /> <span class="help-inline"><wicket:message key="gb.headRefDescription"></wicket:message></span></td></tr>
+ <tr><th><wicket:message key="gb.owner"></wicket:message></th><td class="edit"><select class="span2" wicket:id="owner" tabindex="5" /> <span class="help-inline"><wicket:message key="gb.ownerDescription"></wicket:message></span></td></tr>
+ <tr><th><wicket:message key="gb.gcPeriod"></wicket:message></th><td class="edit"><select class="span2" wicket:id="gcPeriod" tabindex="6" /> <span class="help-inline"><wicket:message key="gb.gcPeriodDescription"></wicket:message></span></td></tr>
+ <tr><th><wicket:message key="gb.gcThreshold"></wicket:message></th><td class="edit"><input class="span1" type="text" wicket:id="gcThreshold" tabindex="7" /> <span class="help-inline"><wicket:message key="gb.gcThresholdDescription"></wicket:message></span></td></tr>
+ <tr><th><wicket:message key="gb.enableTickets"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="useTickets" tabindex="8" /> <span class="help-inline"><wicket:message key="gb.useTicketsDescription"></wicket:message></span></label></td></tr>
+ <tr><th><wicket:message key="gb.enableDocs"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="useDocs" tabindex="9" /> <span class="help-inline"><wicket:message key="gb.useDocsDescription"></wicket:message></span></label></td></tr>
+ <tr><th><wicket:message key="gb.showRemoteBranches"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="showRemoteBranches" tabindex="10" /> <span class="help-inline"><wicket:message key="gb.showRemoteBranchesDescription"></wicket:message></span></label></td></tr>
+ <tr><th><wicket:message key="gb.showReadme"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="showReadme" tabindex="11" /> <span class="help-inline"><wicket:message key="gb.showReadmeDescription"></wicket:message></span></label></td></tr>
+ <tr><th><wicket:message key="gb.skipSizeCalculation"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="skipSizeCalculation" tabindex="12" /> <span class="help-inline"><wicket:message key="gb.skipSizeCalculationDescription"></wicket:message></span></label></td></tr>
+ <tr><th><wicket:message key="gb.skipSummaryMetrics"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="skipSummaryMetrics" tabindex="13" /> <span class="help-inline"><wicket:message key="gb.skipSummaryMetricsDescription"></wicket:message></span></label></td></tr>
+ <tr><th><wicket:message key="gb.mailingLists"></wicket:message></th><td class="edit"><input class="span8" type="text" wicket:id="mailingLists" size="40" tabindex="14" /></td></tr>
</tbody>
</table>
</div>
@@ -45,18 +47,18 @@ <div class="tab-pane" id="permissions">
<table class="plain">
<tbody class="settings">
- <tr><th><wicket:message key="gb.accessRestriction"></wicket:message></th><td class="edit"><select class="span4" wicket:id="accessRestriction" tabindex="13" /></td></tr>
+ <tr><th><wicket:message key="gb.accessRestriction"></wicket:message></th><td class="edit"><select class="span4" wicket:id="accessRestriction" tabindex="15" /></td></tr>
<tr><th colspan="2"><hr/></th></tr>
<tr><th><wicket:message key="gb.authorizationControl"></wicket:message></th><td style="padding:2px;">
<wicket:container wicket:id="authorizationControl">
- <label class="radio"><input type="radio" wicket:id="allowAuthenticated" tabindex="14" /> <span class="help-inline"><wicket:message key="gb.allowAuthenticatedDescription"></wicket:message></span></label>
- <label class="radio"><input type="radio" wicket:id="allowNamed" tabindex="15" /> <span class="help-inline"><wicket:message key="gb.allowNamedDescription"></wicket:message></span></label>
+ <label class="radio"><input type="radio" wicket:id="allowAuthenticated" tabindex="16" /> <span class="help-inline"><wicket:message key="gb.allowAuthenticatedDescription"></wicket:message></span></label>
+ <label class="radio"><input type="radio" wicket:id="allowNamed" tabindex="17" /> <span class="help-inline"><wicket:message key="gb.allowNamedDescription"></wicket:message></span></label>
</wicket:container>
</td></tr>
<tr><th colspan="2"><hr/></th></tr>
- <tr><th><wicket:message key="gb.isFrozen"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="isFrozen" tabindex="16" /> <span class="help-inline"><wicket:message key="gb.isFrozenDescription"></wicket:message></span></label></td></tr>
- <tr><th><wicket:message key="gb.allowForks"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="allowForks" tabindex="17" /> <span class="help-inline"><wicket:message key="gb.allowForksDescription"></wicket:message></span></label></td></tr>
- <tr><th><wicket:message key="gb.verifyCommitter"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="verifyCommitter" tabindex="18" /> <span class="help-inline"><wicket:message key="gb.verifyCommitterDescription"></wicket:message></span><br/><span class="help-inline" style="padding-left:10px;"><wicket:message key="gb.verifyCommitterNote"></wicket:message></span></label></td></tr>
+ <tr><th><wicket:message key="gb.isFrozen"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="isFrozen" tabindex="18" /> <span class="help-inline"><wicket:message key="gb.isFrozenDescription"></wicket:message></span></label></td></tr>
+ <tr><th><wicket:message key="gb.allowForks"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="allowForks" tabindex="19" /> <span class="help-inline"><wicket:message key="gb.allowForksDescription"></wicket:message></span></label></td></tr>
+ <tr><th><wicket:message key="gb.verifyCommitter"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="verifyCommitter" tabindex="20" /> <span class="help-inline"><wicket:message key="gb.verifyCommitterDescription"></wicket:message></span><br/><span class="help-inline" style="padding-left:10px;"><wicket:message key="gb.verifyCommitterNote"></wicket:message></span></label></td></tr>
<tr><th colspan="2"><hr/></th></tr>
<tr><th><wicket:message key="gb.userPermissions"></wicket:message></th><td style="padding:2px;"><span wicket:id="users"></span></td></tr>
<tr><th colspan="2"><hr/></th></tr>
@@ -69,7 +71,7 @@ <div class="tab-pane" id="federation">
<table class="plain">
<tbody class="settings">
- <tr><th><wicket:message key="gb.federationStrategy"></wicket:message></th><td class="edit"><select class="span4" wicket:id="federationStrategy" tabindex="19" /></td></tr>
+ <tr><th><wicket:message key="gb.federationStrategy"></wicket:message></th><td class="edit"><select class="span4" wicket:id="federationStrategy" tabindex="21" /></td></tr>
<tr><th><wicket:message key="gb.federationSets"></wicket:message></th><td style="padding:2px;"><span wicket:id="federationSets"></span></td></tr>
</tbody>
</table>
diff --git a/src/com/gitblit/wicket/pages/EditRepositoryPage.java b/src/com/gitblit/wicket/pages/EditRepositoryPage.java index 98b04f50..1a2e63ce 100644 --- a/src/com/gitblit/wicket/pages/EditRepositoryPage.java +++ b/src/com/gitblit/wicket/pages/EditRepositoryPage.java @@ -379,6 +379,10 @@ public class EditRepositoryPage extends RootSubPage { }
form.add(new DropDownChoice<String>("HEAD", availableRefs).setEnabled(availableRefs.size() > 0));
+ List<String> gcPeriods = Arrays.asList("1 day", "2 days", "3 days", "4 days", "5 days", "7 days", "10 days", "14 days");
+ form.add(new DropDownChoice<String>("gcPeriod", gcPeriods));
+ form.add(new TextField<String>("gcThreshold"));
+
// federation strategies - remove ORIGIN choice if this repository has
// no origin.
List<FederationStrategy> federationStrategies = new ArrayList<FederationStrategy>(
diff --git a/src/com/gitblit/wicket/pages/RepositoryPage.java b/src/com/gitblit/wicket/pages/RepositoryPage.java index bacf233d..2bd1ec26 100644 --- a/src/com/gitblit/wicket/pages/RepositoryPage.java +++ b/src/com/gitblit/wicket/pages/RepositoryPage.java @@ -92,6 +92,18 @@ public abstract class RepositoryPage extends BasePage { }
objectId = WicketUtils.getObject(params);
+ if (StringUtils.isEmpty(repositoryName)) {
+ error(MessageFormat.format(getString("gb.repositoryNotSpecifiedFor"), getPageName()), true);
+ }
+
+ if (!getRepositoryModel().hasCommits) {
+ setResponsePage(EmptyRepositoryPage.class, params);
+ }
+
+ if (getRepositoryModel().isCollectingGarbage) {
+ error(MessageFormat.format(getString("gb.busyCollectingGarbage"), getRepositoryModel().name), true);
+ }
+
if (objectId != null) {
RefModel branch = null;
if ((branch = JGitUtils.getBranch(getRepository(), objectId)) != null) {
@@ -103,17 +115,10 @@ public abstract class RepositoryPage extends BasePage { boolean canAccess = user.hasBranchPermission(repositoryName,
branch.reference.getName());
if (!canAccess) {
- error("Access denied", true);
+ error(getString("gb.accessDeined"), true);
}
}
}
- if (StringUtils.isEmpty(repositoryName)) {
- error(MessageFormat.format(getString("gb.repositoryNotSpecifiedFor"), getPageName()), true);
- }
-
- if (!getRepositoryModel().hasCommits) {
- setResponsePage(EmptyRepositoryPage.class, params);
- }
// register the available page links for this page and user
registeredPages = registerPages();
diff --git a/src/com/gitblit/wicket/panels/BranchesPanel.java b/src/com/gitblit/wicket/panels/BranchesPanel.java index 3ca02cb6..cfe9f5fe 100644 --- a/src/com/gitblit/wicket/panels/BranchesPanel.java +++ b/src/com/gitblit/wicket/panels/BranchesPanel.java @@ -166,6 +166,14 @@ public class BranchesPanel extends BasePanel { @Override
public void onClick() {
Repository r = GitBlit.self().getRepository(repositoryModel.name);
+ if (r == null) {
+ if (GitBlit.self().isCollectingGarbage(repositoryModel.name)) {
+ error(MessageFormat.format(getString("gb.busyCollectingGarbage"), repositoryModel.name));
+ } else {
+ error(MessageFormat.format("Failed to find repository {0}", repositoryModel.name));
+ }
+ return;
+ }
boolean success = JGitUtils.deleteBranchRef(r, entry.getName());
r.close();
if (success) {
|