View Javadoc
1   package org.eclipse.jgit.internal.storage.dfs;
2   
3   import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.GC;
4   import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.GC_REST;
5   import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.INSERT;
6   import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.UNREACHABLE_GARBAGE;
7   import static org.eclipse.jgit.internal.storage.pack.PackExt.PACK;
8   import static org.eclipse.jgit.internal.storage.pack.PackExt.REFTABLE;
9   import static org.junit.Assert.assertEquals;
10  import static org.junit.Assert.assertFalse;
11  import static org.junit.Assert.assertNotNull;
12  import static org.junit.Assert.assertSame;
13  import static org.junit.Assert.assertTrue;
14  import static org.junit.Assert.fail;
15  
16  import java.io.IOException;
17  import java.nio.charset.StandardCharsets;
18  import java.util.Collections;
19  import java.util.concurrent.TimeUnit;
20  
21  import org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource;
22  import org.eclipse.jgit.internal.storage.reftable.RefCursor;
23  import org.eclipse.jgit.internal.storage.reftable.ReftableConfig;
24  import org.eclipse.jgit.internal.storage.reftable.ReftableReader;
25  import org.eclipse.jgit.internal.storage.reftable.ReftableWriter;
26  import org.eclipse.jgit.junit.MockSystemReader;
27  import org.eclipse.jgit.junit.TestRepository;
28  import org.eclipse.jgit.lib.AnyObjectId;
29  import org.eclipse.jgit.lib.BatchRefUpdate;
30  import org.eclipse.jgit.lib.NullProgressMonitor;
31  import org.eclipse.jgit.lib.ObjectId;
32  import org.eclipse.jgit.lib.ObjectIdRef;
33  import org.eclipse.jgit.lib.Ref;
34  import org.eclipse.jgit.lib.Repository;
35  import org.eclipse.jgit.revwalk.RevBlob;
36  import org.eclipse.jgit.revwalk.RevCommit;
37  import org.eclipse.jgit.revwalk.RevWalk;
38  import org.eclipse.jgit.storage.pack.PackConfig;
39  import org.eclipse.jgit.transport.ReceiveCommand;
40  import org.eclipse.jgit.util.SystemReader;
41  import org.junit.After;
42  import org.junit.Before;
43  import org.junit.Test;
44  
45  /** Tests for pack creation and garbage expiration. */
46  public class DfsGarbageCollectorTest {
47  	private TestRepository<InMemoryRepository> git;
48  	private InMemoryRepository repo;
49  	private DfsObjDatabase odb;
50  	private MockSystemReader mockSystemReader;
51  
52  	@Before
53  	public void setUp() throws IOException {
54  		DfsRepositoryDescription desc = new DfsRepositoryDescription("test");
55  		git = new TestRepository<>(new InMemoryRepository(desc));
56  		repo = git.getRepository();
57  		odb = repo.getObjectDatabase();
58  		mockSystemReader = new MockSystemReader();
59  		SystemReader.setInstance(mockSystemReader);
60  	}
61  
62  	@After
63  	public void tearDown() {
64  		SystemReader.setInstance(null);
65  	}
66  
67  	@Test
68  	public void testCollectionWithNoGarbage() throws Exception {
69  		RevCommit commit0 = commit().message("0").create();
70  		RevCommit commit1 = commit().message("1").parent(commit0).create();
71  		git.update("master", commit1);
72  
73  		assertTrue("commit0 reachable", isReachable(repo, commit0));
74  		assertTrue("commit1 reachable", isReachable(repo, commit1));
75  
76  		// Packs start out as INSERT.
77  		assertEquals(2, odb.getPacks().length);
78  		for (DfsPackFile pack : odb.getPacks()) {
79  			assertEquals(INSERT, pack.getPackDescription().getPackSource());
80  		}
81  
82  		gcNoTtl();
83  
84  		// Single GC pack present with all objects.
85  		assertEquals(1, odb.getPacks().length);
86  		DfsPackFile pack = odb.getPacks()[0];
87  		assertEquals(GC, pack.getPackDescription().getPackSource());
88  		assertTrue("commit0 in pack", isObjectInPack(commit0, pack));
89  		assertTrue("commit1 in pack", isObjectInPack(commit1, pack));
90  	}
91  
92  	@Test
93  	public void testRacyNoReusePrefersSmaller() throws Exception {
94  		StringBuilder msg = new StringBuilder();
95  		for (int i = 0; i < 100; i++) {
96  			msg.append(i).append(": i am a teapot\n");
97  		}
98  		RevBlob a = git.blob(msg.toString());
99  		RevCommit c0 = git.commit()
100 				.add("tea", a)
101 				.message("0")
102 				.create();
103 
104 		msg.append("short and stout\n");
105 		RevBlob b = git.blob(msg.toString());
106 		RevCommit c1 = git.commit().parent(c0).tick(1)
107 				.add("tea", b)
108 				.message("1")
109 				.create();
110 		git.update("master", c1);
111 
112 		PackConfig cfg = new PackConfig();
113 		cfg.setReuseObjects(false);
114 		cfg.setReuseDeltas(false);
115 		cfg.setDeltaCompress(false);
116 		cfg.setThreads(1);
117 		DfsGarbageCollector gc = new DfsGarbageCollector(repo);
118 		gc.setGarbageTtl(0, TimeUnit.MILLISECONDS); // disable TTL
119 		gc.setPackConfig(cfg);
120 		run(gc);
121 
122 		assertEquals(1, odb.getPacks().length);
123 		DfsPackDescription large = odb.getPacks()[0].getPackDescription();
124 		assertSame(PackSource.GC, large.getPackSource());
125 
126 		cfg.setDeltaCompress(true);
127 		gc = new DfsGarbageCollector(repo);
128 		gc.setGarbageTtl(0, TimeUnit.MILLISECONDS); // disable TTL
129 		gc.setPackConfig(cfg);
130 		run(gc);
131 
132 		assertEquals(1, odb.getPacks().length);
133 		DfsPackDescription small = odb.getPacks()[0].getPackDescription();
134 		assertSame(PackSource.GC, small.getPackSource());
135 		assertTrue(
136 				"delta compression pack is smaller",
137 				small.getFileSize(PACK) < large.getFileSize(PACK));
138 		assertTrue(
139 				"large pack is older",
140 				large.getLastModified() < small.getLastModified());
141 
142 		// Forcefully reinsert the older larger GC pack.
143 		odb.commitPack(Collections.singleton(large), null);
144 		odb.clearCache();
145 		assertEquals(2, odb.getPacks().length);
146 
147 		gc = new DfsGarbageCollector(repo);
148 		gc.setGarbageTtl(0, TimeUnit.MILLISECONDS); // disable TTL
149 		run(gc);
150 
151 		assertEquals(1, odb.getPacks().length);
152 		DfsPackDescription rebuilt = odb.getPacks()[0].getPackDescription();
153 		assertEquals(small.getFileSize(PACK), rebuilt.getFileSize(PACK));
154 	}
155 
156 	@Test
157 	public void testCollectionWithGarbage() throws Exception {
158 		RevCommit commit0 = commit().message("0").create();
159 		RevCommit commit1 = commit().message("1").parent(commit0).create();
160 		git.update("master", commit0);
161 
162 		assertTrue("commit0 reachable", isReachable(repo, commit0));
163 		assertFalse("commit1 garbage", isReachable(repo, commit1));
164 		gcNoTtl();
165 
166 		assertEquals(2, odb.getPacks().length);
167 		DfsPackFile gc = null;
168 		DfsPackFile garbage = null;
169 		for (DfsPackFile pack : odb.getPacks()) {
170 			DfsPackDescription d = pack.getPackDescription();
171 			if (d.getPackSource() == GC) {
172 				gc = pack;
173 			} else if (d.getPackSource() == UNREACHABLE_GARBAGE) {
174 				garbage = pack;
175 			} else {
176 				fail("unexpected " + d.getPackSource());
177 			}
178 		}
179 
180 		assertNotNull("created GC pack", gc);
181 		assertTrue(isObjectInPack(commit0, gc));
182 
183 		assertNotNull("created UNREACHABLE_GARBAGE pack", garbage);
184 		assertTrue(isObjectInPack(commit1, garbage));
185 	}
186 
187 	@Test
188 	public void testCollectionWithGarbageAndGarbagePacksPurged()
189 			throws Exception {
190 		RevCommit commit0 = commit().message("0").create();
191 		RevCommit commit1 = commit().message("1").parent(commit0).create();
192 		git.update("master", commit0);
193 
194 		gcWithTtl();
195 		// The repository should have a GC pack with commit0 and an
196 		// UNREACHABLE_GARBAGE pack with commit1.
197 		assertEquals(2, odb.getPacks().length);
198 		boolean gcPackFound = false;
199 		boolean garbagePackFound = false;
200 		for (DfsPackFile pack : odb.getPacks()) {
201 			DfsPackDescription d = pack.getPackDescription();
202 			if (d.getPackSource() == GC) {
203 				gcPackFound = true;
204 				assertTrue("has commit0", isObjectInPack(commit0, pack));
205 				assertFalse("no commit1", isObjectInPack(commit1, pack));
206 			} else if (d.getPackSource() == UNREACHABLE_GARBAGE) {
207 				garbagePackFound = true;
208 				assertFalse("no commit0", isObjectInPack(commit0, pack));
209 				assertTrue("has commit1", isObjectInPack(commit1, pack));
210 			} else {
211 				fail("unexpected " + d.getPackSource());
212 			}
213 		}
214 		assertTrue("gc pack found", gcPackFound);
215 		assertTrue("garbage pack found", garbagePackFound);
216 
217 		gcWithTtl();
218 		// The gc operation should have removed UNREACHABLE_GARBAGE pack along with commit1.
219 		DfsPackFile[] packs = odb.getPacks();
220 		assertEquals(1, packs.length);
221 
222 		assertEquals(GC, packs[0].getPackDescription().getPackSource());
223 		assertTrue("has commit0", isObjectInPack(commit0, packs[0]));
224 		assertFalse("no commit1", isObjectInPack(commit1, packs[0]));
225 	}
226 
227 	@Test
228 	public void testCollectionWithGarbageAndRereferencingGarbage()
229 			throws Exception {
230 		RevCommit commit0 = commit().message("0").create();
231 		RevCommit commit1 = commit().message("1").parent(commit0).create();
232 		git.update("master", commit0);
233 
234 		gcWithTtl();
235 		// The repository should have a GC pack with commit0 and an
236 		// UNREACHABLE_GARBAGE pack with commit1.
237 		assertEquals(2, odb.getPacks().length);
238 		boolean gcPackFound = false;
239 		boolean garbagePackFound = false;
240 		for (DfsPackFile pack : odb.getPacks()) {
241 			DfsPackDescription d = pack.getPackDescription();
242 			if (d.getPackSource() == GC) {
243 				gcPackFound = true;
244 				assertTrue("has commit0", isObjectInPack(commit0, pack));
245 				assertFalse("no commit1", isObjectInPack(commit1, pack));
246 			} else if (d.getPackSource() == UNREACHABLE_GARBAGE) {
247 				garbagePackFound = true;
248 				assertFalse("no commit0", isObjectInPack(commit0, pack));
249 				assertTrue("has commit1", isObjectInPack(commit1, pack));
250 			} else {
251 				fail("unexpected " + d.getPackSource());
252 			}
253 		}
254 		assertTrue("gc pack found", gcPackFound);
255 		assertTrue("garbage pack found", garbagePackFound);
256 
257 		git.update("master", commit1);
258 
259 		gcWithTtl();
260 		// The gc operation should have removed the UNREACHABLE_GARBAGE pack and
261 		// moved commit1 into GC pack.
262 		DfsPackFile[] packs = odb.getPacks();
263 		assertEquals(1, packs.length);
264 
265 		assertEquals(GC, packs[0].getPackDescription().getPackSource());
266 		assertTrue("has commit0", isObjectInPack(commit0, packs[0]));
267 		assertTrue("has commit1", isObjectInPack(commit1, packs[0]));
268 	}
269 
270 	@Test
271 	public void testCollectionWithPureGarbageAndGarbagePacksPurged()
272 			throws Exception {
273 		RevCommit commit0 = commit().message("0").create();
274 		RevCommit commit1 = commit().message("1").parent(commit0).create();
275 
276 		gcWithTtl();
277 		// The repository should have a single UNREACHABLE_GARBAGE pack with commit0
278 		// and commit1.
279 		DfsPackFile[] packs = odb.getPacks();
280 		assertEquals(1, packs.length);
281 
282 		assertEquals(UNREACHABLE_GARBAGE, packs[0].getPackDescription().getPackSource());
283 		assertTrue("has commit0", isObjectInPack(commit0, packs[0]));
284 		assertTrue("has commit1", isObjectInPack(commit1, packs[0]));
285 
286 		gcWithTtl();
287 		// The gc operation should have removed UNREACHABLE_GARBAGE pack along
288 		// with commit0 and commit1.
289 		assertEquals(0, odb.getPacks().length);
290 	}
291 
292 	@Test
293 	public void testCollectionWithPureGarbageAndRereferencingGarbage()
294 			throws Exception {
295 		RevCommit commit0 = commit().message("0").create();
296 		RevCommit commit1 = commit().message("1").parent(commit0).create();
297 
298 		gcWithTtl();
299 		// The repository should have a single UNREACHABLE_GARBAGE pack with commit0
300 		// and commit1.
301 		DfsPackFile[] packs = odb.getPacks();
302 		assertEquals(1, packs.length);
303 
304 		DfsPackDescription pack = packs[0].getPackDescription();
305 		assertEquals(UNREACHABLE_GARBAGE, pack.getPackSource());
306 		assertTrue("has commit0", isObjectInPack(commit0, packs[0]));
307 		assertTrue("has commit1", isObjectInPack(commit1, packs[0]));
308 
309 		git.update("master", commit0);
310 
311 		gcWithTtl();
312 		// The gc operation should have moved commit0 into the GC pack and
313 		// removed UNREACHABLE_GARBAGE along with commit1.
314 		packs = odb.getPacks();
315 		assertEquals(1, packs.length);
316 
317 		pack = packs[0].getPackDescription();
318 		assertEquals(GC, pack.getPackSource());
319 		assertTrue("has commit0", isObjectInPack(commit0, packs[0]));
320 		assertFalse("no commit1", isObjectInPack(commit1, packs[0]));
321 	}
322 
323 	@Test
324 	public void testCollectionWithGarbageCoalescence() throws Exception {
325 		RevCommit commit0 = commit().message("0").create();
326 		RevCommit commit1 = commit().message("1").parent(commit0).create();
327 		git.update("master", commit0);
328 
329 		for (int i = 0; i < 3; i++) {
330 			commit1 = commit().message("g" + i).parent(commit1).create();
331 
332 			// Make sure we don't have more than 1 UNREACHABLE_GARBAGE pack
333 			// because they're coalesced.
334 			gcNoTtl();
335 			assertEquals(1, countPacks(UNREACHABLE_GARBAGE));
336 		}
337 	}
338 
339 	@Test
340 	public void testCollectionWithGarbageNoCoalescence() throws Exception {
341 		RevCommit commit0 = commit().message("0").create();
342 		RevCommit commit1 = commit().message("1").parent(commit0).create();
343 		git.update("master", commit0);
344 
345 		for (int i = 0; i < 3; i++) {
346 			commit1 = commit().message("g" + i).parent(commit1).create();
347 
348 			DfsGarbageCollector gc = new DfsGarbageCollector(repo);
349 			gc.setCoalesceGarbageLimit(0);
350 			gc.setGarbageTtl(0, TimeUnit.MILLISECONDS);
351 			run(gc);
352 			assertEquals(1 + i, countPacks(UNREACHABLE_GARBAGE));
353 		}
354 	}
355 
356 	@Test
357 	public void testCollectionWithGarbageCoalescenceWithShortTtl()
358 			throws Exception {
359 		RevCommit commit0 = commit().message("0").create();
360 		RevCommit commit1 = commit().message("1").parent(commit0).create();
361 		git.update("master", commit0);
362 
363 		// Create commits at 1 minute intervals with 1 hour ttl.
364 		for (int i = 0; i < 100; i++) {
365 			mockSystemReader.tick(60);
366 			commit1 = commit().message("g" + i).parent(commit1).create();
367 
368 			DfsGarbageCollector gc = new DfsGarbageCollector(repo);
369 			gc.setGarbageTtl(1, TimeUnit.HOURS);
370 			run(gc);
371 
372 			// Make sure we don't have more than 4 UNREACHABLE_GARBAGE packs
373 			// because all the packs that are created in a 20 minutes interval
374 			// should be coalesced and the packs older than 60 minutes should be
375 			// removed due to ttl.
376 			int count = countPacks(UNREACHABLE_GARBAGE);
377 			assertTrue("Garbage pack count should not exceed 4, but found "
378 					+ count, count <= 4);
379 		}
380 	}
381 
382 	@Test
383 	public void testCollectionWithGarbageCoalescenceWithLongTtl()
384 			throws Exception {
385 		RevCommit commit0 = commit().message("0").create();
386 		RevCommit commit1 = commit().message("1").parent(commit0).create();
387 		git.update("master", commit0);
388 
389 		// Create commits at 1 hour intervals with 2 days ttl.
390 		for (int i = 0; i < 100; i++) {
391 			mockSystemReader.tick(3600);
392 			commit1 = commit().message("g" + i).parent(commit1).create();
393 
394 			DfsGarbageCollector gc = new DfsGarbageCollector(repo);
395 			gc.setGarbageTtl(2, TimeUnit.DAYS);
396 			run(gc);
397 
398 			// Make sure we don't have more than 3 UNREACHABLE_GARBAGE packs
399 			// because all the packs that are created in a single day should
400 			// be coalesced and the packs older than 2 days should be
401 			// removed due to ttl.
402 			int count = countPacks(UNREACHABLE_GARBAGE);
403 			assertTrue("Garbage pack count should not exceed 3, but found "
404 					+ count, count <= 3);
405 		}
406 	}
407 
408 	@Test
409 	public void testEstimateGcPackSizeInNewRepo() throws Exception {
410 		RevCommit commit0 = commit().message("0").create();
411 		RevCommit commit1 = commit().message("1").parent(commit0).create();
412 		git.update("master", commit1);
413 
414 		// Packs start out as INSERT.
415 		long inputPacksSize = 32;
416 		assertEquals(2, odb.getPacks().length);
417 		for (DfsPackFile pack : odb.getPacks()) {
418 			assertEquals(INSERT, pack.getPackDescription().getPackSource());
419 			inputPacksSize += pack.getPackDescription().getFileSize(PACK) - 32;
420 		}
421 
422 		gcNoTtl();
423 
424 		// INSERT packs are combined into a single GC pack.
425 		assertEquals(1, odb.getPacks().length);
426 		DfsPackFile pack = odb.getPacks()[0];
427 		assertEquals(GC, pack.getPackDescription().getPackSource());
428 		assertEquals(inputPacksSize,
429 				pack.getPackDescription().getEstimatedPackSize());
430 	}
431 
432 	@Test
433 	public void testEstimateGcPackSizeWithAnExistingGcPack() throws Exception {
434 		RevCommit commit0 = commit().message("0").create();
435 		RevCommit commit1 = commit().message("1").parent(commit0).create();
436 		git.update("master", commit1);
437 
438 		gcNoTtl();
439 
440 		RevCommit commit2 = commit().message("2").parent(commit1).create();
441 		git.update("master", commit2);
442 
443 		// There will be one INSERT pack and one GC pack.
444 		assertEquals(2, odb.getPacks().length);
445 		boolean gcPackFound = false;
446 		boolean insertPackFound = false;
447 		long inputPacksSize = 32;
448 		for (DfsPackFile pack : odb.getPacks()) {
449 			DfsPackDescription d = pack.getPackDescription();
450 			if (d.getPackSource() == GC) {
451 				gcPackFound = true;
452 			} else if (d.getPackSource() == INSERT) {
453 				insertPackFound = true;
454 			} else {
455 				fail("unexpected " + d.getPackSource());
456 			}
457 			inputPacksSize += d.getFileSize(PACK) - 32;
458 		}
459 		assertTrue(gcPackFound);
460 		assertTrue(insertPackFound);
461 
462 		gcNoTtl();
463 
464 		// INSERT pack is combined into the GC pack.
465 		DfsPackFile pack = odb.getPacks()[0];
466 		assertEquals(GC, pack.getPackDescription().getPackSource());
467 		assertEquals(inputPacksSize,
468 				pack.getPackDescription().getEstimatedPackSize());
469 	}
470 
471 	@Test
472 	public void testEstimateGcRestPackSizeInNewRepo() throws Exception {
473 		RevCommit commit0 = commit().message("0").create();
474 		RevCommit commit1 = commit().message("1").parent(commit0).create();
475 		git.update("refs/notes/note1", commit1);
476 
477 		// Packs start out as INSERT.
478 		long inputPacksSize = 32;
479 		assertEquals(2, odb.getPacks().length);
480 		for (DfsPackFile pack : odb.getPacks()) {
481 			assertEquals(INSERT, pack.getPackDescription().getPackSource());
482 			inputPacksSize += pack.getPackDescription().getFileSize(PACK) - 32;
483 		}
484 
485 		gcNoTtl();
486 
487 		// INSERT packs are combined into a single GC_REST pack.
488 		assertEquals(1, odb.getPacks().length);
489 		DfsPackFile pack = odb.getPacks()[0];
490 		assertEquals(GC_REST, pack.getPackDescription().getPackSource());
491 		assertEquals(inputPacksSize,
492 				pack.getPackDescription().getEstimatedPackSize());
493 	}
494 
495 	@Test
496 	public void testEstimateGcRestPackSizeWithAnExistingGcPack()
497 			throws Exception {
498 		RevCommit commit0 = commit().message("0").create();
499 		RevCommit commit1 = commit().message("1").parent(commit0).create();
500 		git.update("refs/notes/note1", commit1);
501 
502 		gcNoTtl();
503 
504 		RevCommit commit2 = commit().message("2").parent(commit1).create();
505 		git.update("refs/notes/note2", commit2);
506 
507 		// There will be one INSERT pack and one GC_REST pack.
508 		assertEquals(2, odb.getPacks().length);
509 		boolean gcRestPackFound = false;
510 		boolean insertPackFound = false;
511 		long inputPacksSize = 32;
512 		for (DfsPackFile pack : odb.getPacks()) {
513 			DfsPackDescription d = pack.getPackDescription();
514 			if (d.getPackSource() == GC_REST) {
515 				gcRestPackFound = true;
516 			} else if (d.getPackSource() == INSERT) {
517 				insertPackFound = true;
518 			} else {
519 				fail("unexpected " + d.getPackSource());
520 			}
521 			inputPacksSize += d.getFileSize(PACK) - 32;
522 		}
523 		assertTrue(gcRestPackFound);
524 		assertTrue(insertPackFound);
525 
526 		gcNoTtl();
527 
528 		// INSERT pack is combined into the GC_REST pack.
529 		DfsPackFile pack = odb.getPacks()[0];
530 		assertEquals(GC_REST, pack.getPackDescription().getPackSource());
531 		assertEquals(inputPacksSize,
532 				pack.getPackDescription().getEstimatedPackSize());
533 	}
534 
535 	@Test
536 	public void testEstimateGcPackSizesWithGcAndGcRestPacks() throws Exception {
537 		RevCommit commit0 = commit().message("0").create();
538 		git.update("head", commit0);
539 		RevCommit commit1 = commit().message("1").parent(commit0).create();
540 		git.update("refs/notes/note1", commit1);
541 
542 		gcNoTtl();
543 
544 		RevCommit commit2 = commit().message("2").parent(commit1).create();
545 		git.update("refs/notes/note2", commit2);
546 
547 		// There will be one INSERT, one GC and one GC_REST packs.
548 		assertEquals(3, odb.getPacks().length);
549 		boolean gcPackFound = false;
550 		boolean gcRestPackFound = false;
551 		boolean insertPackFound = false;
552 		long gcPackSize = 0;
553 		long gcRestPackSize = 0;
554 		long insertPackSize = 0;
555 		for (DfsPackFile pack : odb.getPacks()) {
556 			DfsPackDescription d = pack.getPackDescription();
557 			if (d.getPackSource() == GC) {
558 				gcPackFound = true;
559 				gcPackSize = d.getFileSize(PACK);
560 			} else if (d.getPackSource() == GC_REST) {
561 				gcRestPackFound = true;
562 				gcRestPackSize = d.getFileSize(PACK);
563 			} else if (d.getPackSource() == INSERT) {
564 				insertPackFound = true;
565 				insertPackSize = d.getFileSize(PACK);
566 			} else {
567 				fail("unexpected " + d.getPackSource());
568 			}
569 		}
570 		assertTrue(gcPackFound);
571 		assertTrue(gcRestPackFound);
572 		assertTrue(insertPackFound);
573 
574 		gcNoTtl();
575 
576 		// In this test INSERT pack would be combined into the GC_REST pack.
577 		// But, as there is no good heuristic to know whether the new packs will
578 		// be combined into a GC pack or GC_REST packs, the new pick size is
579 		// considered while estimating both the GC and GC_REST packs.
580 		assertEquals(2, odb.getPacks().length);
581 		gcPackFound = false;
582 		gcRestPackFound = false;
583 		for (DfsPackFile pack : odb.getPacks()) {
584 			DfsPackDescription d = pack.getPackDescription();
585 			if (d.getPackSource() == GC) {
586 				gcPackFound = true;
587 				assertEquals(gcPackSize + insertPackSize - 32,
588 						pack.getPackDescription().getEstimatedPackSize());
589 			} else if (d.getPackSource() == GC_REST) {
590 				gcRestPackFound = true;
591 				assertEquals(gcRestPackSize + insertPackSize - 32,
592 						pack.getPackDescription().getEstimatedPackSize());
593 			} else {
594 				fail("unexpected " + d.getPackSource());
595 			}
596 		}
597 		assertTrue(gcPackFound);
598 		assertTrue(gcRestPackFound);
599 	}
600 
601 	@Test
602 	public void testEstimateUnreachableGarbagePackSize() throws Exception {
603 		RevCommit commit0 = commit().message("0").create();
604 		RevCommit commit1 = commit().message("1").parent(commit0).create();
605 		git.update("master", commit0);
606 
607 		assertTrue("commit0 reachable", isReachable(repo, commit0));
608 		assertFalse("commit1 garbage", isReachable(repo, commit1));
609 
610 		// Packs start out as INSERT.
611 		long packSize0 = 0;
612 		long packSize1 = 0;
613 		assertEquals(2, odb.getPacks().length);
614 		for (DfsPackFile pack : odb.getPacks()) {
615 			DfsPackDescription d = pack.getPackDescription();
616 			assertEquals(INSERT, d.getPackSource());
617 			if (isObjectInPack(commit0, pack)) {
618 				packSize0 = d.getFileSize(PACK);
619 			} else if (isObjectInPack(commit1, pack)) {
620 				packSize1 = d.getFileSize(PACK);
621 			} else {
622 				fail("expected object not found in the pack");
623 			}
624 		}
625 
626 		gcNoTtl();
627 
628 		assertEquals(2, odb.getPacks().length);
629 		for (DfsPackFile pack : odb.getPacks()) {
630 			DfsPackDescription d = pack.getPackDescription();
631 			if (d.getPackSource() == GC) {
632 				// Even though just commit0 will end up in GC pack, because
633 				// there is no good way to know that up front, both the pack
634 				// sizes are considered while computing the estimated size of GC
635 				// pack.
636 				assertEquals(packSize0 + packSize1 - 32,
637 						d.getEstimatedPackSize());
638 			} else if (d.getPackSource() == UNREACHABLE_GARBAGE) {
639 				// commit1 is moved to UNREACHABLE_GARBAGE pack.
640 				assertEquals(packSize1, d.getEstimatedPackSize());
641 			} else {
642 				fail("unexpected " + d.getPackSource());
643 			}
644 		}
645 	}
646 
647 	@Test
648 	public void testSinglePackForAllRefs() throws Exception {
649 		RevCommit commit0 = commit().message("0").create();
650 		git.update("head", commit0);
651 		RevCommit commit1 = commit().message("1").parent(commit0).create();
652 		git.update("refs/notes/note1", commit1);
653 
654 		DfsGarbageCollector gc = new DfsGarbageCollector(repo);
655 		gc.setGarbageTtl(0, TimeUnit.MILLISECONDS);
656 		gc.getPackConfig().setSinglePack(true);
657 		run(gc);
658 		assertEquals(1, odb.getPacks().length);
659 
660 		gc = new DfsGarbageCollector(repo);
661 		gc.setGarbageTtl(0, TimeUnit.MILLISECONDS);
662 		gc.getPackConfig().setSinglePack(false);
663 		run(gc);
664 		assertEquals(2, odb.getPacks().length);
665 	}
666 
667 	@SuppressWarnings("boxing")
668 	@Test
669 	public void producesNewReftable() throws Exception {
670 		String master = "refs/heads/master";
671 		RevCommit commit0 = commit().message("0").create();
672 		RevCommit commit1 = commit().message("1").parent(commit0).create();
673 
674 		BatchRefUpdate bru = git.getRepository().getRefDatabase()
675 				.newBatchUpdate();
676 		bru.addCommand(new ReceiveCommand(ObjectId.zeroId(), commit1, master));
677 		for (int i = 1; i <= 5100; i++) {
678 			bru.addCommand(new ReceiveCommand(ObjectId.zeroId(), commit0,
679 					String.format("refs/pulls/%04d", i)));
680 		}
681 		try (RevWalk rw = new RevWalk(git.getRepository())) {
682 			bru.execute(rw, NullProgressMonitor.INSTANCE);
683 		}
684 
685 		DfsGarbageCollector gc = new DfsGarbageCollector(repo);
686 		gc.setReftableConfig(new ReftableConfig());
687 		run(gc);
688 
689 		// Single GC pack present with all objects.
690 		assertEquals(1, odb.getPacks().length);
691 		DfsPackFile pack = odb.getPacks()[0];
692 		DfsPackDescription desc = pack.getPackDescription();
693 		assertEquals(GC, desc.getPackSource());
694 		assertTrue("commit0 in pack", isObjectInPack(commit0, pack));
695 		assertTrue("commit1 in pack", isObjectInPack(commit1, pack));
696 
697 		// Sibling REFTABLE is also present.
698 		assertTrue(desc.hasFileExt(REFTABLE));
699 		ReftableWriter.Stats stats = desc.getReftableStats();
700 		assertNotNull(stats);
701 		assertTrue(stats.totalBytes() > 0);
702 		assertEquals(5101, stats.refCount());
703 		assertEquals(1, stats.minUpdateIndex());
704 		assertEquals(1, stats.maxUpdateIndex());
705 
706 		DfsReftable table = new DfsReftable(DfsBlockCache.getInstance(), desc);
707 		try (DfsReader ctx = odb.newReader();
708 				ReftableReader rr = table.open(ctx);
709 				RefCursor rc = rr.seekRef("refs/pulls/5100")) {
710 			assertTrue(rc.next());
711 			assertEquals(commit0, rc.getRef().getObjectId());
712 			assertFalse(rc.next());
713 		}
714 	}
715 
716 	@Test
717 	public void leavesNonGcReftablesIfNotConfigured() throws Exception {
718 		String master = "refs/heads/master";
719 		RevCommit commit0 = commit().message("0").create();
720 		RevCommit commit1 = commit().message("1").parent(commit0).create();
721 		git.update(master, commit1);
722 
723 		DfsPackDescription t1 = odb.newPack(INSERT);
724 		try (DfsOutputStream out = odb.writeFile(t1, REFTABLE)) {
725 			out.write("ignored".getBytes(StandardCharsets.UTF_8));
726 			t1.addFileExt(REFTABLE);
727 		}
728 		odb.commitPack(Collections.singleton(t1), null);
729 
730 		DfsGarbageCollector gc = new DfsGarbageCollector(repo);
731 		gc.setReftableConfig(null);
732 		run(gc);
733 
734 		// Single GC pack present with all objects.
735 		assertEquals(1, odb.getPacks().length);
736 		DfsPackFile pack = odb.getPacks()[0];
737 		DfsPackDescription desc = pack.getPackDescription();
738 		assertEquals(GC, desc.getPackSource());
739 		assertTrue("commit0 in pack", isObjectInPack(commit0, pack));
740 		assertTrue("commit1 in pack", isObjectInPack(commit1, pack));
741 
742 		// Only INSERT REFTABLE above is present.
743 		DfsReftable[] tables = odb.getReftables();
744 		assertEquals(1, tables.length);
745 		assertEquals(t1, tables[0].getPackDescription());
746 	}
747 
748 	@Test
749 	public void prunesNonGcReftables() throws Exception {
750 		String master = "refs/heads/master";
751 		RevCommit commit0 = commit().message("0").create();
752 		RevCommit commit1 = commit().message("1").parent(commit0).create();
753 		git.update(master, commit1);
754 
755 		DfsPackDescription t1 = odb.newPack(INSERT);
756 		try (DfsOutputStream out = odb.writeFile(t1, REFTABLE)) {
757 			out.write("ignored".getBytes(StandardCharsets.UTF_8));
758 			t1.addFileExt(REFTABLE);
759 		}
760 		odb.commitPack(Collections.singleton(t1), null);
761 		odb.clearCache();
762 
763 		DfsGarbageCollector gc = new DfsGarbageCollector(repo);
764 		gc.setReftableConfig(new ReftableConfig());
765 		run(gc);
766 
767 		// Single GC pack present with all objects.
768 		assertEquals(1, odb.getPacks().length);
769 		DfsPackFile pack = odb.getPacks()[0];
770 		DfsPackDescription desc = pack.getPackDescription();
771 		assertEquals(GC, desc.getPackSource());
772 		assertTrue("commit0 in pack", isObjectInPack(commit0, pack));
773 		assertTrue("commit1 in pack", isObjectInPack(commit1, pack));
774 
775 		// Only sibling GC REFTABLE is present.
776 		DfsReftable[] tables = odb.getReftables();
777 		assertEquals(1, tables.length);
778 		assertEquals(desc, tables[0].getPackDescription());
779 		assertTrue(desc.hasFileExt(REFTABLE));
780 	}
781 
782 	@Test
783 	public void compactsReftables() throws Exception {
784 		String master = "refs/heads/master";
785 		RevCommit commit0 = commit().message("0").create();
786 		RevCommit commit1 = commit().message("1").parent(commit0).create();
787 		git.update(master, commit1);
788 
789 		DfsGarbageCollector gc = new DfsGarbageCollector(repo);
790 		gc.setReftableConfig(new ReftableConfig());
791 		run(gc);
792 
793 		DfsPackDescription t1 = odb.newPack(INSERT);
794 		Ref next = new ObjectIdRef.PeeledNonTag(Ref.Storage.LOOSE,
795 				"refs/heads/next", commit0.copy());
796 		try (DfsOutputStream out = odb.writeFile(t1, REFTABLE)) {
797 			ReftableWriter w = new ReftableWriter();
798 			w.setMinUpdateIndex(42);
799 			w.setMaxUpdateIndex(42);
800 			w.begin(out);
801 			w.sortAndWriteRefs(Collections.singleton(next));
802 			w.finish();
803 			t1.addFileExt(REFTABLE);
804 			t1.setReftableStats(w.getStats());
805 		}
806 		odb.commitPack(Collections.singleton(t1), null);
807 
808 		gc = new DfsGarbageCollector(repo);
809 		gc.setReftableConfig(new ReftableConfig());
810 		run(gc);
811 
812 		// Single GC pack present with all objects.
813 		assertEquals(1, odb.getPacks().length);
814 		DfsPackFile pack = odb.getPacks()[0];
815 		DfsPackDescription desc = pack.getPackDescription();
816 		assertEquals(GC, desc.getPackSource());
817 		assertTrue("commit0 in pack", isObjectInPack(commit0, pack));
818 		assertTrue("commit1 in pack", isObjectInPack(commit1, pack));
819 
820 		// Only sibling GC REFTABLE is present.
821 		DfsReftable[] tables = odb.getReftables();
822 		assertEquals(1, tables.length);
823 		assertEquals(desc, tables[0].getPackDescription());
824 		assertTrue(desc.hasFileExt(REFTABLE));
825 
826 		// GC reftable contains the compaction.
827 		DfsReftable table = new DfsReftable(DfsBlockCache.getInstance(), desc);
828 		try (DfsReader ctx = odb.newReader();
829 				ReftableReader rr = table.open(ctx);
830 				RefCursor rc = rr.allRefs()) {
831 			assertEquals(1, rr.minUpdateIndex());
832 			assertEquals(42, rr.maxUpdateIndex());
833 
834 			assertTrue(rc.next());
835 			assertEquals(master, rc.getRef().getName());
836 			assertEquals(commit1, rc.getRef().getObjectId());
837 
838 			assertTrue(rc.next());
839 			assertEquals(next.getName(), rc.getRef().getName());
840 			assertEquals(commit0, rc.getRef().getObjectId());
841 
842 			assertFalse(rc.next());
843 		}
844 	}
845 
846 	private TestRepository<InMemoryRepository>.CommitBuilder commit() {
847 		return git.commit();
848 	}
849 
850 	private void gcNoTtl() throws IOException {
851 		DfsGarbageCollector gc = new DfsGarbageCollector(repo);
852 		gc.setGarbageTtl(0, TimeUnit.MILLISECONDS); // disable TTL
853 		run(gc);
854 	}
855 
856 	private void gcWithTtl() throws IOException {
857 		// Move the clock forward by 1 minute and use the same as ttl.
858 		mockSystemReader.tick(60);
859 		DfsGarbageCollector gc = new DfsGarbageCollector(repo);
860 		gc.setGarbageTtl(1, TimeUnit.MINUTES);
861 		run(gc);
862 	}
863 
864 	private void run(DfsGarbageCollector gc) throws IOException {
865 		// adjust the current time that will be used by the gc operation.
866 		mockSystemReader.tick(1);
867 		assertTrue("gc repacked", gc.pack(null));
868 		odb.clearCache();
869 	}
870 
871 	private static boolean isReachable(Repository repo, AnyObjectId id)
872 			throws IOException {
873 		try (RevWalk rw = new RevWalk(repo)) {
874 			for (Ref ref : repo.getAllRefs().values()) {
875 				rw.markStart(rw.parseCommit(ref.getObjectId()));
876 			}
877 			for (RevCommit next; (next = rw.next()) != null;) {
878 				if (AnyObjectId.equals(next, id)) {
879 					return true;
880 				}
881 			}
882 		}
883 		return false;
884 	}
885 
886 	private boolean isObjectInPack(AnyObjectId id, DfsPackFile pack)
887 			throws IOException {
888 		try (DfsReader reader = odb.newReader()) {
889 			return pack.hasObject(reader, id);
890 		}
891 	}
892 
893 	private int countPacks(PackSource source) throws IOException {
894 		int cnt = 0;
895 		for (DfsPackFile pack : odb.getPacks()) {
896 			if (pack.getPackDescription().getPackSource() == source) {
897 				cnt++;
898 			}
899 		}
900 		return cnt;
901 	}
902 }