summaryrefslogtreecommitdiffstats
path: root/src/main/java/com/gitblit/GCExecutor.java
blob: 681065be9608aeec6729f8ed14af629bc860a1b4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
/*
 * Copyright 2012 gitblit.com.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.gitblit;

import java.lang.reflect.Field;
import java.text.MessageFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

import org.eclipse.jgit.api.GarbageCollectCommand;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.lib.Repository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.gitblit.models.RepositoryModel;
import com.gitblit.utils.FileUtils;

/**
 * The GC executor handles periodic garbage collection in repositories.
 *
 * @author James Moger
 *
 */
public class GCExecutor implements Runnable {

	public static enum GCStatus {
		READY, COLLECTING;

		public boolean exceeds(GCStatus s) {
			return ordinal() > s.ordinal();
		}
	}
	private final Logger logger = LoggerFactory.getLogger(GCExecutor.class);

	private final IStoredSettings settings;

	private AtomicBoolean running = new AtomicBoolean(false);

	private AtomicBoolean forceClose = new AtomicBoolean(false);

	private final Map<String, GCStatus> gcCache = new ConcurrentHashMap<String, GCStatus>();

	public GCExecutor(IStoredSettings settings) {
		this.settings = settings;
	}

	/**
	 * Indicates if the GC executor is ready to process repositories.
	 *
	 * @return true if the GC executor is ready to process repositories
	 */
	public boolean isReady() {
		return settings.getBoolean(Keys.git.enableGarbageCollection, false);
	}

	public boolean isRunning() {
		return running.get();
	}

	public boolean lock(String repositoryName) {
		return setGCStatus(repositoryName, GCStatus.COLLECTING);
	}

	/**
	 * Tries to set a GCStatus for the specified repository.
	 *
	 * @param repositoryName
	 * @return true if the status has been set
	 */
	private boolean setGCStatus(String repositoryName, GCStatus status) {
		String key = repositoryName.toLowerCase();
		if (gcCache.containsKey(key)) {
			if (gcCache.get(key).exceeds(GCStatus.READY)) {
				// already collecting or blocked
				return false;
			}
		}
		gcCache.put(key, status);
		return true;
	}

	/**
	 * Returns true if Gitblit is actively collecting garbage in this repository.
	 *
	 * @param repositoryName
	 * @return true if actively collecting garbage
	 */
	public boolean isCollectingGarbage(String repositoryName) {
		String key = repositoryName.toLowerCase();
		return gcCache.containsKey(key) && GCStatus.COLLECTING.equals(gcCache.get(key));
	}

	/**
	 * Resets the GC status to ready.
	 *
	 * @param repositoryName
	 */
	public void releaseLock(String repositoryName) {
		gcCache.put(repositoryName.toLowerCase(), GCStatus.READY);
	}

	public void close() {
		forceClose.set(true);
	}

	@Override
	public void run() {
		if (!isReady()) {
			return;
		}

		running.set(true);
		Date now = new Date();

		for (String repositoryName : GitBlit.self().getRepositoryList()) {
			if (forceClose.get()) {
				break;
			}
			if (isCollectingGarbage(repositoryName)) {
				logger.warn(MessageFormat.format("Already collecting garbage from {0}?!?", repositoryName));
				continue;
			}
			boolean garbageCollected = false;
			RepositoryModel model = null;
			Repository repository = null;
			try {
				model = GitBlit.self().getRepositoryModel(repositoryName);
				repository = GitBlit.self().getRepository(repositoryName);
				if (repository == null) {
					logger.warn(MessageFormat.format("GCExecutor is missing repository {0}?!?", repositoryName));
					continue;
				}

				if (!isRepositoryIdle(repository)) {
					logger.debug(MessageFormat.format("GCExecutor is skipping {0} because it is not idle", repositoryName));
					continue;
				}

				// By setting the GCStatus to COLLECTING we are
				// disabling *all* access to this repository from Gitblit.
				// Think of this as a clutch in a manual transmission vehicle.
				if (!setGCStatus(repositoryName, GCStatus.COLLECTING)) {
					logger.warn(MessageFormat.format("Can not acquire GC lock for {0}, skipping", repositoryName));
					continue;
				}

				logger.debug(MessageFormat.format("GCExecutor locked idle repository {0}", repositoryName));

				Git git = new Git(repository);
				GarbageCollectCommand gc = git.gc();
				Properties stats = gc.getStatistics();

				// determine if this is a scheduled GC
				Calendar cal = Calendar.getInstance();
				cal.setTime(model.lastGC);
				cal.set(Calendar.HOUR_OF_DAY, 0);
				cal.set(Calendar.MINUTE, 0);
				cal.set(Calendar.SECOND, 0);
				cal.set(Calendar.MILLISECOND, 0);
				cal.add(Calendar.DATE, model.gcPeriod);
				Date gcDate = cal.getTime();
				boolean shouldCollectGarbage = now.after(gcDate);

				// determine if filesize triggered GC
				long gcThreshold = FileUtils.convertSizeToLong(model.gcThreshold, 500*1024L);
				long sizeOfLooseObjects = (Long) stats.get("sizeOfLooseObjects");
				boolean hasEnoughGarbage = sizeOfLooseObjects >= gcThreshold;

				// if we satisfy one of the requirements, GC
				boolean hasGarbage = sizeOfLooseObjects > 0;
				if (hasGarbage && (hasEnoughGarbage || shouldCollectGarbage)) {
					long looseKB = sizeOfLooseObjects/1024L;
					logger.info(MessageFormat.format("Collecting {1} KB of loose objects from {0}", repositoryName, looseKB));

					// do the deed
					gc.call();

					garbageCollected = true;
				}
			} catch (Exception e) {
				logger.error("Error collecting garbage in " + repositoryName, e);
			} finally {
				// cleanup
				if (repository != null) {
					if (garbageCollected) {
						// update the last GC date
						model.lastGC = new Date();
						GitBlit.self().updateConfiguration(repository, model);
					}

					repository.close();
				}

				// reset the GC lock
				releaseLock(repositoryName);
				logger.debug(MessageFormat.format("GCExecutor released GC lock for {0}", repositoryName));
			}
		}

		running.set(false);
	}

	private boolean isRepositoryIdle(Repository repository) {
		try {
			// Read the use count.
			// An idle use count is 2:
			// +1 for being in the cache
			// +1 for the repository parameter in this method
			Field useCnt = Repository.class.getDeclaredField("useCnt");
			useCnt.setAccessible(true);
			int useCount = ((AtomicInteger) useCnt.get(repository)).get();
			return useCount == 2;
		} catch (Exception e) {
			logger.warn(MessageFormat
					.format("Failed to reflectively determine use count for repository {0}",
							repository.getDirectory().getPath()), e);
		}
		return false;
	}
}