]> git.ipfire.org Git - thirdparty/ccache.git/commitdiff
cleanup: Improve robustness when multiple cleanups run concurrently
authorJoel Rosdahl <joel@rosdahl.net>
Mon, 12 Mar 2018 21:25:28 +0000 (22:25 +0100)
committerJoel Rosdahl <joel@rosdahl.net>
Tue, 13 Mar 2018 13:41:24 +0000 (14:41 +0100)
The file count/size counters are now intentionally subtracted even if
there file to delete has disappeared since the final cache size
calculation will be incorrect if they aren’t. This can happen when there
are several parallel ongoing cleanups of the same subdirectory.

Also removed the “delete sibling files” logic; it’s unnecessary for all
siblings except .stderr since that’s the only file in a result that is
optional. Any other missing file will be detected by
get_file_from_cache.

doc/NEWS.adoc
src/cleanup.c
test/run
test/suites/cleanup.bash

index 3ac6478857e583398488198eeb4fe877c0cf7f1a..f3f54716da62b1f39e4725a418a64ef1a9073b1e 100644 (file)
@@ -8,6 +8,12 @@ Release date: unknown
 Bug fixes
 ~~~~~~~~~
 
+- The cleanup algorithm has been fixed to not misbehave when files are removed
+  by another process while the cleanup process is running. Previously, too many
+  files could be removed from the cache if multiple cleanup processes were
+  triggered at the same time, in extreme cases trimming the cache to a much
+  smaller size than the configured limits.
+
 - Correctly hash preprocessed headers located in a ``.gch directory''.
   Previously, ccache would not pick up changes to such precompiled headers,
   risking false positive cache hits.
index 557a5ad8bc7c6f987e74a29ce4006f40cffd5801..8c46eac59b568a96dd27f2d99b5d1e65099b1391 100644 (file)
@@ -89,27 +89,19 @@ out:
 }
 
 static void
-delete_file(const char *path, size_t size)
+delete_file(const char *path, size_t size, bool update_counters)
 {
-       if (x_unlink(path) == 0) {
+       bool deleted = x_try_unlink(path) == 0;
+       if (!deleted && errno != ENOENT && errno != ESTALE) {
+               cc_log("Failed to unlink %s (%s)", path, strerror(errno));
+       } else if (update_counters) {
+               // The counters are intentionally subtracted even if there was no file to
+               // delete since the final cache size calculation will be incorrect if they
+               // aren't. (This can happen when there are several parallel ongoing
+               // cleanups of the same directory.)
                cache_size -= size;
                files_in_cache--;
-       } else if (errno != ENOENT && errno != ESTALE) {
-               cc_log("Failed to unlink %s (%s)", path, strerror(errno));
-       }
-}
-
-static void
-delete_sibling_file(const char *base, const char *extension)
-{
-       struct stat st;
-       char *path = format("%s%s", base, extension);
-       if (lstat(path, &st) == 0) {
-               delete_file(path, file_size(&st));
-       } else if (errno != ENOENT && errno != ESTALE) {
-               cc_log("Failed to stat %s: %s", path, strerror(errno));
        }
-       free(path);
 }
 
 // Sort the files we've found and delete the oldest ones until we are below the
@@ -123,7 +115,6 @@ sort_and_clean(void)
        }
 
        // Delete enough files to bring us below the threshold.
-       char *last_base = x_strdup("");
        bool cleaned = false;
        for (unsigned i = 0; i < num_files; i++) {
                const char *ext;
@@ -136,33 +127,29 @@ sort_and_clean(void)
                }
 
                ext = get_extension(files[i]->fname);
-               if (str_eq(ext, ".o")
-                   || str_eq(ext, ".d")
-                   || str_eq(ext, ".gcno")
-                   || str_eq(ext, ".dia")
-                   || str_eq(ext, ".stderr")) {
+               if (str_eq(ext, ".stderr")) {
+                       // Make sure that the .o file is deleted before .stderr, because if the
+                       // ccache process gets killed after deleting the .stderr but before
+                       // deleting the .o, the cached result will be inconsistent. (.stderr is
+                       // the only file that is optional; any other file missing from the cache
+                       // will be detected by get_file_from_cache.)
                        char *base = remove_extension(files[i]->fname);
-                       if (!str_eq(base, last_base)) { // Avoid redundant unlinks.
-                               // Make sure that all sibling files are deleted so that a cached result
-                               // is removed completely. Note the order of deletions -- the stderr
-                               // file must be deleted last because if the ccache process gets killed
-                               // after deleting the .stderr but before deleting the .o, the cached
-                               // result would be inconsistent.
-                               delete_sibling_file(base, ".o");
-                               delete_sibling_file(base, ".d");
-                               delete_sibling_file(base, ".gcno");
-                               delete_sibling_file(base, ".dia");
-                               delete_sibling_file(base, ".stderr");
-                       }
-                       free(last_base);
-                       last_base = base;
-               } else {
-                       // .manifest or unknown file.
-                       delete_file(files[i]->fname, files[i]->size);
+                       char *o_file = format("%s.o", base);
+
+                       // Don't subtract this extra deletion from the cache size; that
+                       // bookkeeping will be done when the loop reaches the .o file. If the
+                       // loop doesn't reach the .o file since the target limits have been
+                       // reached, the bookkeeping won't happen, but that small counter
+                       // discrepancy won't do much harm and it will correct itself in the next
+                       // cleanup.
+                       delete_file(o_file, 0, false);
+
+                       free(o_file);
+                       free(base);
                }
+               delete_file(files[i]->fname, files[i]->size, true);
                cleaned = true;
        }
