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.Map;
  16. import java.util.concurrent.ConcurrentHashMap;
  17. import java.util.concurrent.ScheduledFuture;
  18. import java.util.concurrent.ScheduledThreadPoolExecutor;
  19. import java.util.concurrent.TimeUnit;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

  180.     private final Map<Key, Repository> cacheMap;

  181.     private final Lock[] openLocks;

  182.     private ScheduledFuture<?> cleanupTask;

  183.     private volatile long expireAfter;

  184.     private final Object schedulerLock = new Lock();

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

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

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

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

  237.     private Repository unregisterRepository(Key location) {
  238.         return cacheMap.remove(location);
  239.     }

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

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

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

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

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

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

  270.     private static class Lock {
  271.         // Used only for its monitor.
  272.     }

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

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

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

  344.         private final File path;
  345.         private final FS fs;

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

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

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

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

  374.         @Override
  375.         public int hashCode() {
  376.             return path.hashCode();
  377.         }

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

  382.         @Override
  383.         public String toString() {
  384.             return path.toString();
  385.         }

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

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

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

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

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