jetbrains.buildServer.buildTriggers.vcs.git.RepositoryManagerImpl.java Source code

Java tutorial

Introduction

Here is the source code for jetbrains.buildServer.buildTriggers.vcs.git.RepositoryManagerImpl.java

Source

/*
 * Copyright 2000-2014 JetBrains s.r.o.
 *
 * 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 jetbrains.buildServer.buildTriggers.vcs.git;

import com.intellij.openapi.diagnostic.Logger;
import jetbrains.buildServer.util.FileUtil;
import jetbrains.buildServer.vcs.VcsException;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.RepositoryCache;
import org.eclipse.jgit.transport.URIish;
import org.eclipse.jgit.util.FS;
import org.jetbrains.annotations.NotNull;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import static jetbrains.buildServer.buildTriggers.vcs.git.GitServerUtil.getWrongUrlError;

/**
 * @author dmitry.neverov
 */
public final class RepositoryManagerImpl implements RepositoryManager {

    private static final Logger LOG = Logger.getInstance(RepositoryManagerImpl.class.getName());

    private final MirrorManager myMirrorManager;
    private final long myExpirationTimeout;
    /**
     * During repository creation jgit checks existence of some files and directories. When several threads
     * try to create repository concurrently some of them could see it in inconsistent state. This map contains
     * locks for repository creation, so only one thread at a time will create repository at give dir.
     */
    private final ConcurrentMap<File, Object> myCreateLocks = new ConcurrentHashMap<File, Object>();
    /**
     * In the past jgit has some concurrency problems, in order to fix them we do only one fetch at a time.
     * Also several concurrent fetches in single repository does not make sense since only one of them succeed.
     * This map contains locks used for fetch and push operations.
     */
    private final ConcurrentMap<File, Object> myWriteLocks = new ConcurrentHashMap<File, Object>();
    /**
     * During cleanup unused bare repositories are removed. This map contains rw locks for repository removal.
     * Fetch/push/create operations should be done with read lock hold, remove operation is done with write lock hold.
     * @see Cleanup
     */
    private final ConcurrentMap<File, ReadWriteLock> myRmLocks = new ConcurrentHashMap<File, ReadWriteLock>();

    private final ConcurrentMap<File, Object> myUpdateLastUsedTimeLocks = new ConcurrentHashMap<File, Object>();

    private final AutoCloseRepositoryCache myRepositoryCache = new AutoCloseRepositoryCache();

    private final ServerPluginConfig myConfig;

    public RepositoryManagerImpl(@NotNull final ServerPluginConfig config,
            @NotNull final MirrorManager mirrorManager) {
        myConfig = config;
        myExpirationTimeout = config.getMirrorExpirationTimeoutMillis();
        myMirrorManager = mirrorManager;
    }

    @NotNull
    public File getBaseMirrorsDir() {
        return myMirrorManager.getBaseMirrorsDir();
    }

    @NotNull
    public File getMirrorDir(@NotNull String repositoryUrl) {
        return myMirrorManager.getMirrorDir(repositoryUrl);
    }

    public void invalidate(@NotNull final File dir) {
        myMirrorManager.invalidate(dir);
    }

    public Map<String, File> getMappings() {
        return myMirrorManager.getMappings();
    }

    @NotNull
    public List<File> getExpiredDirs() {
        long now = System.currentTimeMillis();
        List<File> result = new ArrayList<File>();
        final File[] files = myMirrorManager.getBaseMirrorsDir().listFiles();
        if (files == null)
            return result;
        for (File f : files) {
            if (f.isDirectory() && isExpired(f, now))
                result.add(f);
        }
        return result;
    }

    private boolean isExpired(@NotNull final File dir, long now) {
        long lastUsedTime = getLastUsedTime(dir);
        return now - lastUsedTime > myExpirationTimeout;
    }

    public long getLastUsedTime(@NotNull File dir) {
        return myMirrorManager.getLastUsedTime(dir);
    }

    @NotNull
    public Repository openRepository(@NotNull final URIish fetchUrl) throws VcsException {
        final URIish canonicalURI = getCanonicalURI(fetchUrl);
        final File dir = getMirrorDir(canonicalURI.toString());
        return openRepository(dir, canonicalURI);
    }

