View Javadoc
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  
11  package org.eclipse.jgit.lib;
12  
13  import java.io.File;
14  import java.io.IOException;
15  import java.util.ArrayList;
16  import java.util.Collection;
17  import java.util.Map;
18  import java.util.concurrent.ConcurrentHashMap;
19  import java.util.concurrent.ScheduledFuture;
20  import java.util.concurrent.ScheduledThreadPoolExecutor;
21  import java.util.concurrent.TimeUnit;
22  
23  import org.eclipse.jgit.annotations.NonNull;
24  import org.eclipse.jgit.errors.RepositoryNotFoundException;
25  import org.eclipse.jgit.internal.storage.file.FileRepository;
26  import org.eclipse.jgit.lib.internal.WorkQueue;
27  import org.eclipse.jgit.util.FS;
28  import org.eclipse.jgit.util.IO;
29  import org.eclipse.jgit.util.RawParseUtils;
30  import org.slf4j.Logger;
31  import org.slf4j.LoggerFactory;
32  
33  /**
34   * Cache of active {@link org.eclipse.jgit.lib.Repository} instances.
35   */
36  public class RepositoryCache {
37  	private static final Logger LOG = LoggerFactory
38  			.getLogger(RepositoryCache.class);
39  
40  	private static final RepositoryCache cache = new RepositoryCache();
41  
42  	/**
43  	 * Open an existing repository, reusing a cached instance if possible.
44  	 * <p>
45  	 * When done with the repository, the caller must call
46  	 * {@link org.eclipse.jgit.lib.Repository#close()} to decrement the
47  	 * repository's usage counter.
48  	 *
49  	 * @param location
50  	 *            where the local repository is. Typically a
51  	 *            {@link org.eclipse.jgit.lib.RepositoryCache.FileKey}.
52  	 * @return the repository instance requested; caller must close when done.
53  	 * @throws java.io.IOException
54  	 *             the repository could not be read (likely its core.version
55  	 *             property is not supported).
56  	 * @throws org.eclipse.jgit.errors.RepositoryNotFoundException
57  	 *             there is no repository at the given location.
58  	 */
59  	public static Repository open(Key location) throws IOException,
60  			RepositoryNotFoundException {
61  		return open(location, true);
62  	}
63  
64  	/**
65  	 * Open a repository, reusing a cached instance if possible.
66  	 * <p>
67  	 * When done with the repository, the caller must call
68  	 * {@link org.eclipse.jgit.lib.Repository#close()} to decrement the
69  	 * repository's usage counter.
70  	 *
71  	 * @param location
72  	 *            where the local repository is. Typically a
73  	 *            {@link org.eclipse.jgit.lib.RepositoryCache.FileKey}.
74  	 * @param mustExist
75  	 *            If true, and the repository is not found, throws {@code
76  	 *            RepositoryNotFoundException}. If false, a repository instance
77  	 *            is created and registered anyway.
78  	 * @return the repository instance requested; caller must close when done.
79  	 * @throws java.io.IOException
80  	 *             the repository could not be read (likely its core.version
81  	 *             property is not supported).
82  	 * @throws RepositoryNotFoundException
83  	 *             There is no repository at the given location, only thrown if
84  	 *             {@code mustExist} is true.
85  	 */
86  	public static Repository open(Key location, boolean mustExist)
87  			throws IOException {
88  		return cache.openRepository(location, mustExist);
89  	}
90  
91  	/**
92  	 * Register one repository into the cache.
93  	 * <p>
94  	 * During registration the cache automatically increments the usage counter,
95  	 * permitting it to retain the reference. A
96  	 * {@link org.eclipse.jgit.lib.RepositoryCache.FileKey} for the repository's
97  	 * {@link org.eclipse.jgit.lib.Repository#getDirectory()} is used to index
98  	 * the repository in the cache.
99  	 * <p>
100 	 * If another repository already is registered in the cache at this
101 	 * location, the other instance is closed.
102 	 *
103 	 * @param db
104 	 *            repository to register.
105 	 */
106 	public static void register(Repository db) {
107 		if (db.getDirectory() != null) {
108 			FileKey key = FileKey.exact(db.getDirectory(), db.getFS());
109 			cache.registerRepository(key, db);
110 		}
111 	}
112 
113 	/**
114 	 * Close and remove a repository from the cache.
115 	 * <p>
116 	 * Removes a repository from the cache, if it is still registered here, and
117 	 * close it.
118 	 *
119 	 * @param db
120 	 *            repository to unregister.
121 	 */
122 	public static void close(@NonNull Repository db) {
123 		if (db.getDirectory() != null) {
124 			FileKey key = FileKey.exact(db.getDirectory(), db.getFS());
125 			cache.unregisterAndCloseRepository(key);
126 		}
127 	}
128 
129 	/**
130 	 * Remove a repository from the cache.
131 	 * <p>
132 	 * Removes a repository from the cache, if it is still registered here. This
133 	 * method will not close the repository, only remove it from the cache. See
134 	 * {@link org.eclipse.jgit.lib.RepositoryCache#close(Repository)} to remove
135 	 * and close the repository.
136 	 *
137 	 * @param db
138 	 *            repository to unregister.
139 	 * @since 4.3
140 	 */
141 	public static void unregister(Repository db) {
142 		if (db.getDirectory() != null) {
143 			unregister(FileKey.exact(db.getDirectory(), db.getFS()));
144 		}
145 	}
146 
147 	/**
148 	 * Remove a repository from the cache.
149 	 * <p>
150 	 * Removes a repository from the cache, if it is still registered here. This
151 	 * method will not close the repository, only remove it from the cache. See
152 	 * {@link org.eclipse.jgit.lib.RepositoryCache#close(Repository)} to remove
153 	 * and close the repository.
154 	 *
155 	 * @param location
156 	 *            location of the repository to remove.
157 	 * @since 4.1
158 	 */
159 	public static void unregister(Key location) {
160 		cache.unregisterRepository(location);
161 	}
162 
163 	/**
164 	 * Get the locations of all repositories registered in the cache.
165 	 *
166 	 * @return the locations of all repositories registered in the cache.
167 	 * @since 4.1
168 	 */
169 	public static Collection<Key> getRegisteredKeys() {
170 		return cache.getKeys();
171 	}
172 
173 	static boolean isCached(@NonNull Repository repo) {
174 		File gitDir = repo.getDirectory();
175 		if (gitDir == null) {
176 			return false;
177 		}
178 		FileKey key = new FileKey(gitDir, repo.getFS());
179 		return cache.cacheMap.get(key) == repo;
180 	}
181 
182 	/**
183 	 * Unregister all repositories from the cache.
184 	 */
185 	public static void clear() {
186 		cache.clearAll();
187 	}
188 
189 	static void clearExpired() {
190 		cache.clearAllExpired();
191 	}
192 
193 	static void reconfigure(RepositoryCacheConfig repositoryCacheConfig) {
194 		cache.configureEviction(repositoryCacheConfig);
195 	}
196 
197 	private final Map<Key, Repository> cacheMap;
198 
199 	private final Lock[] openLocks;
200 
201 	private ScheduledFuture<?> cleanupTask;
202 
203 	private volatile long expireAfter;
204 
205 	private final Object schedulerLock = new Lock();
206 
207 	private RepositoryCache() {
208 		cacheMap = new ConcurrentHashMap<>();
209 		openLocks = new Lock[4];
210 		for (int i = 0; i < openLocks.length; i++) {
211 			openLocks[i] = new Lock();
212 		}
213 		configureEviction(new RepositoryCacheConfig());
214 	}
215 
216 	private void configureEviction(
217 			RepositoryCacheConfig repositoryCacheConfig) {
218 		expireAfter = repositoryCacheConfig.getExpireAfter();
219 		ScheduledThreadPoolExecutor scheduler = WorkQueue.getExecutor();
220 		synchronized (schedulerLock) {
221 			if (cleanupTask != null) {
222 				cleanupTask.cancel(false);
223 			}
224 			long delay = repositoryCacheConfig.getCleanupDelay();
225 			if (delay == RepositoryCacheConfig.NO_CLEANUP) {
226 				return;
227 			}
228 			cleanupTask = scheduler.scheduleWithFixedDelay(() -> {
229 				try {
230 					cache.clearAllExpired();
231 				} catch (Throwable e) {
232 					LOG.error(e.getMessage(), e);
233 				}
234 			}, delay, delay, TimeUnit.MILLISECONDS);
235 		}
236 	}
237 
238 	private Repository openRepository(final Key location,
239 			final boolean mustExist) throws IOException {
240 		Repository db = cacheMap.get(location);
241 		if (db == null) {
242 			synchronized (lockFor(location)) {
243 				db = cacheMap.get(location);
244 				if (db == null) {
245 					db = location.open(mustExist);
246 					cacheMap.put(location, db);
247 				} else {
248 					db.incrementOpen();
249 				}
250 			}
251 		} else {
252 			db.incrementOpen();
253 		}
254 		return db;
255 	}
256 
257 	private void registerRepository(Key location, Repository db) {
258 		try (Repository oldDb = cacheMap.put(location, db)) {
259 			// oldDb is auto-closed
260 		}
261 	}
262 
263 	private Repository unregisterRepository(Key location) {
264 		return cacheMap.remove(location);
265 	}
266 
267 	private boolean isExpired(Repository db) {
268 		return db != null && db.useCnt.get() <= 0
269 			&& (System.currentTimeMillis() - db.closedAt.get() > expireAfter);
270 	}
271 
272 	private void unregisterAndCloseRepository(Key location) {
273 		synchronized (lockFor(location)) {
274 			Repository oldDb = unregisterRepository(location);
275 			if (oldDb != null) {
276 				oldDb.doClose();
277 			}
278 		}
279 	}
280 
281 	private Collection<Key> getKeys() {
282 		return new ArrayList<>(cacheMap.keySet());
283 	}
284 
285 	private void clearAllExpired() {
286 		for (Repository db : cacheMap.values()) {
287 			if (isExpired(db)) {
288 				RepositoryCache.close(db);
289 			}
290 		}
291 	}
292 
293 	private void clearAll() {
294 		for (Key k : cacheMap.keySet()) {
295 			unregisterAndCloseRepository(k);
296 		}
297 	}
298 
299 	private Lock lockFor(Key location) {
300 		return openLocks[(location.hashCode() >>> 1) % openLocks.length];
301 	}
302 
303 	private static class Lock {
304 		// Used only for its monitor.
305 	}
306 
307 	/**
308 	 * Abstract hash key for {@link RepositoryCache} entries.
309 	 * <p>
310 	 * A Key instance should be lightweight, and implement hashCode() and
311 	 * equals() such that two Key instances are equal if they represent the same
312 	 * Repository location.
313 	 */
314 	public static interface Key {
315 		/**
316 		 * Called by {@link RepositoryCache#open(Key)} if it doesn't exist yet.
317 		 * <p>
318 		 * If a repository does not exist yet in the cache, the cache will call
319 		 * this method to acquire a handle to it.
320 		 *
321 		 * @param mustExist
322 		 *            true if the repository must exist in order to be opened;
323 		 *            false if a new non-existent repository is permitted to be
324 		 *            created (the caller is responsible for calling create).
325 		 * @return the new repository instance.
326 		 * @throws IOException
327 		 *             the repository could not be read (likely its core.version
328 		 *             property is not supported).
329 		 * @throws RepositoryNotFoundException
330 		 *             There is no repository at the given location, only thrown
331 		 *             if {@code mustExist} is true.
332 		 */
333 		Repository open(boolean mustExist) throws IOException,
334 				RepositoryNotFoundException;
335 	}
336 
337 	/** Location of a Repository, using the standard java.io.File API. */
338 	public static class FileKey implements Key {
339 		/**
340 		 * Obtain a pointer to an exact location on disk.
341 		 * <p>
342 		 * No guessing is performed, the given location is exactly the GIT_DIR
343 		 * directory of the repository.
344 		 *
345 		 * @param directory
346 		 *            location where the repository database is.
347 		 * @param fs
348 		 *            the file system abstraction which will be necessary to
349 		 *            perform certain file system operations.
350 		 * @return a key for the given directory.
351 		 * @see #lenient(File, FS)
352 		 */
353 		public static FileKey exact(File directory, FS fs) {
354 			return new FileKey(directory, fs);
355 		}
356 
357 		/**
358 		 * Obtain a pointer to a location on disk.
359 		 * <p>
360 		 * The method performs some basic guessing to locate the repository.
361 		 * Searched paths are:
362 		 * <ol>
363 		 * <li>{@code directory} // assume exact match</li>
364 		 * <li>{@code directory} + "/.git" // assume working directory</li>
365 		 * <li>{@code directory} + ".git" // assume bare</li>
366 		 * </ol>
367 		 *
368 		 * @param directory
369 		 *            location where the repository database might be.
370 		 * @param fs
371 		 *            the file system abstraction which will be necessary to
372 		 *            perform certain file system operations.
373 		 * @return a key for the given directory.
374 		 * @see #exact(File, FS)
375 		 */
376 		public static FileKey lenient(File directory, FS fs) {
377 			final File gitdir = resolve(directory, fs);
378 			return new FileKey(gitdir != null ? gitdir : directory, fs);
379 		}
380 
381 		private final File path;
382 		private final FS fs;
383 
384 		/**
385 		 * @param directory
386 		 *            exact location of the repository.
387 		 * @param fs
388 		 *            the file system abstraction which will be necessary to
389 		 *            perform certain file system operations.
390 		 */
391 		protected FileKey(File directory, FS fs) {
392 			path = canonical(directory);
393 			this.fs = fs;
394 		}
395 
396 		private static File canonical(File path) {
397 			try {
398 				return path.getCanonicalFile();
399 			} catch (IOException e) {
400 				return path.getAbsoluteFile();
401 			}
402 		}
403 
404 		/** @return location supplied to the constructor. */
405 		public final File getFile() {
406 			return path;
407 		}
408 
409 		@Override
410 		public Repository open(boolean mustExist) throws IOException {
411 			if (mustExist && !isGitRepository(path, fs))
412 				throw new RepositoryNotFoundException(path);
413 			return new FileRepository(path);
414 		}
415 
416 		@Override
417 		public int hashCode() {
418 			return path.hashCode();
419 		}
420 
421 		@Override
422 		public boolean equals(Object o) {
423 			return o instanceof FileKey && path.equals(((FileKey) o).path);
424 		}
425 
426 		@Override
427 		public String toString() {
428 			return path.toString();
429 		}
430 
431 		/**
432 		 * Guess if a directory contains a Git repository.
433 		 * <p>
434 		 * This method guesses by looking for the existence of some key files
435 		 * and directories.
436 		 *
437 		 * @param dir
438 		 *            the location of the directory to examine.
439 		 * @param fs
440 		 *            the file system abstraction which will be necessary to
441 		 *            perform certain file system operations.
442 		 * @return true if the directory "looks like" a Git repository; false if
443 		 *         it doesn't look enough like a Git directory to really be a
444 		 *         Git directory.
445 		 */
446 		public static boolean isGitRepository(File dir, FS fs) {
447 			return fs.resolve(dir, Constants.OBJECTS).exists()
448 					&& fs.resolve(dir, "refs").exists() //$NON-NLS-1$
449 					&& (fs.resolve(dir, Constants.REFTABLE).exists()
450 							|| isValidHead(new File(dir, Constants.HEAD)));
451 		}
452 
453 		private static boolean isValidHead(File head) {
454 			final String ref = readFirstLine(head);
455 			return ref != null
456 					&& (ref.startsWith("ref: refs/") || ObjectId.isId(ref)); //$NON-NLS-1$
457 		}
458 
459 		private static String readFirstLine(File head) {
460 			try {
461 				final byte[] buf = IO.readFully(head, 4096);
462 				int n = buf.length;
463 				if (n == 0)
464 					return null;
465 				if (buf[n - 1] == '\n')
466 					n--;
467 				return RawParseUtils.decode(buf, 0, n);
468 			} catch (IOException e) {
469 				return null;
470 			}
471 		}
472 
473 		/**
474 		 * Guess the proper path for a Git repository.
475 		 * <p>
476 		 * The method performs some basic guessing to locate the repository.
477 		 * Searched paths are:
478 		 * <ol>
479 		 * <li>{@code directory} // assume exact match</li>
480 		 * <li>{@code directory} + "/.git" // assume working directory</li>
481 		 * <li>{@code directory} + ".git" // assume bare</li>
482 		 * </ol>
483 		 *
484 		 * @param directory
485 		 *            location to guess from. Several permutations are tried.
486 		 * @param fs
487 		 *            the file system abstraction which will be necessary to
488 		 *            perform certain file system operations.
489 		 * @return the actual directory location if a better match is found;
490 		 *         null if there is no suitable match.
491 		 */
492 		public static File resolve(File directory, FS fs) {
493 			if (isGitRepository(directory, fs))
494 				return directory;
495 			if (isGitRepository(new File(directory, Constants.DOT_GIT), fs))
496 				return new File(directory, Constants.DOT_GIT);
497 
498 			final String name = directory.getName();
499 			final File parent = directory.getParentFile();
500 			if (isGitRepository(new File(parent, name + Constants.DOT_GIT_EXT), fs))
501 				return new File(parent, name + Constants.DOT_GIT_EXT);
502 			return null;
503 		}
504 	}
505 }