5009224dc01c31b7b9e7853f17e0101fb872d06d
[teamcity/git-plugin.git] / git-server / src / jetbrains / buildServer / buildTriggers / vcs / git / RepositoryManagerImpl.java
1 /*
2  * Copyright 2000-2018 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<String, Object> myCreateLocks = new ConcurrentHashMap<>();
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<String, Object> myWriteLocks = new ConcurrentHashMap<>();
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<String, ReadWriteLock> myRmLocks = new ConcurrentHashMap<>();
70
71   private final ConcurrentMap<String, Object> myUpdateLastUsedTimeLocks = new ConcurrentHashMap<>();
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   @Override
104   public void removeMirrorDir(@NotNull final File dir) {
105     myMirrorManager.removeMirrorDir(dir);
106   }
107
108
109   @NotNull
110   public Map<String, File> getMappings() {
111     return myMirrorManager.getMappings();
112   }
113
114   @Nullable
115   @Override
116   public String getUrl(@NotNull String cloneDirName) {
117     return myMirrorManager.getUrl(cloneDirName);
118   }
119
120   @NotNull
121   public List<File> getExpiredDirs() {
122     long now = System.currentTimeMillis();
123     List<File> result = new ArrayList<File>();
124     final File[] files = myMirrorManager.getBaseMirrorsDir().listFiles();
125     if (files == null)
126       return result;
127     for (File f : files) {
128       if (f.isDirectory() && isExpired(f, now))
129         result.add(f);
130     }
131     return result;
132   }
133
134
135   private boolean isExpired(@NotNull final File dir, long now) {
136     long lastUsedTime = getLastUsedTime(dir);
137     return now - lastUsedTime > myExpirationTimeout;
138   }
139
140
141   public long getLastUsedTime(@NotNull File dir) {
142     return myMirrorManager.getLastUsedTime(dir);
143   }
144
145   @NotNull
146   public Repository openRepository(@NotNull final URIish fetchUrl) throws VcsException {
147     final URIish canonicalURI = getCanonicalURI(fetchUrl);
148     final File dir = getMirrorDir(canonicalURI.toString());
149     return openRepository(dir, canonicalURI);
150   }
151
152
153   @NotNull
154   public Repository openRepository(@NotNull final File dir, @NotNull final URIish fetchUrl) throws VcsException {
155     final URIish canonicalURI = getCanonicalURI(fetchUrl);
156     if (isDefaultMirrorDir(dir))
157       updateLastUsedTime(dir);
158     Repository result = myRepositoryCache.get(RepositoryCache.FileKey.exact(dir, FS.DETECTED));
159     if (result == null)
160       return createRepository(dir, canonicalURI);
161     String existingRemote = result.getConfig().getString("teamcity", null, "remote");
162     if (existingRemote == null) {
163       myRepositoryCache.release(result);
164       invalidate(dir);
165       return GitServerUtil.getRepository(dir, fetchUrl);
166     }
167     if (!canonicalURI.toString().equals(existingRemote)) {
168       myRepositoryCache.release(result);
169       throw getWrongUrlError(dir, existingRemote, fetchUrl);
170     }
171     return result;
172   }
173
174   public void closeRepository(@NotNull Repository repository) {
175     myRepositoryCache.release(repository);
176   }
177
178   @NotNull
179   private Repository createRepository(@NotNull final File dir, @NotNull final URIish fetchUrl) throws VcsException {
180     return runWithDisabledRemove(dir, () -> {
181       synchronized (getCreateLock(dir)) {
182         Repository result = GitServerUtil.getRepository(dir, fetchUrl);
183         return myRepositoryCache.add(RepositoryCache.FileKey.exact(dir, FS.DETECTED), result);
184       }
185     });
186   }
187
188
189   private void updateLastUsedTime(@NotNull final File dir) {
190     Long timeNano = myLastAccessTime.get(dir);
191     //don't update last used time too often to decrease file-system activity
192     if (timeNano != null && TimeUnit.NANOSECONDS.toMinutes(System.nanoTime() - timeNano) < myConfig.getAccessTimeUpdateRateMinutes()) {
193       return;
194     }
195     Lock rmLock = getRmLock(dir).readLock();
196     rmLock.lock();
197     try {
198       synchronized (getUpdateLastUsedTimeLock(dir)) {
199         File timestamp = new File(dir, "timestamp");
200         if (!dir.exists() && !dir.mkdirs())
201           throw new IOException("Cannot create directory " + dir.getAbsolutePath());
202         if (!timestamp.exists())
203           timestamp.createNewFile();
204         FileUtil.writeFileAndReportErrors(timestamp, String.valueOf(System.currentTimeMillis()));
205         myLastAccessTime.put(dir, System.nanoTime());
206       }
207     } catch (IOException e) {
208       LOG.error("Error while updating timestamp in " + dir.getAbsolutePath(), e);
209     } finally {
210       rmLock.unlock();
211     }
212   }
213
214
215   private boolean isDefaultMirrorDir(@NotNull final File dir) {
216     File baseDir = myMirrorManager.getBaseMirrorsDir();
217     return baseDir.equals(dir.getParentFile());
218   }
219
220
221   @NotNull
222   private Object getUpdateLastUsedTimeLock(@NotNull File dir) {
223     return getOrCreate(myUpdateLastUsedTimeLocks, getCanonicalName(dir), new Object());
224   }
225
226
227   @NotNull
228   public Object getWriteLock(@NotNull final File dir) {
229     return getOrCreate(myWriteLocks, getCanonicalName(dir), new Object());
230   }
231
232
233   @NotNull
234   public ReadWriteLock getRmLock(@NotNull final File dir) {
235     return getOrCreate(myRmLocks, getCanonicalName(dir), new ReentrantReadWriteLock());
236   }
237
238   @NotNull
239   private String getCanonicalName(final @NotNull File dir) {
240     String name = dir.getName();
241     if (".".equals(name) || "..".equals(name)) {
242       // call getCanonical in special cases only
243       return FileUtil.getCanonicalFile(dir).getName();
244     }
245
246     return name;
247   }
248
249
250   @Override
251   public <T> T runWithDisabledRemove(@NotNull File dir, @NotNull VcsOperation<T> operation) throws VcsException {
252     Lock readLock = getRmLock(dir).readLock();
253     readLock.lock();
254     try {
255       return operation.run();
256     } finally {
257       readLock.unlock();
258     }
259   }
260
261
262   @Override
263   public void runWithDisabledRemove(@NotNull File dir, @NotNull VcsAction action) throws VcsException {
264     Lock readLock = getRmLock(dir).readLock();
265     readLock.lock();
266     try {
267       action.run();
268     } finally {
269       readLock.unlock();
270     }
271   }
272
273   @NotNull
274   public Object getCreateLock(File dir) {
275     return getOrCreate(myCreateLocks, getCanonicalName(dir), new Object());
276   }
277
278
279   public void cleanLocksFor(@NotNull final File dir) {
280     final String canonicalName = getCanonicalName(dir);
281     myWriteLocks.remove(canonicalName);
282     myCreateLocks.remove(canonicalName);
283     myRmLocks.remove(canonicalName);
284   }
285
286   private <K, V> V getOrCreate(ConcurrentMap<K, V> map, K key, V value) {
287     V existing = map.putIfAbsent(key, value);
288     if (existing != null)
289       return existing;
290     else
291       return value;
292   }
293
294
295   @NotNull
296   private URIish getCanonicalURI(@NotNull final URIish uri) {
297     return uri;
298 //    return new URIish()
299 //      .setScheme(uri.getScheme())
300 //      .setHost(uri.getHost())
301 //      .setPort(uri.getPort())
302 //      .setPath(uri.getPath());
303   }
304 }