about summary refs log tree commit diff
path: root/third_party/nix/src/libstore/pathlocks.cc
#include "libstore/pathlocks.hh"

#include <cerrno>
#include <cstdlib>

#include <fcntl.h>
#include <glog/logging.h>
#include <sys/file.h>
#include <sys/stat.h>
#include <sys/types.h>

#include "libutil/sync.hh"
#include "libutil/util.hh"

namespace nix {

AutoCloseFD openLockFile(const Path& path, bool create) {
  AutoCloseFD fd;

  fd = open(path.c_str(), O_CLOEXEC | O_RDWR | (create ? O_CREAT : 0), 0600);
  if (!fd && (create || errno != ENOENT)) {
    throw SysError(format("opening lock file '%1%'") % path);
  }

  return fd;
}

void deleteLockFile(const Path& path, int fd) {
  /* Get rid of the lock file.  Have to be careful not to introduce
     races.  Write a (meaningless) token to the file to indicate to
     other processes waiting on this lock that the lock is stale
     (deleted). */
  unlink(path.c_str());
  writeFull(fd, "d");
  /* Note that the result of unlink() is ignored; removing the lock
     file is an optimisation, not a necessity. */
}

bool lockFile(int fd, LockType lockType, bool wait) {
  int type = 0;
  if (lockType == ltRead) {
    type = LOCK_SH;
  } else if (lockType == ltWrite) {
    type = LOCK_EX;
  } else if (lockType == ltNone) {
    type = LOCK_UN;
  } else {
    abort();
  }

  if (wait) {
    while (flock(fd, type) != 0) {
      checkInterrupt();
      if (errno != EINTR) {
        throw SysError(format("acquiring/releasing lock"));
      }
      return false;
    }
  } else {
    while (flock(fd, type | LOCK_NB) != 0) {
      checkInterrupt();
      if (errno == EWOULDBLOCK) {
        return false;
      }
      if (errno != EINTR) {
        throw SysError(format("acquiring/releasing lock"));
      }
    }
  }

  return true;
}

PathLocks::PathLocks() : deletePaths(false) {}

PathLocks::PathLocks(const PathSet& paths, const std::string& waitMsg)
    : deletePaths(false) {
  lockPaths(paths, waitMsg);
}

bool PathLocks::lockPaths(const PathSet& paths, const std::string& waitMsg,
                          bool wait) {
  assert(fds.empty());

  /* Note that `fds' is built incrementally so that the destructor
     will only release those locks that we have already acquired. */

  /* Acquire the lock for each path in sorted order. This ensures
     that locks are always acquired in the same order, thus
     preventing deadlocks. */
  for (auto& path : paths) {
    checkInterrupt();
    Path lockPath = path + ".lock";

    DLOG(INFO) << "locking path '" << path << "'";

    AutoCloseFD fd;

    while (true) {
      /* Open/create the lock file. */
      fd = openLockFile(lockPath, true);

      /* Acquire an exclusive lock. */
      if (!lockFile(fd.get(), ltWrite, false)) {
        if (wait) {
          if (!waitMsg.empty()) {
            LOG(WARNING) << waitMsg;
          }
          lockFile(fd.get(), ltWrite, true);
        } else {
          /* Failed to lock this path; release all other
             locks. */
          unlock();
          return false;
        }
      }

      DLOG(INFO) << "lock acquired on '" << lockPath << "'";

      /* Check that the lock file hasn't become stale (i.e.,
         hasn't been unlinked). */
      struct stat st {};
      if (fstat(fd.get(), &st) == -1) {
        throw SysError(format("statting lock file '%1%'") % lockPath);
      }
      if (st.st_size != 0) {
        /* This lock file has been unlinked, so we're holding
           a lock on a deleted file.  This means that other
           processes may create and acquire a lock on
           `lockPath', and proceed.  So we must retry. */
        DLOG(INFO) << "open lock file '" << lockPath << "' has become stale";
      } else {
        break;
      }
    }

    /* Use borrow so that the descriptor isn't closed. */
    fds.emplace_back(fd.release(), lockPath);
  }

  return true;
}

PathLocks::~PathLocks() {
  try {
    unlock();
  } catch (...) {
    ignoreException();
  }
}

void PathLocks::unlock() {
  for (auto& i : fds) {
    if (deletePaths) {
      deleteLockFile(i.second, i.first);
    }

    if (close(i.first) == -1) {
      LOG(WARNING) << "cannot close lock file on '" << i.second << "'";
    }

    DLOG(INFO) << "lock released on '" << i.second << "'";
  }

  fds.clear();
}

void PathLocks::setDeletion(bool deletePaths) {
  this->deletePaths = deletePaths;
}

}  // namespace nix