James Moger
2012-11-28 0bbdd9f9adf12ad9082a4c49ae1c9a0778b00bb4
commit | author | age
e92c6d 1 /*
JM 2  * Copyright 2012 gitblit.com.
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 package com.gitblit;
17
18 import java.lang.reflect.Field;
19 import java.text.MessageFormat;
20 import java.util.Calendar;
21 import java.util.Date;
22 import java.util.Map;
23 import java.util.concurrent.ConcurrentHashMap;
24 import java.util.concurrent.atomic.AtomicBoolean;
25 import java.util.concurrent.atomic.AtomicInteger;
26
27 import org.eclipse.jgit.lib.Repository;
28 import org.eclipse.jgit.storage.file.FileRepository;
29 import org.eclipse.jgit.storage.file.GC;
30 import org.eclipse.jgit.storage.file.GC.RepoStatistics;
31 import org.slf4j.Logger;
32 import org.slf4j.LoggerFactory;
33
34 import com.gitblit.models.RepositoryModel;
35 import com.gitblit.utils.FileUtils;
36
37 /**
38  * The GC executor handles periodic garbage collection in repositories.
39  * 
40  * @author James Moger
41  * 
42  */
43 public class GCExecutor implements Runnable {
44
45     public static enum GCStatus {
46         READY, COLLECTING;
47         
48         public boolean exceeds(GCStatus s) {
49             return ordinal() > s.ordinal();
50         }
51     }
52     private final Logger logger = LoggerFactory.getLogger(GCExecutor.class);
53
54     private final IStoredSettings settings;
55     
dad8b4 56     private AtomicBoolean running = new AtomicBoolean(false);
JM 57     
e92c6d 58     private AtomicBoolean forceClose = new AtomicBoolean(false);
JM 59     
60     private final Map<String, GCStatus> gcCache = new ConcurrentHashMap<String, GCStatus>();
61
62     public GCExecutor(IStoredSettings settings) {
63         this.settings = settings;
64     }
65
66     /**
67      * Indicates if the GC executor is ready to process repositories.
68      * 
69      * @return true if the GC executor is ready to process repositories
70      */
71     public boolean isReady() {
72         return settings.getBoolean(Keys.git.enableGarbageCollection, false);
dad8b4 73     }
JM 74     
75     public boolean isRunning() {
76         return running.get();
e92c6d 77     }
JM 78     
79     public boolean lock(String repositoryName) {
80         return setGCStatus(repositoryName, GCStatus.COLLECTING);
81     }
82
83     /**
84      * Tries to set a GCStatus for the specified repository.
85      * 
86      * @param repositoryName
87      * @return true if the status has been set
88      */
89     private boolean setGCStatus(String repositoryName, GCStatus status) {
90         String key = repositoryName.toLowerCase();
91         if (gcCache.containsKey(key)) {
92             if (gcCache.get(key).exceeds(GCStatus.READY)) {
93                 // already collecting or blocked
94                 return false;
95             }
96         }
97         gcCache.put(key, status);
98         return true;
99     }
100
101     /**
102      * Returns true if Gitblit is actively collecting garbage in this repository.
103      * 
104      * @param repositoryName
105      * @return true if actively collecting garbage
106      */
107     public boolean isCollectingGarbage(String repositoryName) {
108         String key = repositoryName.toLowerCase();
109         return gcCache.containsKey(key) && GCStatus.COLLECTING.equals(gcCache.get(key));
110     }
111
112     /**
113      * Resets the GC status to ready.
114      * 
115      * @param repositoryName
116      */
117     public void releaseLock(String repositoryName) {
118         gcCache.put(repositoryName.toLowerCase(), GCStatus.READY);
119     }
120     
121     public void close() {
122         forceClose.set(true);
123     }
124
125     @Override
126     public void run() {
127         if (!isReady()) {
128             return;
129         }
dad8b4 130         
JM 131         running.set(true);        
e92c6d 132         Date now = new Date();
JM 133
134         for (String repositoryName : GitBlit.self().getRepositoryList()) {
135             if (forceClose.get()) {
136                 break;
137             }
138             if (isCollectingGarbage(repositoryName)) {
139                 logger.warn(MessageFormat.format("Already collecting garbage from {0}?!?", repositoryName));
140                 continue;
141             }
142             boolean garbageCollected = false;
143             RepositoryModel model = null;
144             FileRepository repository = null;
145             try {
146                 model = GitBlit.self().getRepositoryModel(repositoryName);
147                 repository = (FileRepository) GitBlit.self().getRepository(repositoryName);
148                 if (repository == null) {
149                     logger.warn(MessageFormat.format("GCExecutor is missing repository {0}?!?", repositoryName));
150                     continue;
151                 }
152                 
153                 if (!isRepositoryIdle(repository)) {
154                     logger.debug(MessageFormat.format("GCExecutor is skipping {0} because it is not idle", repositoryName));
155                     continue;
156                 }
157
158                 // By setting the GCStatus to COLLECTING we are
159                 // disabling *all* access to this repository from Gitblit.
160                 // Think of this as a clutch in a manual transmission vehicle.
161                 if (!setGCStatus(repositoryName, GCStatus.COLLECTING)) {
162                     logger.warn(MessageFormat.format("Can not acquire GC lock for {0}, skipping", repositoryName));
163                     continue;
164                 }
165                 
166                 logger.debug(MessageFormat.format("GCExecutor locked idle repository {0}", repositoryName));
167                 
168                 GC gc = new GC(repository);
169                 RepoStatistics stats = gc.getStatistics();
170                 
171                 // determine if this is a scheduled GC
172                 Calendar cal = Calendar.getInstance();
173                 cal.setTime(model.lastGC);
174                 cal.set(Calendar.HOUR_OF_DAY, 0);
175                 cal.set(Calendar.MINUTE, 0);
176                 cal.set(Calendar.SECOND, 0);
177                 cal.set(Calendar.MILLISECOND, 0);
e26d93 178                 cal.add(Calendar.DATE, model.gcPeriod);
e92c6d 179                 Date gcDate = cal.getTime();
JM 180                 boolean shouldCollectGarbage = now.after(gcDate);
181
182                 // determine if filesize triggered GC
183                 long gcThreshold = FileUtils.convertSizeToLong(model.gcThreshold, 500*1024L);
184                 boolean hasEnoughGarbage = stats.sizeOfLooseObjects >= gcThreshold;
185
186                 // if we satisfy one of the requirements, GC
187                 boolean hasGarbage = stats.sizeOfLooseObjects > 0;
188                 if (hasGarbage && (hasEnoughGarbage || shouldCollectGarbage)) {
189                     long looseKB = stats.sizeOfLooseObjects/1024L;
190                     logger.info(MessageFormat.format("Collecting {1} KB of loose objects from {0}", repositoryName, looseKB));
191                     
192                     // do the deed
193                     gc.gc();
194                     
195                     garbageCollected = true;
196                 }
197             } catch (Exception e) {
198                 logger.error("Error collecting garbage in " + repositoryName, e);
199             } finally {
200                 // cleanup
201                 if (repository != null) {
202                     if (garbageCollected) {
203                         // update the last GC date
204                         model.lastGC = new Date();
205                         GitBlit.self().updateConfiguration(repository, model);
206                     }
207                 
208                     repository.close();
209                 }
210                 
211                 // reset the GC lock 
212                 releaseLock(repositoryName);
213                 logger.debug(MessageFormat.format("GCExecutor released GC lock for {0}", repositoryName));
214             }
215         }
dad8b4 216         
JM 217         running.set(false);
e92c6d 218     }
JM 219     
220     private boolean isRepositoryIdle(FileRepository repository) {
221         try {
222             // Read the use count.
223             // An idle use count is 2:
224             // +1 for being in the cache
225             // +1 for the repository parameter in this method
226             Field useCnt = Repository.class.getDeclaredField("useCnt");
227             useCnt.setAccessible(true);
228             int useCount = ((AtomicInteger) useCnt.get(repository)).get();
229             return useCount == 2;
230         } catch (Exception e) {
231             logger.warn(MessageFormat
232                     .format("Failed to reflectively determine use count for repository {0}",
233                             repository.getDirectory().getPath()), e);
234         }
235         return false;
236     }
237 }