-       free(last_base);
        return cleaned;
 }
 
index f065316cc82587a62bda6707692d1214b80c3b71..82a80541049bca92f58c656ccc197fdc59c6d09b 100755 (executable)
--- a/test/run
+++ b/test/run
@@ -97,7 +97,13 @@ sed_in_place() {
 }
 
 backdate() {
-    touch -t 199901010000 "$@"
+    if [[ $1 =~ ^[0-9]+$ ]]; then
+        m=$1
+        shift
+    else
+        m=0
+    fi
+    touch -t 1999010100$(printf "%02u" $m) "$@"
 }
 
 expect_stat() {
@@ -176,7 +182,7 @@ expect_file_count() {
     local expected=$1
     local pattern=$2
     local dir=$3
-    local actual=`find $dir -name "$pattern" | wc -l`
+    local actual=`find $dir -type f -name "$pattern" | wc -l`
     if [ $actual -ne $expected ]; then
         test_failed "Found $actual (expected $expected) $pattern files in $dir"
     fi
index 42638c762ede3a70560a25321fd755553cf9bcb8..8eaac4f6ef7721f3138f557403369118b864385c 100644 (file)
@@ -5,11 +5,9 @@ prepare_cleanup_test_dir() {
     mkdir -p $dir
     for i in $(seq 0 9); do
         printf '%4017s' '' | tr ' ' 'A' >$dir/result$i-4017.o
-        touch $dir/result$i-4017.stderr
-        touch $dir/result$i-4017.d
-        if [ $i -gt 5 ]; then
-            backdate $dir/result$i-4017.stderr
-        fi
+        backdate $((3 * i + 1)) $dir/result$i-4017.o
+        backdate $((3 * i + 2)) $dir/result$i-4017.d
+        backdate $((3 * i + 3)) $dir/result$i-4017.stderr
     done
     # NUMFILES: 30, TOTALSIZE: 40 KiB, MAXFILES: 0, MAXSIZE: 0
     echo "0 0 0 0 0 0 0 0 0 0 0 30 40 0 0" >$dir/stats
@@ -59,21 +57,21 @@ SUITE_cleanup() {
 
     # Reduce file limit
     #
-    # 21 * 16 = 336
-    $CCACHE -F 336 -M 0 >/dev/null
+    # 22 * 16 = 352
+    $CCACHE -F 352 -M 0 >/dev/null
     $CCACHE -c >/dev/null
     expect_file_count 7 '*.o' $CCACHE_DIR
     expect_file_count 7 '*.d' $CCACHE_DIR
-    expect_file_count 7 '*.stderr' $CCACHE_DIR
-    expect_stat 'files in cache' 21
+    expect_file_count 8 '*.stderr' $CCACHE_DIR
+    expect_stat 'files in cache' 22
     expect_stat 'cleanups performed' 1
-    for i in 0 1 2 3 4 5 9; do
+    for i in 0 1 2; do
         file=$CCACHE_DIR/a/result$i-4017.o
-        expect_file_exists $file
+        expect_file_missing $CCACHE_DIR/a/result$i-4017.o
     done
-    for i in 6 7 8; do
+    for i in 3 4 5 6 7 8 9; do
         file=$CCACHE_DIR/a/result$i-4017.o
-        expect_file_missing $file
+        expect_file_exists $file
     done
 
     # -------------------------------------------------------------------------
@@ -91,17 +89,17 @@ SUITE_cleanup() {
     $CCACHE -F 0 -M 256K >/dev/null
     CCACHE_LOGFILE=/tmp/foo $CCACHE -c >/dev/null
     expect_file_count 3 '*.o' $CCACHE_DIR
-    expect_file_count 3 '*.d' $CCACHE_DIR
-    expect_file_count 3 '*.stderr' $CCACHE_DIR
-    expect_stat 'files in cache' 9
+    expect_file_count 4 '*.d' $CCACHE_DIR
+    expect_file_count 4 '*.stderr' $CCACHE_DIR
+    expect_stat 'files in cache' 11
     expect_stat 'cleanups performed' 1
-    for i in 3 4 5; do
+    for i in 0 1 2 3 4 5 6; do
         file=$CCACHE_DIR/a/result$i-4017.o
-        expect_file_exists $file
+        expect_file_missing $file
     done
-    for i in 0 1 2 6 7 8 9; do
+    for i in 7 8 9; do
         file=$CCACHE_DIR/a/result$i-4017.o
-        expect_file_missing $file
+        expect_file_exists $file
     done
 
     # -------------------------------------------------------------------------
@@ -122,9 +120,9 @@ SUITE_cleanup() {
     touch empty.c
     CCACHE_LIMIT_MULTIPLE=0.9 $CCACHE_COMPILE -c empty.c -o empty.o
     expect_file_count 159 '*.o' $CCACHE_DIR
-    expect_file_count 158 '*.d' $CCACHE_DIR
-    expect_file_count 158 '*.stderr' $CCACHE_DIR
-    expect_stat 'files in cache' 475
+    expect_file_count 159 '*.d' $CCACHE_DIR
+    expect_file_count 159 '*.stderr' $CCACHE_DIR
+    expect_stat 'files in cache' 477
     expect_stat 'cleanups performed' 1
 
     # -------------------------------------------------------------------------
@@ -145,32 +143,38 @@ SUITE_cleanup() {
     touch empty.c
     CCACHE_LIMIT_MULTIPLE=0.7 $CCACHE_COMPILE -c empty.c -o empty.o
     expect_file_count 157 '*.o' $CCACHE_DIR
-    expect_file_count 156 '*.d' $CCACHE_DIR
-    expect_file_count 156 '*.stderr' $CCACHE_DIR
-    expect_stat 'files in cache' 469
+    expect_file_count 157 '*.d' $CCACHE_DIR
+    expect_file_count 157 '*.stderr' $CCACHE_DIR
+    expect_stat 'files in cache' 471
     expect_stat 'cleanups performed' 1
 
     # -------------------------------------------------------------------------
-    TEST "Cleanup of sibling files"
+    TEST ".o file is removed before .stderr"
 
     prepare_cleanup_test_dir $CCACHE_DIR/a
+    $CCACHE -F 474 -M 0 >/dev/null
+    backdate 0 $CCACHE_DIR/a/result9-4017.stderr
+    $CCACHE -c >/dev/null
+    expect_file_missing $CCACHE_DIR/a/result9-4017.stderr
+    expect_file_missing $CCACHE_DIR/a/result9-4017.o
+
+    # Counters expectedly doesn't match reality if x.stderr is found before
+    # x.o and the cleanup stops before x.o is found.
+    expect_stat 'files in cache' 29
+    expect_file_count 28 '*.*' $CCACHE_DIR/a
+
+    # -------------------------------------------------------------------------
+    TEST ".stderr file is not removed before .o"
 
-    $CCACHE -F 336 -M 0 >/dev/null
-    backdate $CCACHE_DIR/a/result2-4017.stderr
+    prepare_cleanup_test_dir $CCACHE_DIR/a
+    $CCACHE -F 474 -M 0 >/dev/null
+    backdate 0 $CCACHE_DIR/a/result9-4017.o
     $CCACHE -c >/dev/null
-    # floor(0.8 * 9) = 7
-    expect_file_count 7 '*.o' $CCACHE_DIR
-    expect_file_count 7 '*.d' $CCACHE_DIR
-    expect_file_count 7 '*.stderr' $CCACHE_DIR
-    expect_stat 'files in cache' 21
-    for i in 0 1 3 4 5 8 9; do
-        file=$CCACHE_DIR/a/result$i-4017.o
-        expect_file_exists $file
-    done
-    for i in 2 6 7; do
-        file=$CCACHE_DIR/a/result$i-4017.o
-        expect_file_missing $file
-    done
+    expect_file_exists $CCACHE_DIR/a/result9-4017.stderr
+    expect_file_missing $CCACHE_DIR/a/result9-4017.o
+
+    expect_stat 'files in cache' 29
+    expect_file_count 29 '*.*' $CCACHE_DIR/a
 
     # -------------------------------------------------------------------------
     TEST "No cleanup of new unknown file"
@@ -184,7 +188,7 @@ SUITE_cleanup() {
     $CCACHE -F 480 -M 0 >/dev/null
     $CCACHE -c >/dev/null
     expect_file_exists $CCACHE_DIR/a/abcd.unknown
-    expect_stat 'files in cache' 28
+    expect_stat 'files in cache' 30
 
     # -------------------------------------------------------------------------
     TEST "Cleanup of old unknown file"