diff --git a/src/main/java/com/google/devtools/build/lib/remote/disk/DiskCacheGarbageCollector.java b/src/main/java/com/google/devtools/build/lib/remote/disk/DiskCacheGarbageCollector.java index dbfabcb509b8ac..f714e9c543feac 100644 --- a/src/main/java/com/google/devtools/build/lib/remote/disk/DiskCacheGarbageCollector.java +++ b/src/main/java/com/google/devtools/build/lib/remote/disk/DiskCacheGarbageCollector.java @@ -32,6 +32,7 @@ import java.util.List; import java.util.Optional; import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicLong; /** * A garbage collector for the disk cache. @@ -99,6 +100,12 @@ private long getTimeCutoff() { } } + private record DeletionStats(long deletedEntries, long deletedBytes) {} + + /** Stats for a garbage collection run. */ + public record CollectionStats( + long totalEntries, long totalBytes, long deletedEntries, long deletedBytes) {} + private final Path root; private final CollectionPolicy policy; private final ExecutorService executorService; @@ -135,7 +142,7 @@ public CollectionPolicy getPolicy() { * @throws IOException if an I/O error occurred * @throws InterruptedException if the thread was interrupted */ - public void run() throws IOException, InterruptedException { + public CollectionStats run() throws IOException, InterruptedException { EntryScanner scanner = new EntryScanner(); EntryDeleter deleter = new EntryDeleter(); @@ -143,10 +150,16 @@ public void run() throws IOException, InterruptedException { List entriesToDelete = policy.getEntriesToDelete(allEntries); for (Entry entry : entriesToDelete) { - deleter.delete(root.getRelative(entry.path())); + deleter.delete(root.getRelative(entry.path()), entry.size()); } - deleter.await(); + DeletionStats deletionStats = deleter.await(); + + return new CollectionStats( + allEntries.size(), + allEntries.stream().mapToLong(Entry::size).sum(), + deletionStats.deletedEntries(), + deletionStats.deletedBytes()); } /** Lists all disk cache entries, performing I/O in parallel. */ @@ -203,6 +216,9 @@ private void visitDirectory(Path path) { /** Deletes disk cache entries, performing I/O in parallel. */ private final class EntryDeleter extends AbstractQueueVisitor { + private final AtomicLong deletedEntries = new AtomicLong(0); + private final AtomicLong deletedBytes = new AtomicLong(0); + EntryDeleter() { super( executorService, @@ -212,11 +228,14 @@ private final class EntryDeleter extends AbstractQueueVisitor { } /** Enqueues an entry to be deleted. */ - void delete(Path path) { + void delete(Path path, long size) { execute( () -> { try { - path.delete(); + if (path.delete()) { + deletedEntries.incrementAndGet(); + deletedBytes.addAndGet(size); + } } catch (IOException e) { throw new IORuntimeException(e); } @@ -224,12 +243,13 @@ void delete(Path path) { } /** Waits for all enqueued deletions to complete. */ - void await() throws IOException, InterruptedException { + DeletionStats await() throws IOException, InterruptedException { try { awaitQuiescence(true); } catch (IORuntimeException e) { throw e.getCauseIOException(); } + return new DeletionStats(deletedEntries.get(), deletedBytes.get()); } } } diff --git a/src/main/java/com/google/devtools/build/lib/remote/disk/DiskCacheGarbageCollectorIdleTask.java b/src/main/java/com/google/devtools/build/lib/remote/disk/DiskCacheGarbageCollectorIdleTask.java index 68bdf688538973..5530986400a377 100644 --- a/src/main/java/com/google/devtools/build/lib/remote/disk/DiskCacheGarbageCollectorIdleTask.java +++ b/src/main/java/com/google/devtools/build/lib/remote/disk/DiskCacheGarbageCollectorIdleTask.java @@ -16,6 +16,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.flogger.GoogleLogger; import com.google.devtools.build.lib.remote.disk.DiskCacheGarbageCollector.CollectionPolicy; +import com.google.devtools.build.lib.remote.disk.DiskCacheGarbageCollector.CollectionStats; import com.google.devtools.build.lib.remote.options.RemoteOptions; import com.google.devtools.build.lib.server.IdleTask; import com.google.devtools.build.lib.vfs.Path; @@ -84,12 +85,30 @@ public Duration delay() { public void run() { try { logger.atInfo().log("Disk cache garbage collection started"); - gc.run(); - logger.atInfo().log("Disk cache garbage collection finished"); + CollectionStats stats = gc.run(); + logger.atInfo().log( + "Disk cache garbage collection finished: deleted %d of %d files, reclaimed %s of %s", + stats.deletedEntries(), + stats.totalEntries(), + formatBytes(stats.deletedBytes()), + formatBytes(stats.totalBytes())); } catch (IOException e) { logger.atInfo().withCause(e).log("Disk cache garbage collection failed"); } catch (InterruptedException e) { logger.atInfo().withCause(e).log("Disk cache garbage collection interrupted"); } } + + private static String formatBytes(long bytes) { + if (bytes >= 1024 * 1024 * 1024) { + return "%3fGB".formatted((double) bytes / (1024 * 1024 * 1024)); + } + if (bytes >= 1024 * 1024) { + return "%3fMB".formatted((double) bytes / (1024 * 1024)); + } + if (bytes >= 1024) { + return "%3fKB".formatted((double) bytes / 1024); + } + return "%dB".formatted(bytes); + } } diff --git a/src/test/java/com/google/devtools/build/lib/remote/disk/DiskCacheGarbageCollectorTest.java b/src/test/java/com/google/devtools/build/lib/remote/disk/DiskCacheGarbageCollectorTest.java index 81e00165cee892..9e3fc3648bac6a 100644 --- a/src/test/java/com/google/devtools/build/lib/remote/disk/DiskCacheGarbageCollectorTest.java +++ b/src/test/java/com/google/devtools/build/lib/remote/disk/DiskCacheGarbageCollectorTest.java @@ -13,9 +13,11 @@ // limitations under the License. package com.google.devtools.build.lib.remote.disk; +import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; import com.google.common.util.concurrent.MoreExecutors; +import com.google.devtools.build.lib.remote.disk.DiskCacheGarbageCollector.CollectionStats; import com.google.devtools.build.lib.testutil.TestUtils; import com.google.devtools.build.lib.vfs.Path; import java.io.IOException; @@ -56,8 +58,9 @@ public void sizePolicy_noCollection() throws Exception { Entry.of("ac/123", kbytes(1), Instant.now()), Entry.of("cas/456", kbytes(1), Instant.now())); - runGarbageCollector(Optional.of(kbytes(2)), Optional.empty()); + CollectionStats stats = runGarbageCollector(Optional.of(kbytes(2)), Optional.empty()); + assertThat(stats).isEqualTo(new CollectionStats(2, kbytes(2), 0, 0)); assertFilesExist("ac/123", "cas/456"); } @@ -69,8 +72,9 @@ public void sizePolicy_collectsOldest() throws Exception { Entry.of("ac/abc", kbytes(1), daysAgo(3)), Entry.of("cas/def", kbytes(1), daysAgo(4))); - runGarbageCollector(Optional.of(kbytes(2)), Optional.empty()); + CollectionStats stats = runGarbageCollector(Optional.of(kbytes(2)), Optional.empty()); + assertThat(stats).isEqualTo(new CollectionStats(4, kbytes(4), 2, kbytes(2))); assertFilesExist("ac/123", "cas/456"); assertFilesDoNotExist("ac/abc", "cas/def"); } @@ -81,8 +85,9 @@ public void agePolicy_noCollection() throws Exception { Entry.of("ac/123", kbytes(1), Instant.now()), Entry.of("cas/456", kbytes(1), Instant.now())); - runGarbageCollector(Optional.empty(), Optional.of(days(3))); + CollectionStats stats = runGarbageCollector(Optional.empty(), Optional.of(days(3))); + assertThat(stats).isEqualTo(new CollectionStats(2, kbytes(2), 0, 0)); assertFilesExist("ac/123", "cas/456"); } @@ -94,8 +99,9 @@ public void agePolicy_collectsOldest() throws Exception { Entry.of("ac/abc", kbytes(1), daysAgo(4)), Entry.of("cas/def", kbytes(1), daysAgo(5))); - runGarbageCollector(Optional.empty(), Optional.of(Duration.ofDays(3))); + CollectionStats stats = runGarbageCollector(Optional.empty(), Optional.of(Duration.ofDays(3))); + assertThat(stats).isEqualTo(new CollectionStats(4, kbytes(4), 2, kbytes(2))); assertFilesExist("ac/123", "cas/456"); assertFilesDoNotExist("ac/abc", "cas/def"); } @@ -106,8 +112,9 @@ public void sizeAndAgePolicy_noCollection() throws Exception { Entry.of("ac/123", kbytes(1), Instant.now()), Entry.of("cas/456", kbytes(1), Instant.now())); - runGarbageCollector(Optional.of(kbytes(2)), Optional.of(days(1))); + CollectionStats stats = runGarbageCollector(Optional.of(kbytes(2)), Optional.of(days(1))); + assertThat(stats).isEqualTo(new CollectionStats(2, kbytes(2), 0, 0)); assertFilesExist("ac/123", "cas/456"); } @@ -119,8 +126,9 @@ public void sizeAndAgePolicy_sizeMoreRestrictiveThanAge_collectsOldest() throws Entry.of("ac/abc", kbytes(1), daysAgo(3)), Entry.of("cas/def", kbytes(1), daysAgo(4))); - runGarbageCollector(Optional.of(kbytes(2)), Optional.of(days(4))); + CollectionStats stats = runGarbageCollector(Optional.of(kbytes(2)), Optional.of(days(4))); + assertThat(stats).isEqualTo(new CollectionStats(4, kbytes(4), 2, kbytes(2))); assertFilesExist("ac/123", "cas/456"); assertFilesDoNotExist("ac/abc", "cas/def"); } @@ -133,8 +141,9 @@ public void sizeAndAgePolicy_ageMoreRestrictiveThanSize_collectsOldest() throws Entry.of("ac/abc", kbytes(1), daysAgo(3)), Entry.of("cas/def", kbytes(1), daysAgo(4))); - runGarbageCollector(Optional.of(kbytes(3)), Optional.of(days(3))); + CollectionStats stats = runGarbageCollector(Optional.of(kbytes(3)), Optional.of(days(3))); + assertThat(stats).isEqualTo(new CollectionStats(4, kbytes(4), 2, kbytes(2))); assertFilesExist("ac/123", "cas/456"); assertFilesDoNotExist("ac/abc", "cas/def"); } @@ -144,8 +153,9 @@ public void ignoresTmpAndGcSubdirectories() throws Exception { writeFiles( Entry.of("gc/foo", kbytes(1), daysAgo(1)), Entry.of("tmp/foo", kbytes(1), daysAgo(1))); - runGarbageCollector(Optional.of(1L), Optional.of(days(1))); + CollectionStats stats = runGarbageCollector(Optional.of(1L), Optional.of(days(1))); + assertThat(stats).isEqualTo(new CollectionStats(0, 0, 0, 0)); assertFilesExist("gc/foo", "tmp/foo"); } @@ -167,14 +177,14 @@ private void assertFilesDoNotExist(String... relativePaths) throws IOException { } } - private void runGarbageCollector(Optional maxSizeBytes, Optional maxAge) - throws Exception { + private CollectionStats runGarbageCollector( + Optional maxSizeBytes, Optional maxAge) throws Exception { var gc = new DiskCacheGarbageCollector( rootDir, executorService, new DiskCacheGarbageCollector.CollectionPolicy(maxSizeBytes, maxAge)); - gc.run(); + return gc.run(); } private void writeFiles(Entry... entries) throws IOException {