]> git.ipfire.org Git - thirdparty/ccache.git/commitdiff
C++-ify lockfile routines
authorJoel Rosdahl <joel@rosdahl.net>
Fri, 6 Mar 2020 18:39:32 +0000 (19:39 +0100)
committerJoel Rosdahl <joel@rosdahl.net>
Tue, 10 Mar 2020 20:05:01 +0000 (21:05 +0100)
Makefile.in
src/Lockfile.cpp [new file with mode: 0644]
src/Lockfile.hpp [new file with mode: 0644]
src/stats.cpp
unittest/main.cpp
unittest/test_Lockfile.cpp [moved from src/lockfile.hpp with 57% similarity]

index 6600dd3d45f7d4f016e522f46c99d916ad405fbf..fb070555dfe88efb0b8478755238014a29b9b846 100644 (file)
@@ -42,6 +42,7 @@ non_third_party_sources = \
     src/Context.cpp \
     src/Counters.cpp \
     src/Decompressor.cpp \
+    src/Lockfile.cpp \
     src/NullCompressor.cpp \
     src/NullDecompressor.cpp \
     src/ProgressBar.cpp \
@@ -60,7 +61,6 @@ non_third_party_sources = \
     src/hashutil.cpp \
     src/language.cpp \
     src/legacy_util.cpp \
-    src/lockfile.cpp \
     src/logging.cpp \
     src/manifest.cpp \
     src/result.cpp \
@@ -86,6 +86,7 @@ test_suites += unittest/test_Checksum.cpp
 test_suites += unittest/test_Compression.cpp
 test_suites += unittest/test_Config.cpp
 test_suites += unittest/test_FormatNonstdStringView.cpp
+test_suites += unittest/test_Lockfile.cpp
 test_suites += unittest/test_NullCompression.cpp
 test_suites += unittest/test_ScopeGuard.cpp
 test_suites += unittest/test_Stat.cpp
@@ -97,7 +98,6 @@ test_suites += unittest/test_compopt.cpp
 test_suites += unittest/test_hash.cpp
 test_suites += unittest/test_hashutil.cpp
 test_suites += unittest/test_legacy_util.cpp
-test_suites += unittest/test_lockfile.cpp
 
 test_sources += unittest/catch2_tests.cpp
 test_sources += unittest/framework.cpp
