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