Browse Source
DfsGarbageCollector will now enforce a maximum time to live (TTL) for UNREACHABLE_GARBAGE packs. The default TTL is 1 day, which should be enough time to avoid races with other processes that are inserting data into the repository. Change-Id: Id719e6e2a03cfc9a0c0aef8ed71d261dda14bd0c Signed-off-by: Mike Williams <miwilliams@google.com>stable-4.5
Mike Williams
9 years ago
committed by
Shawn Pearce
2 changed files with 345 additions and 11 deletions
@ -0,0 +1,239 @@
|
||||
package org.eclipse.jgit.internal.storage.dfs; |
||||
|
||||
import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.GC; |
||||
import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.INSERT; |
||||
import static org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource.UNREACHABLE_GARBAGE; |
||||
import static org.junit.Assert.assertEquals; |
||||
import static org.junit.Assert.assertFalse; |
||||
import static org.junit.Assert.assertNotEquals; |
||||
import static org.junit.Assert.assertNotNull; |
||||
import static org.junit.Assert.assertTrue; |
||||
import static org.junit.Assert.fail; |
||||
|
||||
import java.io.IOException; |
||||
import java.util.concurrent.TimeUnit; |
||||
|
||||
import org.eclipse.jgit.internal.storage.dfs.DfsObjDatabase.PackSource; |
||||
import org.eclipse.jgit.junit.TestRepository; |
||||
import org.eclipse.jgit.lib.AnyObjectId; |
||||
import org.eclipse.jgit.lib.Ref; |
||||
import org.eclipse.jgit.lib.Repository; |
||||
import org.eclipse.jgit.revwalk.RevCommit; |
||||
import org.eclipse.jgit.revwalk.RevWalk; |
||||
import org.junit.Before; |
||||
import org.junit.Test; |
||||
|
||||
public class DfsGarbageCollectorTest { |
||||
private TestRepository<InMemoryRepository> git; |
||||
private InMemoryRepository repo; |
||||
private DfsObjDatabase odb; |
||||
|
||||
@Before |
||||
public void setUp() throws IOException { |
||||
DfsRepositoryDescription desc = new DfsRepositoryDescription("test"); |
||||
git = new TestRepository<>(new InMemoryRepository(desc)); |
||||
repo = git.getRepository(); |
||||
odb = repo.getObjectDatabase(); |
||||
} |
||||
|
||||
@Test |
||||
public void testCollectionWithNoGarbage() throws Exception { |
||||
RevCommit commit0 = commit().message("0").create(); |
||||
RevCommit commit1 = commit().message("1").parent(commit0).create(); |
||||
git.update("master", commit1); |
||||
|
||||
assertTrue("commit0 reachable", isReachable(repo, commit0)); |
||||
assertTrue("commit1 reachable", isReachable(repo, commit1)); |
||||
|
||||
// Packs start out as INSERT.
|
||||
assertEquals(2, odb.getPacks().length); |
||||
for (DfsPackFile pack : odb.getPacks()) { |
||||
assertEquals(INSERT, pack.getPackDescription().getPackSource()); |
||||
} |
||||
|
||||
gcNoTtl(); |
||||
|
||||
// Single GC pack present with all objects.
|
||||
assertEquals(1, odb.getPacks().length); |
||||
DfsPackFile pack = odb.getPacks()[0]; |
||||
assertEquals(GC, pack.getPackDescription().getPackSource()); |
||||
assertTrue("commit0 in pack", isObjectInPack(commit0, pack)); |
||||
assertTrue("commit1 in pack", isObjectInPack(commit1, pack)); |
||||
} |
||||
|
||||
@Test |
||||
public void testCollectionWithGarbage() throws Exception { |
||||
RevCommit commit0 = commit().message("0").create(); |
||||
RevCommit commit1 = commit().message("1").parent(commit0).create(); |
||||
git.update("master", commit0); |
||||
|
||||
assertTrue("commit0 reachable", isReachable(repo, commit0)); |
||||
assertFalse("commit1 garbage", isReachable(repo, commit1)); |
||||
gcNoTtl(); |
||||
|
||||
assertEquals(2, odb.getPacks().length); |
||||
DfsPackFile gc = null; |
||||
DfsPackFile garbage = null; |
||||
for (DfsPackFile pack : odb.getPacks()) { |
||||
DfsPackDescription d = pack.getPackDescription(); |
||||
if (d.getPackSource() == GC) { |
||||
gc = pack; |
||||
} else if (d.getPackSource() == UNREACHABLE_GARBAGE) { |
||||
garbage = pack; |
||||
} else { |
||||
fail("unexpected " + d.getPackSource()); |
||||
} |
||||
} |
||||
|
||||
assertNotNull("created GC pack", gc); |
||||
assertTrue(isObjectInPack(commit0, gc)); |
||||
|
||||
assertNotNull("created UNREACHABLE_GARBAGE pack", garbage); |
||||
assertTrue(isObjectInPack(commit1, garbage)); |
||||
} |
||||
|
||||
@Test |
||||
public void testCollectionWithGarbageAndGarbagePacksPurged() |
||||
throws Exception { |
||||
RevCommit commit0 = commit().message("0").create(); |
||||
RevCommit commit1 = commit().message("1").parent(commit0).create(); |
||||
git.update("master", commit0); |
||||
|
||||
gcNoTtl(); |
||||
gcWithTtl(); |
||||
|
||||
// The repository has an UNREACHABLE_GARBAGE pack that could have
|
||||
// expired, but since we never purge the most recent UNREACHABLE_GARBAGE
|
||||
// pack, it must have survived the GC.
|
||||
boolean commit1Found = false; |
||||
for (DfsPackFile pack : odb.getPacks()) { |
||||
DfsPackDescription d = pack.getPackDescription(); |
||||
if (d.getPackSource() == GC) { |
||||
assertTrue("has commit0", isObjectInPack(commit0, pack)); |
||||
assertFalse("no commit1", isObjectInPack(commit1, pack)); |
||||
} else if (d.getPackSource() == UNREACHABLE_GARBAGE) { |
||||
commit1Found |= isObjectInPack(commit1, pack); |
||||
} else { |
||||
fail("unexpected " + d.getPackSource()); |
||||
} |
||||
} |
||||
assertTrue("garbage commit1 still readable", commit1Found); |
||||
|
||||
// Find oldest UNREACHABLE_GARBAGE; it will be pruned by next GC.
|
||||
DfsPackDescription oldestGarbagePack = null; |
||||
for (DfsPackFile pack : odb.getPacks()) { |
||||
DfsPackDescription d = pack.getPackDescription(); |
||||
if (d.getPackSource() == UNREACHABLE_GARBAGE) { |
||||
oldestGarbagePack = oldestPack(oldestGarbagePack, d); |
||||
} |
||||
} |
||||
assertNotNull("has UNREACHABLE_GARBAGE", oldestGarbagePack); |
||||
|
||||
gcWithTtl(); |
||||
assertTrue("has packs", odb.getPacks().length > 0); |
||||
for (DfsPackFile pack : odb.getPacks()) { |
||||
assertNotEquals(oldestGarbagePack, pack.getPackDescription()); |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
public void testCollectionWithGarbageCoalescence() throws Exception { |
||||
RevCommit commit0 = commit().message("0").create(); |
||||
RevCommit commit1 = commit().message("1").parent(commit0).create(); |
||||
git.update("master", commit0); |
||||
|
||||
for (int i = 0; i < 3; i++) { |
||||
commit1 = commit().message("g" + i).parent(commit1).create(); |
||||
|
||||
// Make sure we don't have more than 1 UNREACHABLE_GARBAGE pack
|
||||
// because they're coalesced.
|
||||
gcNoTtl(); |
||||
assertEquals(1, countPacks(UNREACHABLE_GARBAGE)); |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
public void testCollectionWithGarbageNoCoalescence() throws Exception { |
||||
RevCommit commit0 = commit().message("0").create(); |
||||
RevCommit commit1 = commit().message("1").parent(commit0).create(); |
||||
git.update("master", commit0); |
||||
|
||||
for (int i = 0; i < 3; i++) { |
||||
commit1 = commit().message("g" + i).parent(commit1).create(); |
||||
|
||||
DfsGarbageCollector gc = new DfsGarbageCollector(repo); |
||||
gc.setCoalesceGarbageLimit(0); |
||||
gc.setGarbageTtl(0, TimeUnit.MILLISECONDS); |
||||
run(gc); |
||||
assertEquals(1 + i, countPacks(UNREACHABLE_GARBAGE)); |
||||
} |
||||
} |
||||
|
||||
private TestRepository<InMemoryRepository>.CommitBuilder commit() { |
||||
return git.commit(); |
||||
} |
||||
|
||||
private void gcNoTtl() throws IOException { |
||||
DfsGarbageCollector gc = new DfsGarbageCollector(repo); |
||||
gc.setGarbageTtl(0, TimeUnit.MILLISECONDS); // disable TTL
|
||||
run(gc); |
||||
} |
||||
|
||||
private void gcWithTtl() throws InterruptedException, IOException { |
||||
// Wait for the system clock to move by at least 1 millisecond.
|
||||
// This allows the DfsGarbageCollector to recognize the boundary.
|
||||
long start = System.currentTimeMillis(); |
||||
do { |
||||
Thread.sleep(10); |
||||
} while (System.currentTimeMillis() <= start); |
||||
|
||||
DfsGarbageCollector gc = new DfsGarbageCollector(repo); |
||||
gc.setGarbageTtl(1, TimeUnit.MILLISECONDS); |
||||
run(gc); |
||||
} |
||||
|
||||
private void run(DfsGarbageCollector gc) throws IOException { |
||||
assertTrue("gc repacked", gc.pack(null)); |
||||
odb.clearCache(); |
||||
} |
||||
|
||||
private static boolean isReachable(Repository repo, AnyObjectId id) |
||||
throws IOException { |
||||
try (RevWalk rw = new RevWalk(repo)) { |
||||
for (Ref ref : repo.getAllRefs().values()) { |
||||
rw.markStart(rw.parseCommit(ref.getObjectId())); |
||||
} |
||||
for (RevCommit next; (next = rw.next()) != null;) { |
||||
if (AnyObjectId.equals(next, id)) { |
||||
return true; |
||||
} |
||||
} |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
private boolean isObjectInPack(AnyObjectId id, DfsPackFile pack) |
||||
throws IOException { |
||||
try (DfsReader reader = new DfsReader(odb)) { |
||||
return pack.hasObject(reader, id); |
||||
} |
||||
} |
||||
|
||||
private static DfsPackDescription oldestPack(DfsPackDescription a, |
||||
DfsPackDescription b) { |
||||
if (a != null && a.getLastModified() < b.getLastModified()) { |
||||
return a; |
||||
} |
||||
return b; |
||||
} |
||||
|
||||
private int countPacks(PackSource source) throws IOException { |
||||
int cnt = 0; |
||||
for (DfsPackFile pack : odb.getPacks()) { |
||||
if (pack.getPackDescription().getPackSource() == source) { |
||||
cnt++; |
||||
} |
||||
} |
||||
return cnt; |
||||
} |
||||
} |
Loading…
Reference in new issue