diff --git a/src/Lockfile.cpp b/src/Lockfile.cpp
new file mode 100644 (file)
index 0000000..7aba595
--- /dev/null
@@ -0,0 +1,216 @@
+// Copyright (C) 2020 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#include "Lockfile.hpp"
+
+#include "Util.hpp"
+#include "legacy_util.hpp"
+#include "logging.hpp"
+
+#include "third_party/fmt/core.h"
+
+namespace {
+
+#ifndef _WIN32
+
+bool
+do_acquire_posix(const std::string& lockfile, uint32_t staleness_limit)
+{
+  const uint32_t max_to_sleep = 10000; // Microseconds.
+  uint32_t to_sleep = 1000;            // Microseconds.
+  uint32_t slept = 0;                  // Microseconds.
+  std::string initial_content;
+
+  while (true) {
+    std::string my_content =
+      fmt::format("{}:{}:{}", get_hostname(), getpid(), time(nullptr));
+
+    if (symlink(my_content.c_str(), lockfile.c_str()) == 0) {
+      // We got the lock.
+      return true;
+    }
+
+    int saved_errno = errno;
+    cc_log("lockfile_acquire: symlink %s: %s",
+           lockfile.c_str(),
+           strerror(saved_errno));
+    if (saved_errno == ENOENT) {
+      // Directory doesn't exist?
+      if (Util::create_dir(Util::dir_name(lockfile))) {
+        // OK. Retry.
+        continue;
+      }
+    }
+
+    if (saved_errno == EPERM) {
+      // The file system does not support symbolic links. We have no choice but
+      // to grant the lock anyway.
+      return true;
+    }
+
+    if (saved_errno != EEXIST) {
+      // Directory doesn't exist or isn't writable?
+      return false;
+    }
+
+    std::string content = Util::read_link(lockfile);
+    if (content.empty()) {
+      if (errno == ENOENT) {
+        // The symlink was removed after the symlink() call above, so retry
+        // acquiring it.
+        continue;
+      } else {
+        cc_log("lockfile_acquire: readlink %s: %s",
+               lockfile.c_str(),
+               strerror(errno));
+        return false;
+      }
+    }
+
+    if (content == my_content) {
+      // Lost NFS reply?
+      cc_log("lockfile_acquire: symlink %s failed but we got the lock anyway",
+             lockfile.c_str());
+      return true;
+    }
+
+    // A possible improvement here would be to check if the process holding the
+    // lock is still alive and break the lock early if it isn't.
+    cc_log("lockfile_acquire: lock info for %s: %s",
+           lockfile.c_str(),
+           content.c_str());
+
+    if (initial_content.empty()) {
+      initial_content = content;
+    }
+
+    if (slept <= staleness_limit) {
+      cc_log("lockfile_acquire: failed to acquire %s; sleeping %u microseconds",
+             lockfile.c_str(),
+             to_sleep);
+      usleep(to_sleep);
+      slept += to_sleep;
+      to_sleep = std::min(max_to_sleep, 2 * to_sleep);
+    } else if (content != initial_content) {
+      cc_log("lockfile_acquire: gave up acquiring %s", lockfile.c_str());
+      return false;
+    } else {
+      // The lock seems to be stale -- break it and try again.
+      cc_log("lockfile_acquire: breaking %s", lockfile.c_str());
+      if (tmp_unlink(lockfile.c_str()) != 0) {
+        cc_log("Failed to unlink %s: %s", lockfile.c_str(), strerror(errno));
+        return false;
+      }
+      to_sleep = 1000;
+      slept = 0;
+      initial_content.clear();
+    }
+  }
+}
+
+#else // !_WIN32
+
+HANDLE
+do_acquire_win32(const std::string& lockfile, uint32_t staleness_limit)
+{
+  unsigned to_sleep = 1000;      // Microseconds.
+  unsigned max_to_sleep = 10000; // Microseconds.
+  unsigned slept = 0;            // Microseconds.
+  HANDLE handle;
+
+  while (true) {
+    DWORD flags = FILE_ATTRIBUTE_NORMAL | FILE_FLAG_DELETE_ON_CLOSE;
+    handle = CreateFile(lockfile.c_str(),
+                        GENERIC_WRITE, // desired access
+                        0,             // shared mode (0 = not shared)
+                        NULL,          // security attributes
+                        CREATE_ALWAYS, // creation disposition
+                        flags,         // flags and attributes
+                        NULL           // template file
+    );
+    if (handle != INVALID_HANDLE_VALUE) {
+      break;
+    }
+
+    DWORD error = GetLastError();
+    cc_log("lockfile_acquire: CreateFile %s: error code %lu",
+           lockfile.c_str(),
+           error);
+    if (error == ERROR_PATH_NOT_FOUND) {
+      // Directory doesn't exist?
+      if (Util::create_dir(Util::dir_name(lockfile)) == 0) {
+        // OK. Retry.
+        continue;
+      }
+    }
+
+    // ERROR_SHARING_VIOLATION: lock already held.
+    // ERROR_ACCESS_DENIED: maybe pending delete.
+    if (error != ERROR_SHARING_VIOLATION && error != ERROR_ACCESS_DENIED) {
+      // Fatal error, give up.
+      break;
+    }
+
+    if (slept > staleness_limit) {
+      cc_log("lockfile_acquire: gave up acquiring %s", lockfile.c_str());
+      break;
+    }
+
+    cc_log("lockfile_acquire: failed to acquire %s; sleeping %u microseconds",
+           lockfile.c_str(),
+           to_sleep);
+    usleep(to_sleep);
+    slept += to_sleep;
+    to_sleep = std::min(max_to_sleep, 2 * to_sleep);
+  }
+
+  return handle;
+}
+
+#endif // !_WIN32
+
+} // namespace
+
+Lockfile::Lockfile(const std::string& path, uint32_t staleness_limit)
+  : m_lockfile(path + ".lock")
+{
+#ifndef _WIN32
+  m_acquired = do_acquire_posix(m_lockfile, staleness_limit);
+#else
+  m_handle = do_acquire_win32(m_lockfile, staleness_limit);
+#endif
+  if (acquired()) {
+    cc_log("Acquired lock %s", m_lockfile.c_str());
+  } else {
+    cc_log("Failed to acquire lock %s", m_lockfile.c_str());
+  }
+}
+
+Lockfile::~Lockfile()
+{
+  if (acquired()) {
+    cc_log("Releasing lock %s", m_lockfile.c_str());
+#ifndef _WIN32
+    if (tmp_unlink(m_lockfile.c_str()) != 0) {
+      cc_log("Failed to unlink %s: %s", m_lockfile.c_str(), strerror(errno));
+    }
+#else
+    CloseHandle(m_handle);
+#endif
+  }
+}
diff --git a/src/Lockfile.hpp b/src/Lockfile.hpp
new file mode 100644 (file)
index 0000000..78baed8
--- /dev/null
@@ -0,0 +1,55 @@
+// Copyright (C) 2020 Joel Rosdahl and other contributors
+//
+// See doc/AUTHORS.adoc for a complete list of contributors.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program; if not, write to the Free Software Foundation, Inc., 51
+// Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+#pragma once
+
+#include "system.hpp"
+
+#include <string>
+
+class Lockfile
+{
+public:
+  // Acquire a lock on `path`. Break the lock (or give up, depending on
+  // implementation) after `staleness_limit` Microseconds.
+  Lockfile(const std::string& path, uint32_t staleness_limit = 2000000);
+
+  // Release the lock if acquired.
+  ~Lockfile();
+
+  // Return whether the lockfile was acquired successfully.
+  bool acquired() const;
+
+private:
+  std::string m_lockfile;
+#ifndef _WIN32
+  bool m_acquired = false;
+#else
+  HANDLE m_handle = nullptr;
+#endif
+};
+
+inline bool
+Lockfile::acquired() const
+{
+#ifndef _WIN32
+  return m_acquired;
+#else
+  return m_handle != INVALID_HANDLE_VALUE;
+#endif
+}
index 2008445c005da022274e1f99fe9586efa139f0b5..72bd45642a27a8e13b237c331f2a2b5ebd33bf0c 100644 (file)
@@ -25,9 +25,9 @@
 #include "AtomicFile.hpp"
 #include "Context.hpp"
 #include "Counters.hpp"
