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