]> git.ipfire.org Git - thirdparty/ccache.git/commitdiff
enhance: Improve lock file implementation
authorJoel Rosdahl <joel@rosdahl.net>
Tue, 5 Jul 2022 18:04:25 +0000 (20:04 +0200)
committerJoel Rosdahl <joel@rosdahl.net>
Tue, 5 Jul 2022 18:04:25 +0000 (20:04 +0200)
This commit improves brings two improvements to the lock file
implementation:

1. Support for long-lived locks.
2. Support for non-blocking lock acquisition.

There are now two types of lock file classes: ShortLivedLockFile and
LongLivedLockFile. On Windows the implementations are identical. On
other systems, LongLivedLockFile creates a separate "alive file" whose
mtime will be updated regularly by a helper thread until the lock is
released. This makes it possible for another process to wait for the
lock indefinitely but also to know then a lock is stale and can be
broken. The ShortLivedLockFile class works like the lock file class used
to work before: it considers a lock stale after waiting for two seconds
and noticing that the symlink target has not changed during that time.
ShortLivedLockFile is to be used when the lock is expected to be held
for a very short time so that it's a waste of resources to start a
helper thread to keep the lock alive.

On some systems it would be possible to update mtime of the symlink
itself instead of a separate file, but that does not seem to be portable
enough.

Also worth mentioning is that the reason to not use proper fcntl/flock
locks on non-Windows systems is to continue supporting a cache directory
on NFS since file locks on NFS don't have a good track record.

13 files changed:
src/CMakeLists.txt
src/Lockfile.cpp [deleted file]
src/Lockfile.hpp [deleted file]
src/ccache.cpp
src/storage/primary/StatsFile.cpp
src/test_lockfile.cpp
src/util/CMakeLists.txt
src/util/LockFile.cpp [new file with mode: 0644]
src/util/LockFile.hpp [new file with mode: 0644]
src/util/file.cpp
unittest/CMakeLists.txt
unittest/test_Lockfile.cpp [deleted file]
unittest/test_util_LockFile.cpp [new file with mode: 0644]