+#include "Lockfile.hpp"
 #include "cleanup.hpp"
 #include "hashutil.hpp"
-#include "lockfile.hpp"
 #include "logging.hpp"
 
 #include "third_party/fmt/core.h"
@@ -41,9 +41,6 @@
 #include <sys/types.h>
 #include <unistd.h>
 
-// How long (in microseconds) to wait before breaking a stale lock.
-constexpr unsigned k_lock_staleness_limit = 2000000;
-
 #define FLAG_NOZERO 1 // don't zero with the -z option
 #define FLAG_ALWAYS 2 // always show, even if zero
 #define FLAG_NEVER 4  // never show
@@ -349,17 +346,20 @@ stats_flush_to_file(const Config& config,
       "{}/{:x}/stats", config.cache_dir(), hash_from_int(getpid()) % 16);
   }
 
-  if (!lockfile_acquire(sfile.c_str(), k_lock_staleness_limit)) {
-    return;
-  }
-
   Counters counters;
-  stats_read(sfile, counters);
-  for (int i = 0; i < STATS_END; ++i) {
-    counters[i] += updates[i];
+
+  {
+    Lockfile lock(sfile);
+    if (!lock.acquired()) {
+      return;
+    }
+
+    stats_read(sfile, counters);
+    for (int i = 0; i < STATS_END; ++i) {
+      counters[i] += updates[i];
+    }
+    stats_write(sfile, counters);
   }
-  stats_write(sfile, counters);
-  lockfile_release(sfile.c_str());
 
   std::string subdir(Util::dir_name(sfile));
   bool need_cleanup = false;
@@ -499,7 +499,8 @@ stats_zero(const Config& config)
       free(fname);
       continue;
     }
