/*
* 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.service;
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 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.IStoredSettings;
import com.gitblit.Keys;
import com.gitblit.manager.IRepositoryManager;
import com.gitblit.models.RepositoryModel;
import com.gitblit.utils.FileUtils;
/**
* The Garbage Collector Service handles periodic garbage collection in repositories.
*
* @author James Moger
*
*/
public class GarbageCollectorService implements Runnable {
public static enum GCStatus {
READY, COLLECTING;
public boolean exceeds(GCStatus s) {
return ordinal() > s.ordinal();
}
}
private final Logger logger = LoggerFactory.getLogger(GarbageCollectorService.class);
private final IStoredSettings settings;
private final IRepositoryManager repositoryManager;
private AtomicBoolean running = new AtomicBoolean(false);
private AtomicBoolean forceClose = new AtomicBoolean(false);
private final Map<String, GCStatus> gcCache = new ConcurrentHashMap<String, GCStatus>();
public GarbageCollectorService(
IStoredSettings settings,
IRepositoryManager repositoryManager) {
this.settings = settings;
this.repositoryManager = repositoryManager;
}
/**
* 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 : 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 (!repositoryManager.isIdle(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);
}
}