index c30fa8a92cf6d05a0733e0313dbd754f14b6ced1..000c3182af66a04cfabad179f21067d1261086eb 100644 (file)
@@ -7,7 +7,6 @@ set(
   Depfile.cpp
   Fd.cpp
   Hash.cpp
-  Lockfile.cpp
   Logging.cpp
   ProgressBar.cpp
   Result.cpp
diff --git a/src/Lockfile.cpp b/src/Lockfile.cpp
deleted file mode 100644 (file)
index 85fdd1b..0000000
+++ /dev/null
@@ -1,239 +0,0 @@
-// Copyright (C) 2020-2021 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 "Logging.hpp"
-#include "Util.hpp"
-#include "Win32Util.hpp"
-#include "fmtmacros.hpp"
-
-#include <core/wincompat.hpp>
-
-#include "third_party/fmt/core.h"
-
-#include <thread>
-
-#ifdef HAVE_UNISTD_H
-#  include <unistd.h>
-#endif
-
-#include <algorithm>
-#include <sstream>
-#include <thread>
-
-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;
-
-  std::stringstream ss;
-  ss << Util::get_hostname() << ':' << getpid() << ':'
-     << std::this_thread::get_id();
-  const auto content_prefix = ss.str();
-
-  while (true) {
-    auto my_content = FMT("{}:{}", content_prefix, time(nullptr));
-
-    if (symlink(my_content.c_str(), lockfile.c_str()) == 0) {
-      // We got the lock.
-      return true;
-    }
-
-    int saved_errno = errno;
-    LOG("lockfile_acquire: symlink {}: {}", lockfile, 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 {
-        LOG("lockfile_acquire: readlink {}: {}", lockfile, strerror(errno));
-        return false;
-      }
-    }
-
-    if (content == my_content) {
-      // Lost NFS reply?
-      LOG("lockfile_acquire: symlink {} failed but we got the lock anyway",
-          lockfile);
-      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.
-    LOG("lockfile_acquire: lock info for {}: {}", lockfile, content);
-
-    if (initial_content.empty()) {
-      initial_content = content;
-    }
-
-    if (slept <= staleness_limit) {
-      LOG("lockfile_acquire: failed to acquire {}; sleeping {} microseconds",
-          lockfile,
-          to_sleep);
-      std::this_thread::sleep_for(std::chrono::microseconds(to_sleep));
-      slept += to_sleep;
-      to_sleep = std::min(max_to_sleep, 2 * to_sleep);
-    } else if (content != initial_content) {
-      LOG("lockfile_acquire: gave up acquiring {}", lockfile);
-      return false;
-    } else {
-      // The lock seems to be stale -- break it and try again.
-      LOG("lockfile_acquire: breaking {}", lockfile);
-      if (!Util::unlink_tmp(lockfile)) {
-        LOG("Failed to unlink {}: {}", lockfile, 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)
-{
-  const uint32_t max_to_sleep = 10000; // Microseconds.
-  uint32_t to_sleep = 1000;            // Microseconds.
-  uint32_t 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)
-                        nullptr,       // security attributes
-                        CREATE_ALWAYS, // creation disposition
-                        flags,         // flags and attributes
-                        nullptr        // template file
-    );
-    if (handle != INVALID_HANDLE_VALUE) {
-      break;
-    }
-
-    DWORD error = GetLastError();
-    if (error == ERROR_PATH_NOT_FOUND) {
-      // Directory doesn't exist?
-      if (Util::create_dir(Util::dir_name(lockfile))) {
-        // OK. Retry.
-        continue;
-      }
-    }
-
-    LOG("lockfile_acquire: CreateFile {}: {} ({})",
-        lockfile,
-        Win32Util::error_message(error),
-        error);
-
-    // 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) {
-      LOG("lockfile_acquire: gave up acquiring {}", lockfile);
-      break;
-    }
-
-    LOG("lockfile_acquire: failed to acquire {}; sleeping {} microseconds",
-        lockfile,
-        to_sleep);
-    std::this_thread::sleep_for(std::chrono::microseconds(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()) {
-    LOG("Acquired lock {}", m_lockfile);
-  } else {
-    LOG("Failed to acquire lock {}", m_lockfile);
-  }
-}
-
-Lockfile::~Lockfile()
-{
-  if (acquired()) {
-    LOG("Releasing lock {}", m_lockfile);
-#ifndef _WIN32
-    if (!Util::unlink_tmp(m_lockfile)) {
-      LOG("Failed to unlink {}: {}", m_lockfile, strerror(errno));
-    }
-#else
-    CloseHandle(m_handle);
-#endif
-  }
-}
-
-bool
-Lockfile::acquired() const
-{
-#ifndef _WIN32
-  return m_acquired;
-#else
-  return m_handle != INVALID_HANDLE_VALUE;
-#endif
-}
diff --git a/src/Lockfile.hpp b/src/Lockfile.hpp
deleted file mode 100644 (file)
index 309c0c4..0000000
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright (C) 2020-2021 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 <cstdint>
-#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
-  void* m_handle = nullptr;
-#endif
-};
index b209c3a2a4ca0c75e97a908b2b6bfb2be0521114..53f59d3614bd0ddeffecab6cfa64d49b1f568283 100644 (file)
@@ -27,7 +27,6 @@
 #include "File.hpp"
 #include "Finalizer.hpp"
 #include "Hash.hpp"
-#include "Lockfile.hpp"
 #include "Logging.hpp"
 #include "MiniTrace.hpp"
 #include "Result.hpp"
index 418e02f105d004d8e0f342f845f795d56b5eac91..7e9503ceb6132d0aeb022482f190a6ca8a506c29 100644 (file)
 #include "StatsFile.hpp"
 
 #include <AtomicFile.hpp>
-#include <Lockfile.hpp>
 #include <Logging.hpp>
 #include <Util.hpp>
 #include <core/exceptions.hpp>
 #include <fmtmacros.hpp>
+#include <util/LockFile.hpp>
 
 namespace storage::primary {
 
@@ -64,7 +64,8 @@ std::optional<core::StatisticsCounters>
 StatsFile::update(
   std::function<void(core::StatisticsCounters& counters)> function) const
 {
-  Lockfile lock(m_path);
+  util::ShortLivedLockFile lock_file(m_path);
+  util::LockFileGuard lock(lock_file);
   if (!lock.acquired()) {
     LOG("Failed to acquire lock for {}", m_path);
     return std::nullopt;
index 25f5dc2f1cd70a89a8849ec43057a6c45dfef09d..661b187c7ef10eb4818b314b134c844be0622abd 100644 (file)
 // Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
 #include <Config.hpp>
-#include <Lockfile.hpp>
 #include <Logging.hpp>
 #include <fmtmacros.hpp>
+#include <util/LockFile.hpp>
 #include <util/string.hpp>
 
+#include <memory>
 #include <string>
 #include <thread>
 
 int
 main(int argc, char** argv)
 {
-  if (argc != 3) {
-    PRINT_RAW(stderr, "Usage: test-lockfile PATH SECONDS\n");
+  if (argc != 5) {
+    PRINT_RAW(stderr,
+              "Usage: test-lockfile PATH SECONDS <short|long>"
+              " <blocking|non-blocking>\n");
     return 1;
   }
   Config config;
   config.update_from_environment();
   Logging::init(config);
 
-  std::string path(argv[1]);
-  auto seconds = util::parse_signed(argv[2]);
+  const std::string path(argv[1]);
+  const auto seconds = util::parse_signed(argv[2]);
+  const bool long_lived = std::string(argv[3]) == "long";
+  const bool blocking = std::string(argv[4]) == "blocking";
   if (!seconds) {
     PRINT_RAW(stderr, "Error: Failed to parse seconds\n");
     return 1;
   }
 
-  PRINT_RAW(stdout, "Acquiring\n");
+  std::unique_ptr<util::LockFile> lock_file;
+  lock_file = long_lived ? std::unique_ptr<
+                util::LockFile>{std::make_unique<util::LongLivedLockFile>(path)}
+                         : std::unique_ptr<util::LockFile>{
+                           std::make_unique<util::ShortLivedLockFile>(path)};
+  const auto mode = blocking ? util::LockFileGuard::Mode::blocking
+                             : util::LockFileGuard::Mode::non_blocking;
+
+  PRINT(stdout, "{}\n", blocking ? "Acquiring" : "Trying to acquire");
+  bool acquired = false;
   {
-    Lockfile lock(path);
-    if (lock.acquired()) {
+    util::LockFileGuard lock(*lock_file, mode);
+    acquired = lock.acquired();
+    if (acquired) {
       PRINT_RAW(stdout, "Acquired\n");
       PRINT(
         stdout, "Sleeping {} second{}\n", *seconds, *seconds == 1 ? "" : "s");
       std::this_thread::sleep_for(std::chrono::seconds{*seconds});
     } else {
-      PRINT_RAW(stdout, "Failed to acquire\n");
+      PRINT(stdout, "{} acquire\n", blocking ? "Failed to" : "Did not");
+    }
+    if (acquired) {
+      PRINT_RAW(stdout, "Releasing\n");
     }
-    PRINT_RAW(stdout, "Releasing\n");
   }
-  PRINT_RAW(stdout, "Released\n");
+  if (acquired) {
+    PRINT_RAW(stdout, "Released\n");
+  }
 }
index 0685ad992974b280acc58fc60f2dfa2b922b0ed4..49593f107eda13e69d01df1d2a54841faa9aa0bc 100644 (file)
@@ -1,5 +1,6 @@
 set(
   sources
+  ${CMAKE_CURRENT_SOURCE_DIR}/LockFile.cpp
   ${CMAKE_CURRENT_SOURCE_DIR}/TextTable.cpp
   ${CMAKE_CURRENT_SOURCE_DIR}/Tokenizer.cpp
   ${CMAKE_CURRENT_SOURCE_DIR}/file.cpp
diff --git a/src/util/LockFile.cpp b/src/util/LockFile.cpp
new file mode 100644 (file)
index 0000000..8d4034c
--- /dev/null
@@ -0,0 +1,441 @@
+// Copyright (C) 2020-2022 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 "Logging.hpp"
+#include "Util.hpp"
+#include "Win32Util.hpp"
+#include "fmtmacros.hpp"
+
+#include <core/exceptions.hpp>
+#include <core/wincompat.hpp>
+#include <util/file.hpp>
+
+#include "third_party/fmt/core.h"
+
+#ifdef HAVE_SYS_TIME_H
+#  include <sys/time.h>
+#endif
+#ifdef HAVE_UNISTD_H
+#  include <unistd.h>
+#endif
+
+#include <algorithm>
+#include <random>
+#include <sstream>
+
+// Seconds.
+const double k_min_sleep_time = 0.010;
+const double k_max_sleep_time = 0.050;
+const double k_staleness_limit = 2;
+const double k_keep_alive_interval = k_staleness_limit / 4;
+const auto k_keep_alive_interval_ms = std::chrono::milliseconds{
+  static_cast<uint64_t>(k_keep_alive_interval * 1000)};
+
+namespace {
+
+class RandomNumberGenerator
+{
+public:
+  RandomNumberGenerator(int32_t min, int32_t max)
+    : m_random_engine(m_random_device()),
+      m_distribution(min, max)
+  {
+  }
+
+  int32_t
+  get()
+  {
+    return m_distribution(m_random_engine);
+  };
+
+private:
+  std::random_device m_random_device;
+  std::default_random_engine m_random_engine;
+  std::uniform_int_distribution<int32_t> m_distribution;
+};
+
+} // namespace
+
+namespace util {
+
+LockFile::LockFile(const std::string& path)
+  : m_lock_file(path + ".lock"),
+#ifndef _WIN32
+    m_acquired(false)
+#else
+    m_handle(INVALID_HANDLE_VALUE)
+#endif
+{
+}
+
+bool
+LockFile::acquire()
+{
+  LOG("Acquiring {}", m_lock_file);
+  return acquire(true);
+}
+
+bool
+LockFile::try_acquire()
+{
+  LOG("Trying to acquire {}", m_lock_file);
+  return acquire(false);
+}
+
+void
+LockFile::release()
+{
+  if (!acquired()) {
+    return;
+  }
+
+  LOG("Releasing {}", m_lock_file);
+#ifndef _WIN32
+  on_before_release();
+  Util::unlink_tmp(m_lock_file);
+#else
+  CloseHandle(m_handle);
+#endif
+  LOG("Released {}", m_lock_file);
+#ifndef _WIN32
+  m_acquired = false;
+#else
+  m_handle = INVALID_HANDLE_VALUE;
+#endif
+}
+
+bool
+LockFile::acquired() const
+{
+#ifndef _WIN32
+  return m_acquired;
+#else
+  return m_handle != INVALID_HANDLE_VALUE;
+#endif
+}
+
+bool
+LockFile::acquire(const bool blocking)
+{
+  ASSERT(!acquired());
+
+#ifndef _WIN32
+  m_acquired = do_acquire(blocking);
+#else
+  m_handle = do_acquire(blocking);
+#endif
+  if (acquired()) {
+    LOG("Acquired {}", m_lock_file);
+    on_after_acquire();
+  } else {
+    LOG("Failed to acquire lock {}", m_lock_file);
+  }
+
+  return acquired();
+}
+
+#ifndef _WIN32
+
+static double
+time_from_clock()
+{
+  timeval tv;
+  gettimeofday(&tv, nullptr);
+  return tv.tv_sec + static_cast<double>(tv.tv_usec) / 1'000'000;
+}
+
+static double
+time_from_stat(const Stat& stat)
+{
+  const auto mtime = stat.mtim();
+  return mtime.tv_sec + static_cast<double>(mtime.tv_nsec) / 1'000'000'000;
+}
+
+bool
+LockFile::do_acquire(const bool blocking)
+{
+  std::stringstream ss;
+  ss << Util::get_hostname() << '-' << getpid() << '-'
+     << std::this_thread::get_id();
+  const auto content_prefix = ss.str();
+
+  double last_seen_activity = [this] {
+    const auto last_lock_update = get_last_lock_update();
+    return last_lock_update ? *last_lock_update : time_from_clock();
+  }();
+
+  std::string initial_content;
+  RandomNumberGenerator sleep_ms_generator(k_min_sleep_time * 1000,
+                                           k_max_sleep_time * 1000);
+
+  while (true) {
+    const auto my_content = FMT("{}-{}", content_prefix, time_from_clock());
+
+    if (symlink(my_content.c_str(), m_lock_file.c_str()) == 0) {
+      // We got the lock.
+      return true;
+    }
+
+    int saved_errno = errno;
+    LOG("Could not acquire {}: {}", m_lock_file, strerror(saved_errno));
+    if (saved_errno == ENOENT) {
+      // Directory doesn't exist?
+      if (Util::create_dir(Util::dir_name(m_lock_file))) {
+        // 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(m_lock_file);
+    if (content.empty()) {
+      if (errno == ENOENT) {
+        // The symlink was removed after the symlink() call above, so retry
+        // acquiring it.
+        continue;
+      } else {
+        LOG("Could not read symlink {}: {}", m_lock_file, strerror(errno));
+        return false;
+      }
+    }
+
+    if (content == my_content) {
+      // Lost NFS reply?
+      LOG("Symlinking {} failed but we got the lock anyway", m_lock_file);
+      return true;
+    }
+
+    LOG("Lock info for {}: {}", m_lock_file, content);
+
+    if (initial_content.empty()) {
+      initial_content = content;
+    }
+
+    const auto last_lock_update = get_last_lock_update();
+    if (last_lock_update) {
+      last_seen_activity = std::max(last_seen_activity, *last_lock_update);
+    }
+
+    const double inactive_duration = time_from_clock() - last_seen_activity;
+
+    if (inactive_duration < k_staleness_limit) {
+      LOG("Lock {} held by another process active {:.3f} seconds ago",
+          m_lock_file,
+          inactive_duration);
+      if (!blocking) {
+        return false;
+      }
+    } else if (content == initial_content) {
+      // The lock seems to be stale -- break it and try again.
+      LOG("Breaking {} since it has been inactive for {:.3f} seconds",
+          m_lock_file,
+          inactive_duration);
+      if (!on_before_break() || !Util::unlink_tmp(m_lock_file)) {
+        return false;
+      }
+
+      // Note: There is an inherent race condition here where two processes may
+      // believe they both acquired the lock after breaking it:
+      //
+      // 1. A decides to break the lock.
+      // 2. B decides to break the lock.
+      // 3. A removes the file and retries.
+      // 4. A acquires the lock.
+      // 5. B removes the file and retries.
+      // 6. B acquires the lock.
+      //
+      // To reduce the risk we sleep for a while before retrying so that it's
+      // likely that step 5 happens before step 4.
+    } else {
+      LOG("Lock {} reacquired by another process", m_lock_file);
+      if (!blocking) {
+        return false;
+      }
+      initial_content = content;
+    }
+
+    const std::chrono::milliseconds to_sleep{sleep_ms_generator.get()};
+    LOG("Sleeping {} ms", to_sleep.count());
+    std::this_thread::sleep_for(to_sleep);
+  }
+}
+
+#else // !_WIN32
+
+void*
+LockFile::do_acquire(const bool blocking)
+{
+  void* handle;
+  RandomNumberGenerator sleep_ms_generator(k_min_sleep_time * 1000,
+                                           k_max_sleep_time * 1000);
+
+  while (true) {
+    DWORD flags = FILE_ATTRIBUTE_NORMAL | FILE_FLAG_DELETE_ON_CLOSE;
+    handle = CreateFile(m_lock_file.c_str(),
+                        GENERIC_WRITE, // desired access
+                        0,             // shared mode (0 = not shared)
+                        nullptr,       // security attributes
+                        CREATE_ALWAYS, // creation disposition
+                        flags,         // flags and attributes
+                        nullptr        // template file
+    );
+    if (handle != INVALID_HANDLE_VALUE) {
+      break;
+    }
+
+    DWORD error = GetLastError();
+    if (error == ERROR_PATH_NOT_FOUND) {
+      // Directory doesn't exist?
+      if (Util::create_dir(Util::dir_name(m_lock_file))) {
+        // OK. Retry.
+        continue;
+      }
+    }
+
+    LOG("Could not acquire {}: {} ({})",
+        m_lock_file,
+        Win32Util::error_message(error),
+        error);
+
+    // 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;
+    }
+
+    LOG("Lock {} held by another process", m_lock_file);
+    if (!blocking) {
+      break;
+    }
+
+    const std::chrono::milliseconds to_sleep{sleep_ms_generator.get()};
+    LOG("Sleeping {} ms", to_sleep.count());
+    std::this_thread::sleep_for(to_sleep);
+  }
+
+  return handle;
+}
+
+#endif // !_WIN32
+
+ShortLivedLockFile::ShortLivedLockFile(const std::string& path) : LockFile(path)
+{
+}
+
+LongLivedLockFile::LongLivedLockFile(const std::string& path)
+  : LockFile(path)
+#ifndef _WIN32
+    ,
+    m_alive_file(path + ".alive")
+#endif
+{
+}
+
+#ifndef _WIN32
+
+void
+LongLivedLockFile::on_after_acquire()
+{
+  try {
+    Util::write_file(m_alive_file, "");
+  } catch (const core::Error& e) {
+    LOG("Failed to create {}: {}", m_alive_file, e.what());
+  }
+  LOG_RAW("Starting keep-alive thread");
+  m_keep_alive_thread = std::thread([=] {
+    while (true) {
+      std::unique_lock<std::mutex> lock(m_stop_keep_alive_mutex);
+      m_stop_keep_alive_condition.wait_for(
+        lock, k_keep_alive_interval_ms, [this] { return m_stop_keep_alive; });
+      if (m_stop_keep_alive) {
+        return;
+      }
+      util::set_timestamps(m_alive_file);
+    }
+  });
+  LOG_RAW("Started keep-alive thread");
+}
+
+void
+LongLivedLockFile::on_before_release()
+{
+  if (m_keep_alive_thread.joinable()) {
+    {
+      std::unique_lock<std::mutex> lock(m_stop_keep_alive_mutex);
+      m_stop_keep_alive = true;
+    }
+    m_stop_keep_alive_condition.notify_one();
+    m_keep_alive_thread.join();
+
+    Util::unlink_tmp(m_alive_file);
+  }
+}
+
+bool
+LongLivedLockFile::on_before_break()
+{
+  return Util::unlink_tmp(m_alive_file);
+}
+
+std::optional<double>
+LongLivedLockFile::get_last_lock_update()
+{
+  if (const auto stat = Stat::stat(m_alive_file); stat) {
+    return time_from_stat(stat);
+  } else {
+    return std::nullopt;
+  }
+}
+
+#endif
+
+LockFileGuard::LockFileGuard(LockFile& lock_file, Mode mode)
+  : m_lock_file(lock_file)
+{
+  if (mode == Mode::blocking) {
+    lock_file.acquire();
+  } else {
+    lock_file.try_acquire();
+  }
+}
+
+LockFileGuard::~LockFileGuard() noexcept
+{
+  m_lock_file.release();
+}
+
+bool
+LockFileGuard::acquired() const
+{
+  return m_lock_file.acquired();
+}
+
+} // namespace util
diff --git a/src/util/LockFile.hpp b/src/util/LockFile.hpp
new file mode 100644 (file)
index 0000000..1bf6808
--- /dev/null
@@ -0,0 +1,145 @@
+// Copyright (C) 2020-2022 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 <NonCopyable.hpp>
+
+#include <condition_variable>
+#include <cstdint>
+#include <optional>
+#include <string>
+#include <thread>
+
+namespace util {
+
+class LockFile : NonCopyable
+{
+public:
+  virtual ~LockFile() noexcept = default;
+
+  // Acquire lock, blocking. Returns true if acquired, otherwise false.
+  bool acquire();
+
+  // Acquire lock, non-blocking. Returns true if acquired, otherwise false.
+  bool try_acquire();
+
+  // Release lock. If not previously acquired, nothing happens.
+  void release();
+
+  // Return whether the lock was acquired successfully.
+  bool acquired() const;
+
+protected:
+  LockFile(const std::string& path);
+
+private:
+  std::string m_lock_file;
+#ifndef _WIN32
+  bool m_acquired;
+#else
+  void* m_handle;
+#endif
+
+  bool acquire(bool blocking);
+  virtual void on_after_acquire();
+  virtual void on_before_release();
+#ifndef _WIN32
+  bool do_acquire(bool blocking);
+  virtual bool on_before_break();
+  virtual std::optional<double> get_last_lock_update();
+#else
+  void* do_acquire(bool blocking);
+#endif
+};
+
+// A short-lived lock.
+//
+// The lock is expected to be released shortly after being acquired - if it is
+// held for more than two seconds it risks being considered stale by another
+// client.
+class ShortLivedLockFile : public LockFile
+{
+public:
+  ShortLivedLockFile(const std::string& path);
+};
+
+// A long-lived lock.
+//
+// The lock will (depending on implementation) be kept alive by a helper thread.
+class LongLivedLockFile : public LockFile
+{
+public:
+  LongLivedLockFile(const std::string& path);
+
+private:
+#ifndef _WIN32
+  std::string m_alive_file;
+  std::thread m_keep_alive_thread;
+  std::mutex m_stop_keep_alive_mutex;
+  bool m_stop_keep_alive = false;
+  std::condition_variable m_stop_keep_alive_condition;
+
+  void on_after_acquire() override;
+  void on_before_release() override;
+  bool on_before_break() override;
+  std::optional<double> get_last_lock_update() override;
+#endif
+};
+
+class LockFileGuard : NonCopyable
+{
+public:
+  enum class Mode { blocking, non_blocking };
+
+  LockFileGuard(LockFile& lock_file, Mode mode = Mode::blocking);
+  ~LockFileGuard() noexcept;
+
+  bool acquired() const;
+
+private:
+  LockFile& m_lock_file;
+};
+
+inline void
+LockFile::on_after_acquire()
+{
+}
+
+inline void
+LockFile::on_before_release()
+{
+}
+
+#ifndef _WIN32
+
+inline bool
+LockFile::on_before_break()
+{
+  return true;
+}
+
+inline std::optional<double>
+LockFile::get_last_lock_update()
+{
+  return std::nullopt;
+}
+
+#endif
+
+} // namespace util
index ef35a64e9e710806ded63c1f3edea9ef4ebb8b96..44ca5cbdabed17a9fee781c11dde4d877375da27 100644 (file)
@@ -86,8 +86,10 @@ set_timestamps(const std::string& path,
   if (mtime) {
     atime_mtime.actime = atime ? atime->tv_sec : mtime->tv_sec;
     atime_mtime.modtime = mtime->tv_sec;
+    utime(path.c_str(), &atime_mtime);
+  } else {
+    utime(path.c_str(), nullptr);
   }
-  utime(path.c_str(), &atime_mtime);
 #endif
 }
 
index bc205d2bfbab25dc48e0194e7608e991cac66289..237fead4ac365e4e812237162f41fe785a1dc7f5 100644 (file)
@@ -7,7 +7,6 @@ set(
   test_Config.cpp
   test_Depfile.cpp
   test_Hash.cpp
-  test_Lockfile.cpp
   test_NullCompression.cpp
   test_Stat.cpp
   test_Util.cpp
@@ -22,6 +21,7 @@ set(
   test_hashutil.cpp
   test_storage_primary_StatsFile.cpp
   test_storage_primary_util.cpp
+  test_util_LockFile.cpp
   test_util_TextTable.cpp
   test_util_Tokenizer.cpp
   test_util_XXH3_128.cpp
diff --git a/unittest/test_Lockfile.cpp b/unittest/test_Lockfile.cpp
deleted file mode 100644 (file)
index d95cb3d..0000000
+++ /dev/null
@@ -1,75 +0,0 @@
-// Copyright (C) 2020-2021 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 "../src/Lockfile.hpp"
-#include "../src/Stat.hpp"
-#include "TestUtil.hpp"
-
-#include <core/wincompat.hpp>
-
-#include "third_party/doctest.h"
-
-#ifdef HAVE_UNISTD_H
-#  include <unistd.h>
-#endif
-
-TEST_SUITE_BEGIN("LockFile");
-
-using TestUtil::TestContext;
-
-TEST_CASE("Lockfile acquire and release")
-{
-  TestContext test_context;
-
-  {
-    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"));
-}
-
-TEST_CASE("Lockfile creates missing directories")
-{
-  TestContext test_context;
-
-  Lockfile lock("a/b/c/test", 1000);
-  CHECK(lock.acquired());
-  CHECK(Stat::lstat("a/b/c/test.lock"));
-}
-
-#ifndef _WIN32
-TEST_CASE("Lockfile breaking")
-{
-  TestContext test_context;
-
-  CHECK(symlink("foo", "test.lock") == 0);
-
-  Lockfile lock("test", 1000);
-  CHECK(lock.acquired());
-}
-#endif // !_WIN32
-
-TEST_SUITE_END();
diff --git a/unittest/test_util_LockFile.cpp b/unittest/test_util_LockFile.cpp
new file mode 100644 (file)
index 0000000..96979ee
--- /dev/null
@@ -0,0 +1,163 @@
+// Copyright (C) 2020-2022 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 "../src/Stat.hpp"
+#include "TestUtil.hpp"
+
+#include <Util.hpp>
+#include <core/wincompat.hpp>
+#include <util/LockFile.hpp>
+#include <util/file.hpp>
+
+#include "third_party/doctest.h"
+
+#ifdef HAVE_UNISTD_H
+#  include <unistd.h>
+#endif
+
+using namespace std::chrono_literals;
+
+TEST_SUITE_BEGIN("LockFile");
+
+using TestUtil::TestContext;
+
+TEST_CASE("Acquire and release short-lived lock file")
+{
+  TestContext test_context;
+
+  util::ShortLivedLockFile lock_file("test");
+  {
+    CHECK(!lock_file.acquired());
+    CHECK(!Stat::lstat("test.lock"));
+    CHECK(!Stat::lstat("test.alive"));
+
+    util::LockFileGuard lock(lock_file);
+    CHECK(lock_file.acquired());
+    CHECK(lock.acquired());
+    CHECK(!Stat::lstat("test.alive"));
+    const auto st = Stat::lstat("test.lock");
+    CHECK(st);
+#ifndef _WIN32
+    CHECK(st.is_symlink());
+#else
+    CHECK(st.is_regular());
+#endif
+  }
+
+  CHECK(!lock_file.acquired());
+  CHECK(!Stat::lstat("test.lock"));
+  CHECK(!Stat::lstat("test.alive"));
+}
+
+TEST_CASE("Non-blocking short-lived lock")
+{
+  TestContext test_context;
+
+  util::ShortLivedLockFile lock_file_1("test");
+  CHECK(!lock_file_1.acquired());
+
+  util::ShortLivedLockFile lock_file_2("test");
+  CHECK(!lock_file_2.acquired());
+
+  CHECK(lock_file_1.try_acquire());
+  CHECK(lock_file_1.acquired());
+
+  CHECK(!lock_file_2.try_acquire());
+  CHECK(lock_file_1.acquired());
+  CHECK(!lock_file_2.acquired());
+
+  lock_file_2.release();
+  CHECK(lock_file_1.acquired());
+  CHECK(!lock_file_2.acquired());
+
+  lock_file_1.release();
+  CHECK(!lock_file_1.acquired());
+  CHECK(!lock_file_2.acquired());
+}
+
+TEST_CASE("Acquire and release long-lived lock file")
+{
+  TestContext test_context;
+
+  util::LongLivedLockFile lock_file("test");
+  {
+    CHECK(!lock_file.acquired());
+    CHECK(!Stat::lstat("test.lock"));
+    CHECK(!Stat::lstat("test.alive"));
+
+    util::LockFileGuard lock(lock_file);
+    CHECK(lock_file.acquired());
+    CHECK(lock.acquired());
+#ifndef _WIN32
+    CHECK(Stat::lstat("test.alive"));
+#endif
+    const auto st = Stat::lstat("test.lock");
+    CHECK(st);
+#ifndef _WIN32
+    CHECK(st.is_symlink());
+#else
+    CHECK(st.is_regular());
+#endif
+  }
+
+  CHECK(!lock_file.acquired());
+  CHECK(!Stat::lstat("test.lock"));
+  CHECK(!Stat::lstat("test.alive"));
+}
+
+TEST_CASE("LockFile creates missing directories")
+{
+  TestContext test_context;
+
+  util::ShortLivedLockFile lock_file("a/b/c/test");
+  util::LockFileGuard lock(lock_file);
+  CHECK(lock.acquired());
+  CHECK(Stat::lstat("a/b/c/test.lock"));
+}
+
+#ifndef _WIN32
+TEST_CASE("Break stale lock, blocking")
+{
+  TestContext test_context;
+
+  Util::write_file("test.alive", "");
+  const timespec long_time_ago{0, 0};
+  util::set_timestamps("test.alive", long_time_ago);
+  CHECK(symlink("foo", "test.lock") == 0);
+
+  util::LongLivedLockFile lock_file("test");
+  util::LockFileGuard lock(lock_file);
+  CHECK(lock.acquired());
+}
+
+TEST_CASE("Break stale lock, non-blocking")
+{
+  TestContext test_context;
+
+  Util::write_file("test.alive", "");
+  const timespec long_time_ago{0, 0};
+  util::set_timestamps("test.alive", long_time_ago);
+  CHECK(symlink("foo", "test.lock") == 0);
+
+  util::LongLivedLockFile lock_file("test");
+  util::LockFileGuard lock(lock_file, util::LockFileGuard::Mode::non_blocking);
+  CHECK(lock.acquired());
+}
+#endif // !_WIN32
+
+TEST_SUITE_END();