-    if (lockfile_acquire(fname, k_lock_staleness_limit)) {
+    Lockfile lock(fname);
+    if (lock.acquired()) {
       stats_read(fname, counters);
       for (unsigned i = 0; stats_info[i].message; i++) {
         if (!(stats_info[i].flags & FLAG_NOZERO)) {
@@ -508,7 +509,6 @@ stats_zero(const Config& config)
       }
       counters[STATS_ZEROTIMESTAMP] = timestamp;
       stats_write(fname, counters);
-      lockfile_release(fname);
     }
     free(fname);
   }
@@ -534,12 +534,12 @@ stats_set_sizes(const char* dir, unsigned num_files, uint64_t total_size)
 {
   Counters counters;
   char* statsfile = format("%s/stats", dir);
-  if (lockfile_acquire(statsfile, k_lock_staleness_limit)) {
+  Lockfile lock(statsfile);
+  if (lock.acquired()) {
     stats_read(statsfile, counters);
     counters[STATS_NUMFILES] = num_files;
     counters[STATS_TOTALSIZE] = total_size / 1024;
     stats_write(statsfile, counters);
-    lockfile_release(statsfile);
   }
   free(statsfile);
 }
@@ -550,11 +550,11 @@ stats_add_cleanup(const char* dir, unsigned count)
 {
   Counters counters;
   char* statsfile = format("%s/stats", dir);
-  if (lockfile_acquire(statsfile, k_lock_staleness_limit)) {
+  Lockfile lock(statsfile);
+  if (lock.acquired()) {
     stats_read(statsfile, counters);
     counters[STATS_NUMCLEANUPS] += count;
     stats_write(statsfile, counters);
-    lockfile_release(statsfile);
   }
   free(statsfile);
 }
index eb16c7625c8e8d18cbdff4082e41921930df066c..d6ddb2d652cf59348c9f5ca5f6ead60fb492085f 100644 (file)
@@ -28,7 +28,6 @@ unsigned suite_conf(unsigned);
 unsigned suite_hash(unsigned);
 unsigned suite_hashutil(unsigned);
 unsigned suite_legacy_util(unsigned);
-unsigned suite_lockfile(unsigned);
 
 const suite_fn k_legacy_suites[] = {
   &suite_args,
@@ -37,7 +36,6 @@ const suite_fn k_legacy_suites[] = {
   &suite_hash,
   &suite_hashutil,
   &suite_legacy_util,
-  &suite_lockfile,
   nullptr,
 };
 
similarity index 57%
rename from src/lockfile.hpp
rename to unittest/test_Lockfile.cpp
index 275c7a0c023a4afc01d2253b8590691dd3717278..cbd61b789327d244f0e7be2b79ce9dfa211bc9b1 100644 (file)
 // this program; if not, write to the Free Software Foundation, Inc., 51
 // Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
-#pragma once
+// This file contains tests for functions in lockfile.c.
 
-bool lockfile_acquire(const char* path, unsigned staleness_limit);
-void lockfile_release(const char* path);
+#include "../src/Lockfile.hpp"
+#include "../src/Stat.hpp"
+
+#include "third_party/catch.hpp"
+
+TEST_CASE("Lockfile acquire and release")
+{
+  {
+    Lockfile lock("test", 1000);
+    CHECK(lock.acquired());
+    auto st = Stat::lstat("test.lock");
+    CHECK(st);
+#ifndef _WIN32
+    CHECK(st.is_symlink());
+#else
+    CHECK(st.is_regular());
+#endif
+  }
+
+  CHECK(!Stat::lstat("test.lock"));
+}
+
+#ifndef _WIN32
+TEST_CASE("Lockfile breaking")
+{
+  CHECK(symlink("foo", "test.lock") == 0);
+
+  Lockfile lock("test", 1000);
+  CHECK(lock.acquired());
+}
+#endif // !_WIN32