RepositoryCache.java

  1. /*
  2.  * Copyright (C) 2009, Google Inc. and others
  3.  *
  4.  * This program and the accompanying materials are made available under the
  5.  * terms of the Eclipse Distribution License v. 1.0 which is available at
  6.  * https://www.eclipse.org/org/documents/edl-v10.php.
  7.  *
  8.  * SPDX-License-Identifier: BSD-3-Clause
  9.  */

  10. package org.eclipse.jgit.lib;

  11. import java.io.File;
  12. import java.io.IOException;
  13. import java.util.ArrayList;
  14. import java.util.Collection;
  15. import java.util.concurrent.ConcurrentHashMap;
  16. import java.util.concurrent.ScheduledFuture;
  17. import java.util.concurrent.ScheduledThreadPoolExecutor;
  18. import java.util.concurrent.TimeUnit;

  19. import org.eclipse.jgit.annotations.NonNull;
  20. import org.eclipse.jgit.errors.RepositoryNotFoundException;
  21. import org.eclipse.jgit.internal.storage.file.FileRepository;
  22. import org.eclipse.jgit.lib.internal.WorkQueue;
  23. import org.eclipse.jgit.util.FS;
  24. import org.eclipse.jgit.util.IO;
  25. import org.eclipse.jgit.util.RawParseUtils;
  26. import org.slf4j.Logger;
  27. import org.slf4j.LoggerFactory;

  28. /**
  29.  * Cache of active {@link org.eclipse.jgit.lib.Repository} instances.
  30.  */
  31. public class RepositoryCache {
  32.     private static final Logger LOG = LoggerFactory
  33.             .getLogger(RepositoryCache.class);

  34.     private static final RepositoryCache cache = new RepositoryCache();

  35.     /**
  36.      * Open an existing repository, reusing a cached instance if possible.
  37.      * <p>
  38.      * When done with the repository, the caller must call
  39.      * {@link org.eclipse.jgit.lib.Repository#close()} to decrement the
  40.      * repository's usage counter.
  41.      *
  42.      * @param location
  43.      *            where the local repository is. Typically a
  44.      *            {@link org.eclipse.jgit.lib.RepositoryCache.FileKey}.
  45.      * @return the repository instance requested; caller must close when done.
  46.      * @throws java.io.IOException
  47.      *             the repository could not be read (likely its core.version
  48.      *             property is not supported).
  49.      * @throws org.eclipse.jgit.errors.RepositoryNotFoundException
  50.      *             there is no repository at the given location.
  51.      */
  52.     public static Repository open(Key location) throws IOException,
  53.             RepositoryNotFoundException {
  54.         return open(location, true);
  55.     }

  56.     /**
  57.      * Open a repository, reusing a cached instance if possible.
  58.      * <p>
  59.      * When done with the repository, the caller must call
  60.      * {@link org.eclipse.jgit.lib.Repository#close()} to decrement the
  61.      * repository's usage counter.
  62.      *
  63.      * @param location
  64.      *            where the local repository is. Typically a
  65.      *            {@link org.eclipse.jgit.lib.RepositoryCache.FileKey}.
  66.      * @param mustExist
  67.      *            If true, and the repository is not found, throws {@code
  68.      *            RepositoryNotFoundException}. If false, a repository instance
  69.      *            is created and registered anyway.
  70.      * @return the repository instance requested; caller must close when done.
  71.      * @throws java.io.IOException
  72.      *             the repository could not be read (likely its core.version
  73.      *             property is not supported).
  74.      * @throws RepositoryNotFoundException
  75.      *             There is no repository at the given location, only thrown if
  76.      *             {@code mustExist} is true.
  77.      */
  78.     public static Repository open(Key location, boolean mustExist)
  79.             throws IOException {
  80.         return cache.openRepository(location, mustExist);
  81.     }

  82.     /**
  83.      * Register one repository into the cache.
  84.      * <p>
  85.      * During registration the cache automatically increments the usage counter,
  86.      * permitting it to retain the reference. A
  87.      * {@link org.eclipse.jgit.lib.RepositoryCache.FileKey} for the repository's
  88.      * {@link org.eclipse.jgit.lib.Repository#getDirectory()} is used to index
  89.      * the repository in the cache.
  90.      * <p>
  91.      * If another repository already is registered in the cache at this
  92.      * location, the other instance is closed.
  93.      *
  94.      * @param db
  95.      *            repository to register.
  96.      */
  97.     public static void register(Repository db) {
  98.         if (db.getDirectory() != null) {
  99.             FileKey key = FileKey.exact(db.getDirectory(), db.getFS());
  100.             cache.registerRepository(key, db);
  101.         }
  102.     }

  103.     /**
  104.      * Close and remove a repository from the cache.
  105.      * <p>
  106.      * Removes a repository from the cache, if it is still registered here, and
  107.      * close it.
  108.      *
  109.      * @param db
  110.      *            repository to unregister.
  111.      */
  112.     public static void close(@NonNull Repository db) {
  113.         if (db.getDirectory() != null) {
  114.             FileKey key = FileKey.exact(db.getDirectory(), db.getFS());
  115.             cache.unregisterAndCloseRepository(key);
  116.         }
  117.     }

  118.     /**
  119.      * Remove a repository from the cache.
  120.      * <p>
  121.      * Removes a repository from the cache, if it is still registered here. This
  122.      * method will not close the repository, only remove it from the cache. See
  123.      * {@link org.eclipse.jgit.lib.RepositoryCache#close(Repository)} to remove
  124.      * and close the repository.
  125.      *
  126.      * @param db
  127.      *            repository to unregister.
  128.      * @since 4.3
  129.      */
  130.     public static void unregister(Repository db) {
  131.         if (db.getDirectory() != null) {
  132.             unregister(FileKey.exact(db.getDirectory(), db.getFS()));
  133.         }
  134.     }

  135.     /**
  136.      * Remove a repository from the cache.
  137.      * <p>
  138.      * Removes a repository from the cache, if it is still registered here. This
  139.      * method will not close the repository, only remove it from the cache. See
  140.      * {@link org.eclipse.jgit.lib.RepositoryCache#close(Repository)} to remove
  141.      * and close the repository.
  142.      *
  143.      * @param location
  144.      *            location of the repository to remove.
  145.      * @since 4.1
  146.      */
  147.     public static void unregister(Key location) {
  148.         cache.unregisterRepository(location);
  149.     }

  150.     /**
  151.      * Get the locations of all repositories registered in the cache.
  152.      *
  153.      * @return the locations of all repositories registered in the cache.
  154.      * @since 4.1
  155.      */
  156.     public static Collection<Key> getRegisteredKeys() {
  157.         return cache.getKeys();
  158.     }

  159.     static boolean isCached(@NonNull Repository repo) {
  160.         File gitDir = repo.getDirectory();
  161.         if (gitDir == null) {
  162.             return false;
  163.         }
  164.         FileKey key = new FileKey(gitDir, repo.getFS());
  165.         return cache.cacheMap.get(key) == repo;
  166.     }

  167.     /**
  168.      * Unregister all repositories from the cache.
  169.      */
  170.     public static void clear() {
  171.         cache.clearAll();
  172.     }

  173.     static void clearExpired() {
  174.         cache.clearAllExpired();
  175.     }

  176.     static void reconfigure(RepositoryCacheConfig repositoryCacheConfig) {
  177.         cache.configureEviction(repositoryCacheConfig);
  178.     }

  179.     private final ConcurrentHashMap<Key, Repository> cacheMap;

  180.     private final Lock[] openLocks;

  181.     private ScheduledFuture<?> cleanupTask;

  182.     private volatile long expireAfter;

  183.     private RepositoryCache() {
  184.         cacheMap = new ConcurrentHashMap<>();
  185.         openLocks = new Lock[4];
  186.         for (int i = 0; i < openLocks.length; i++) {
  187.             openLocks[i] = new Lock();
  188.         }
  189.         configureEviction(new RepositoryCacheConfig());
  190.     }

  191.     private void configureEviction(
  192.             RepositoryCacheConfig repositoryCacheConfig) {
  193.         expireAfter = repositoryCacheConfig.getExpireAfter();
  194.         ScheduledThreadPoolExecutor scheduler = WorkQueue.getExecutor();
  195.         synchronized (scheduler) {
  196.             if (cleanupTask != null) {
  197.                 cleanupTask.cancel(false);
  198.             }
  199.             long delay = repositoryCacheConfig.getCleanupDelay();
  200.             if (delay == RepositoryCacheConfig.NO_CLEANUP) {
  201.                 return;
  202.             }
  203.             cleanupTask = scheduler.scheduleWithFixedDelay(() -> {
  204.                 try {
  205.                     cache.clearAllExpired();
  206.                 } catch (Throwable e) {
  207.                     LOG.error(e.getMessage(), e);
  208.                 }
  209.             }, delay, delay, TimeUnit.MILLISECONDS);
  210.         }
  211.     }

  212.     private Repository openRepository(final Key location,
  213.             final boolean mustExist) throws IOException {
  214.         Repository db = cacheMap.get(location);
  215.         if (db == null) {
  216.             synchronized (lockFor(location)) {
  217.                 db = cacheMap.get(location);
  218.                 if (db == null) {
  219.                     db = location.open(mustExist);
  220.                     cacheMap.put(location, db);
  221.                 } else {
  222.                     db.incrementOpen();
  223.                 }
  224.             }
  225.         } else {
  226.             db.incrementOpen();
  227.         }
  228.         return db;
  229.     }

  230.     private void registerRepository(Key location, Repository db) {
  231.         try (Repository oldDb = cacheMap.put(location, db)) {
  232.             // oldDb is auto-closed
  233.         }
  234.     }

  235.     private Repository unregisterRepository(Key location) {
  236.         return cacheMap.remove(location);
  237.     }

  238.     private boolean isExpired(Repository db) {
  239.         return db != null && db.useCnt.get() <= 0
  240.             && (System.currentTimeMillis() - db.closedAt.get() > expireAfter);
  241.     }

  242.     private void unregisterAndCloseRepository(Key location) {
  243.         synchronized (lockFor(location)) {
  244.             Repository oldDb = unregisterRepository(location);
  245.             if (oldDb != null) {
  246.                 oldDb.doClose();
  247.             }
  248.         }
  249.     }

  250.     private Collection<Key> getKeys() {
  251.         return new ArrayList<>(cacheMap.keySet());
  252.     }

  253.     private void clearAllExpired() {
  254.         for (Repository db : cacheMap.values()) {
  255.             if (isExpired(db)) {
  256.                 RepositoryCache.close(db);
  257.             }
  258.         }
  259.     }

  260.     private void clearAll() {
  261.         for (Key k : cacheMap.keySet()) {
  262.             unregisterAndCloseRepository(k);
  263.         }
  264.     }

  265.     private Lock lockFor(Key location) {
  266.         return openLocks[(location.hashCode() >>> 1) % openLocks.length];
  267.     }

  268.     private static class Lock {
  269.         // Used only for its monitor.
  270.     }

  271.     /**
  272.      * Abstract hash key for {@link RepositoryCache} entries.
  273.      * <p>
  274.      * A Key instance should be lightweight, and implement hashCode() and
  275.      * equals() such that two Key instances are equal if they represent the same
  276.      * Repository location.
  277.      */
  278.     public static interface Key {
  279.         /**
  280.          * Called by {@link RepositoryCache#open(Key)} if it doesn't exist yet.
  281.          * <p>
  282.          * If a repository does not exist yet in the cache, the cache will call
  283.          * this method to acquire a handle to it.
  284.          *
  285.          * @param mustExist
  286.          *            true if the repository must exist in order to be opened;
  287.          *            false if a new non-existent repository is permitted to be
  288.          *            created (the caller is responsible for calling create).
  289.          * @return the new repository instance.
  290.          * @throws IOException
  291.          *             the repository could not be read (likely its core.version
  292.          *             property is not supported).
  293.          * @throws RepositoryNotFoundException
  294.          *             There is no repository at the given location, only thrown
  295.          *             if {@code mustExist} is true.
  296.          */
  297.         Repository open(boolean mustExist) throws IOException,
  298.                 RepositoryNotFoundException;
  299.     }

  300.     /** Location of a Repository, using the standard java.io.File API. */
  301.     public static class FileKey implements Key {
  302.         /**
  303.          * Obtain a pointer to an exact location on disk.
  304.          * <p>
  305.          * No guessing is performed, the given location is exactly the GIT_DIR
  306.          * directory of the repository.
  307.          *
  308.          * @param directory
  309.          *            location where the repository database is.
  310.          * @param fs
  311.          *            the file system abstraction which will be necessary to
  312.          *            perform certain file system operations.
  313.          * @return a key for the given directory.
  314.          * @see #lenient(File, FS)
  315.          */
  316.         public static FileKey exact(File directory, FS fs) {
  317.             return new FileKey(directory, fs);
  318.         }

  319.         /**
  320.          * Obtain a pointer to a location on disk.
  321.          * <p>
  322.          * The method performs some basic guessing to locate the repository.
  323.          * Searched paths are:
  324.          * <ol>
  325.          * <li>{@code directory} // assume exact match</li>
  326.          * <li>{@code directory} + "/.git" // assume working directory</li>
  327.          * <li>{@code directory} + ".git" // assume bare</li>
  328.          * </ol>
  329.          *
  330.          * @param directory
  331.          *            location where the repository database might be.
  332.          * @param fs
  333.          *            the file system abstraction which will be necessary to
  334.          *            perform certain file system operations.
  335.          * @return a key for the given directory.
  336.          * @see #exact(File, FS)
  337.          */
  338.         public static FileKey lenient(File directory, FS fs) {
  339.             final File gitdir = resolve(directory, fs);
  340.             return new FileKey(gitdir != null ? gitdir : directory, fs);
  341.         }

  342.         private final File path;
  343.         private final FS fs;

  344.         /**
  345.          * @param directory
  346.          *            exact location of the repository.
  347.          * @param fs
  348.          *            the file system abstraction which will be necessary to
  349.          *            perform certain file system operations.
  350.          */
  351.         protected FileKey(File directory, FS fs) {
  352.             path = canonical(directory);
  353.             this.fs = fs;
  354.         }

  355.         private static File canonical(File path) {
  356.             try {
  357.                 return path.getCanonicalFile();
  358.             } catch (IOException e) {
  359.                 return path.getAbsoluteFile();
  360.             }
  361.         }

  362.         /** @return location supplied to the constructor. */
  363.         public final File getFile() {
  364.             return path;
  365.         }

  366.         @Override
  367.         public Repository open(boolean mustExist) throws IOException {
  368.             if (mustExist && !isGitRepository(path, fs))
  369.                 throw new RepositoryNotFoundException(path);
  370.             return new FileRepository(path);
  371.         }

  372.         @Override
  373.         public int hashCode() {
  374.             return path.hashCode();
  375.         }

  376.         @Override
  377.         public boolean equals(Object o) {
  378.             return o instanceof FileKey && path.equals(((FileKey) o).path);
  379.         }

  380.         @Override
  381.         public String toString() {
  382.             return path.toString();
  383.         }

  384.         /**
  385.          * Guess if a directory contains a Git repository.
  386.          * <p>
  387.          * This method guesses by looking for the existence of some key files
  388.          * and directories.
  389.          *
  390.          * @param dir
  391.          *            the location of the directory to examine.
  392.          * @param fs
  393.          *            the file system abstraction which will be necessary to
  394.          *            perform certain file system operations.
  395.          * @return true if the directory "looks like" a Git repository; false if
  396.          *         it doesn't look enough like a Git directory to really be a
  397.          *         Git directory.
  398.          */
  399.         public static boolean isGitRepository(File dir, FS fs) {
  400.             return fs.resolve(dir, Constants.OBJECTS).exists()
  401.                     && fs.resolve(dir, "refs").exists() //$NON-NLS-1$
  402.                     && (fs.resolve(dir, Constants.REFTABLE).exists()
  403.                             || isValidHead(new File(dir, Constants.HEAD)));
  404.         }

  405.         private static boolean isValidHead(File head) {
  406.             final String ref = readFirstLine(head);
  407.             return ref != null
  408.                     && (ref.startsWith("ref: refs/") || ObjectId.isId(ref)); //$NON-NLS-1$
  409.         }

  410.         private static String readFirstLine(File head) {
  411.             try {
  412.                 final byte[] buf = IO.readFully(head, 4096);
  413.                 int n = buf.length;
  414.                 if (n == 0)
  415.                     return null;
  416.                 if (buf[n - 1] == '\n')
  417.                     n--;
  418.                 return RawParseUtils.decode(buf, 0, n);
  419.             } catch (IOException e) {
  420.                 return null;
  421.             }
  422.         }

  423.         /**
  424.          * Guess the proper path for a Git repository.
  425.          * <p>
  426.          * The method performs some basic guessing to locate the repository.
  427.          * Searched paths are:
  428.          * <ol>
  429.          * <li>{@code directory} // assume exact match</li>
  430.          * <li>{@code directory} + "/.git" // assume working directory</li>
  431.          * <li>{@code directory} + ".git" // assume bare</li>
  432.          * </ol>
  433.          *
  434.          * @param directory
  435.          *            location to guess from. Several permutations are tried.
  436.          * @param fs
  437.          *            the file system abstraction which will be necessary to
  438.          *            perform certain file system operations.
  439.          * @return the actual directory location if a better match is found;
  440.          *         null if there is no suitable match.
  441.          */
  442.         public static File resolve(File directory, FS fs) {
  443.             if (isGitRepository(directory, fs))
  444.                 return directory;
  445.             if (isGitRepository(new File(directory, Constants.DOT_GIT), fs))
  446.                 return new File(directory, Constants.DOT_GIT);

  447.             final String name = directory.getName();
  448.             final File parent = directory.getParentFile();
  449.             if (isGitRepository(new File(parent, name + Constants.DOT_GIT_EXT), fs))
  450.                 return new File(parent, name + Constants.DOT_GIT_EXT);
  451.             return null;
  452.         }
  453.     }
  454. }