Show url of the repository where 'git gc' failed
[teamcity/git-plugin.git] / git-server / src / jetbrains / buildServer / buildTriggers / vcs / git / RepositoryManagerImpl.java
1 /*
2  * Copyright 2000-2014 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package jetbrains.buildServer.buildTriggers.vcs.git;
18
19 import com.intellij.openapi.diagnostic.Logger;
20 import jetbrains.buildServer.util.FileUtil;
21 import jetbrains.buildServer.vcs.VcsException;
22 import org.eclipse.jgit.lib.Repository;
23 import org.eclipse.jgit.lib.RepositoryCache;
24 import org.eclipse.jgit.transport.URIish;
25 import org.eclipse.jgit.util.FS;
26 import org.jetbrains.annotations.NotNull;
27 import org.jetbrains.annotations.Nullable;
28
29 import java.io.File;
30 import java.io.IOException;
31 import java.util.ArrayList;
32 import java.util.List;
33 import java.util.Map;
34 import java.util.concurrent.ConcurrentHashMap;
35 import java.util.concurrent.ConcurrentMap;
36 import java.util.concurrent.TimeUnit;
37 import java.util.concurrent.locks.Lock;
38 import java.util.concurrent.locks.ReadWriteLock;
39 import java.util.concurrent.locks.ReentrantReadWriteLock;
40
41 import static jetbrains.buildServer.buildTriggers.vcs.git.GitServerUtil.getWrongUrlError;
42
43 /**
44  * @author dmitry.neverov
45  */
46 public final class RepositoryManagerImpl implements RepositoryManager {
47
48   private static final Logger LOG = Logger.getInstance(RepositoryManagerImpl.class.getName());
49
50   private final MirrorManager myMirrorManager;
51   private final long myExpirationTimeout;
52   /**
53    * During repository creation jgit checks existence of some files and directories. When several threads
54    * try to create repository concurrently some of them could see it in inconsistent state. This map contains
55    * locks for repository creation, so only one thread at a time will create repository at give dir.
56    */
57   private final ConcurrentMap<File, Object> myCreateLocks = new ConcurrentHashMap<File, Object>();
58   /**
59    * In the past jgit has some concurrency problems, in order to fix them we do only one fetch at a time.
60    * Also several concurrent fetches in single repository does not make sense since only one of them succeed.
61    * This map contains locks used for fetch and push operations.
62    */
63   private final ConcurrentMap<File, Object> myWriteLocks = new ConcurrentHashMap<File, Object>();
64   /**
65    * During cleanup unused bare repositories are removed. This map contains rw locks for repository removal.
66    * Fetch/push/create operations should be done with read lock hold, remove operation is done with write lock hold.
67    * @see Cleanup
68    */
69   private final ConcurrentMap<File, ReadWriteLock> myRmLocks = new ConcurrentHashMap<File, ReadWriteLock>();
70
71   private final ConcurrentMap<File, Object> myUpdateLastUsedTimeLocks = new ConcurrentHashMap<File, Object>();
72
73   //repo dir -> last access time (nano seconds)
74   private final ConcurrentMap<File, Long> myLastAccessTime = new ConcurrentHashMap<>();
75
76   private final AutoCloseRepositoryCache myRepositoryCache = new AutoCloseRepositoryCache();
77
78   private final ServerPluginConfig myConfig;
79
80   public RepositoryManagerImpl(@NotNull final ServerPluginConfig config, @NotNull final MirrorManager mirrorManager) {
81     myConfig = config;
82     myExpirationTimeout = config.getMirrorExpirationTimeoutMillis();
83     myMirrorManager = mirrorManager;
84   }
85
86
87   @NotNull
88   public File getBaseMirrorsDir() {
89     return myMirrorManager.getBaseMirrorsDir();
90   }
91
92
93   @NotNull
94   public File getMirrorDir(@NotNull String repositoryUrl) {
95     return myMirrorManager.getMirrorDir(repositoryUrl);
96   }
97
98
99   public void invalidate(@NotNull final File dir) {
100     myMirrorManager.invalidate(dir);
101   }
102
103
104   @NotNull
105   public Map<String, File> getMappings() {
106     return myMirrorManager.getMappings();
107   }
108
109   @Nullable
110   @Override
111   public String getUrl(@NotNull String cloneDirName) {
112     return myMirrorManager.getUrl(cloneDirName);
113   }
114
115   @NotNull
116   public List<File> getExpiredDirs() {
117     long now = System.currentTimeMillis();
118     List<File> result = new ArrayList<File>();
119     final File[] files = myMirrorManager.getBaseMirrorsDir().listFiles();
120     if (files == null)
121       return result;
122     for (File f : files) {
123       if (f.isDirectory() && isExpired(f, now))
124         result.add(f);
125     }
126     return result;
127   }
128
129
130   private boolean isExpired(@NotNull final File dir, long now) {
131     long lastUsedTime = getLastUsedTime(dir);
132     return now - lastUsedTime > myExpirationTimeout;
133   }
134
135
136   public long getLastUsedTime(@NotNull File dir) {
137     return myMirrorManager.getLastUsedTime(dir);
138   }
139
140   @NotNull
141   public Repository openRepository(@NotNull final URIish fetchUrl) throws VcsException {
142     final URIish canonicalURI = getCanonicalURI(fetchUrl);
143     final File dir = getMirrorDir(canonicalURI.toString());
144     return openRepository(dir, canonicalURI);
145   }
146
147
148   @NotNull
149   public Repository openRepository(@NotNull final File dir, @NotNull final URIish fetchUrl) throws VcsException {
150     final URIish canonicalURI = getCanonicalURI(fetchUrl);
151     if (isDefaultMirrorDir(dir))
152       updateLastUsedTime(dir);
153     Repository result = myRepositoryCache.get(RepositoryCache.FileKey.exact(dir, FS.DETECTED));
154     if (result == null)
155       return createRepository(dir, canonicalURI);
156     String existingRemote = result.getConfig().getString("teamcity", null, "remote");
157     if (existingRemote == null) {
158       myRepositoryCache.release(result);
159       invalidate(dir);
160       return GitServerUtil.getRepository(dir, fetchUrl);
161     }
162     if (!canonicalURI.toString().equals(existingRemote)) {
163       myRepositoryCache.release(result);
164       throw getWrongUrlError(dir, existingRemote, fetchUrl);
165     }
166     return result;
167   }
168
169   public void closeRepository(@NotNull Repository repository) {
170     myRepositoryCache.release(repository);
171   }
172
173   @NotNull
174   private Repository createRepository(@NotNull final File dir, @NotNull final URIish fetchUrl) throws VcsException {
175     return runWithDisabledRemove(dir, () -> {
176       synchronized (getCreateLock(dir)) {
177         Repository result = GitServerUtil.getRepository(dir, fetchUrl);
178         return myRepositoryCache.add(RepositoryCache.FileKey.exact(dir, FS.DETECTED), result);
179       }
180     });
181   }
182
183
184   private void updateLastUsedTime(@NotNull final File dir) {
185     Long timeNano = myLastAccessTime.get(dir);
186     //don't update last used time too often to decrease file-system activity
187     if (timeNano != null && TimeUnit.NANOSECONDS.toMinutes(System.nanoTime() - timeNano) < myConfig.getAccessTimeUpdateRateMinutes()) {
188       return;
189     }
190     Lock rmLock = getRmLock(dir).readLock();
191     rmLock.lock();
192     try {
193       synchronized (getUpdateLastUsedTimeLock(dir)) {
194         File timestamp = new File(dir, "timestamp");
195         if (!dir.exists() && !dir.mkdirs())
196           throw new IOException("Cannot create directory " + dir.getAbsolutePath());
197         if (!timestamp.exists())
198           timestamp.createNewFile();
199         FileUtil.writeFileAndReportErrors(timestamp, String.valueOf(System.currentTimeMillis()));
200         myLastAccessTime.put(dir, System.nanoTime());
201       }
202     } catch (IOException e) {
203       LOG.error("Error while updating timestamp in " + dir.getAbsolutePath(), e);
204     } finally {
205       rmLock.unlock();
206     }
207   }
208
209
210   private boolean isDefaultMirrorDir(@NotNull final File dir) {
211     File baseDir = myMirrorManager.getBaseMirrorsDir();
212     return baseDir.equals(dir.getParentFile());
213   }
214
215
216   @NotNull
217   private Object getUpdateLastUsedTimeLock(@NotNull File dir) {
218     try {
219       File canonical = dir.getCanonicalFile();
220       return getOrCreate(myUpdateLastUsedTimeLocks, canonical, new Object());
221     } catch (IOException e) {
222       throw new RuntimeException(e);
223     }
224   }
225
226
227   @NotNull
228   public Object getWriteLock(@NotNull final File dir) {
229     try {
230       File canonical = dir.getCanonicalFile();
231       return getOrCreate(myWriteLocks, canonical, new Object());
232     } catch (IOException e) {
233       throw new RuntimeException(e);
234     }
235   }
236
237
238   @NotNull
239   public ReadWriteLock getRmLock(@NotNull final File dir) {
240     try {
241       File canonical = dir.getCanonicalFile();
242       return getOrCreate(myRmLocks, canonical, new ReentrantReadWriteLock());
243     } catch (IOException e) {
244       throw new RuntimeException(e);
245     }
246   }
247
248
249   @Override
250   public <T> T runWithDisabledRemove(@NotNull File dir, @NotNull VcsOperation<T> operation) throws VcsException {
251     Lock readLock = getRmLock(dir).readLock();
252     readLock.lock();
253     try {
254       return operation.run();
255     } finally {
256       readLock.unlock();
257     }
258   }
259
260
261   @Override
262   public void runWithDisabledRemove(@NotNull File dir, @NotNull VcsAction action) throws VcsException {
263     Lock readLock = getRmLock(dir).readLock();
264     readLock.lock();
265     try {
266       action.run();
267     } finally {
268       readLock.unlock();
269     }
270   }
271
272   @NotNull
273   public Object getCreateLock(File dir) {
274     try {
275       File canonical = dir.getCanonicalFile();
276       return getOrCreate(myCreateLocks, canonical, new Object());
277     } catch (IOException e) {
278       throw new RuntimeException(e);
279     }
280   }
281
282
283   public void cleanLocksFor(@NotNull final File dir) {
284     try {
285       File canonical = dir.getCanonicalFile();
286       myWriteLocks.remove(canonical);
287       myCreateLocks.remove(canonical);
288       myRmLocks.remove(canonical);
289     } catch (IOException e) {
290       throw new RuntimeException(e);
291     }
292   }
293
294   private <K, V> V getOrCreate(ConcurrentMap<K, V> map, K key, V value) {
295     V existing = map.putIfAbsent(key, value);
296     if (existing != null)
297       return existing;
298     else
299       return value;
300   }
301
302
303   @NotNull
304   private URIish getCanonicalURI(@NotNull final URIish uri) {
305     return uri;
306 //    return new URIish()
307 //      .setScheme(uri.getScheme())
308 //      .setHost(uri.getHost())
309 //      .setPort(uri.getPort())
310 //      .setPath(uri.getPath());
311   }
312 }