    @NotNull
    public Repository openRepository(@NotNull final File dir, @NotNull final URIish fetchUrl) throws VcsException {
        final URIish canonicalURI = getCanonicalURI(fetchUrl);
        if (isDefaultMirrorDir(dir))
            updateLastUsedTime(dir);
        Repository result = myRepositoryCache.get(RepositoryCache.FileKey.exact(dir, FS.DETECTED));
        if (result == null)
            return createRepository(dir, canonicalURI);
        String existingRemote = result.getConfig().getString("teamcity", null, "remote");
        if (existingRemote == null) {
            myRepositoryCache.release(result);
            invalidate(dir);
            return GitServerUtil.getRepository(dir, fetchUrl);
        }
        if (!canonicalURI.toString().equals(existingRemote)) {
            myRepositoryCache.release(result);
            throw getWrongUrlError(dir, existingRemote, fetchUrl);
        }
        return result;
    }

    public void closeRepository(@NotNull Repository repository) {
        myRepositoryCache.release(repository);
    }

    @NotNull
    private Repository createRepository(@NotNull final File dir, @NotNull final URIish fetchUrl)
            throws VcsException {
        Lock rmLock = getRmLock(dir).readLock();
        rmLock.lock();
        try {
            synchronized (getCreateLock(dir)) {
                Repository result = GitServerUtil.getRepository(dir, fetchUrl);
                return myRepositoryCache.add(RepositoryCache.FileKey.exact(dir, FS.DETECTED), result);
            }
        } finally {
            rmLock.unlock();
        }
    }

    private void updateLastUsedTime(@NotNull final File dir) {
        Lock rmLock = getRmLock(dir).readLock();
        try {
            rmLock.lock();
            synchronized (getUpdateLastUsedTimeLock(dir)) {
                File timestamp = new File(dir, "timestamp");
                if (!dir.exists() && !dir.mkdirs())
                    throw new IOException("Cannot create directory " + dir.getAbsolutePath());
                if (!timestamp.exists())
                    timestamp.createNewFile();
                FileUtil.writeFileAndReportErrors(timestamp, String.valueOf(System.currentTimeMillis()));
            }
        } catch (IOException e) {
            LOG.error("Error while updating timestamp in " + dir.getAbsolutePath(), e);
        } finally {
            rmLock.unlock();
        }
    }

    private boolean isDefaultMirrorDir(@NotNull final File dir) {
        File baseDir = myMirrorManager.getBaseMirrorsDir();
        return baseDir.equals(dir.getParentFile());
    }

    @NotNull
    private Object getUpdateLastUsedTimeLock(@NotNull File dir) {
        try {
            File canonical = dir.getCanonicalFile();
            return getOrCreate(myUpdateLastUsedTimeLocks, canonical, new Object());
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @NotNull
    public Object getWriteLock(@NotNull final File dir) {
        try {
            File canonical = dir.getCanonicalFile();
            return getOrCreate(myWriteLocks, canonical, new Object());
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @NotNull
    public ReadWriteLock getRmLock(@NotNull final File dir) {
        try {
            File canonical = dir.getCanonicalFile();
            return getOrCreate(myRmLocks, canonical, new ReentrantReadWriteLock());
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @NotNull
    public Object getCreateLock(File dir) {
        try {
            File canonical = dir.getCanonicalFile();
            return getOrCreate(myCreateLocks, canonical, new Object());
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public void cleanLocksFor(@NotNull final File dir) {
        try {
            File canonical = dir.getCanonicalFile();
            myWriteLocks.remove(canonical);
            myCreateLocks.remove(canonical);
            myRmLocks.remove(canonical);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private <K, V> V getOrCreate(ConcurrentMap<K, V> map, K key, V value) {
        V existing = map.putIfAbsent(key, value);
        if (existing != null)
            return existing;
        else
            return value;
    }

    @NotNull
    private URIish getCanonicalURI(@NotNull final URIish uri) {
        return uri;
        //    return new URIish()
        //      .setScheme(uri.getScheme())
        //      .setHost(uri.getHost())
        //      .setPort(uri.getPort())
        //      .setPath(uri.getPath());
    }
}