com.gitblit.GCExecutor.java Source code

Java tutorial

Introduction

Here is the source code for com.gitblit.GCExecutor.java

Source

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