Wrap blocking syscalls used by LLVMCAS inside RetryAfterSignal so they are retried on EINTR instead of surfacing as error or silently dropping actions due to failed to acquire locks.
264 lines
8.6 KiB
C++
264 lines
8.6 KiB
C++
//===- OnDiskCommon.cpp ---------------------------------------------------===//
|
|
//
|
|
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
|
|
// See https://llvm.org/LICENSE.txt for license information.
|
|
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
#include "OnDiskCommon.h"
|
|
#include "llvm/Support/Errno.h"
|
|
#include "llvm/Support/Error.h"
|
|
#include "llvm/Support/FileSystem.h"
|
|
#include "llvm/Support/Path.h"
|
|
#include "llvm/Support/Process.h"
|
|
#include <mutex>
|
|
#include <thread>
|
|
|
|
#if __has_include(<sys/file.h>)
|
|
#include <sys/file.h>
|
|
#ifdef LOCK_SH
|
|
#define HAVE_FLOCK 1
|
|
#else
|
|
#define HAVE_FLOCK 0
|
|
#endif
|
|
#endif
|
|
|
|
#if __has_include(<fcntl.h>)
|
|
#include <fcntl.h>
|
|
#endif
|
|
|
|
#if __has_include(<sys/mount.h>)
|
|
#include <sys/mount.h> // statfs
|
|
#endif
|
|
|
|
#ifdef __APPLE__
|
|
#if __has_include(<sys/sysctl.h>)
|
|
#include <sys/sysctl.h>
|
|
#endif
|
|
#endif
|
|
|
|
using namespace llvm;
|
|
|
|
static uint64_t OnDiskCASMaxMappingSize = 0;
|
|
|
|
Expected<std::optional<uint64_t>> cas::ondisk::getOverriddenMaxMappingSize() {
|
|
static std::once_flag Flag;
|
|
Error Err = Error::success();
|
|
std::call_once(Flag, [&Err] {
|
|
ErrorAsOutParameter EAO(&Err);
|
|
constexpr const char *EnvVar = "LLVM_CAS_MAX_MAPPING_SIZE";
|
|
auto Value = sys::Process::GetEnv(EnvVar);
|
|
if (!Value)
|
|
return;
|
|
|
|
uint64_t Size;
|
|
if (StringRef(*Value).getAsInteger(/*auto*/ 0, Size))
|
|
Err = createStringError(inconvertibleErrorCode(),
|
|
"invalid value for %s: expected integer", EnvVar);
|
|
OnDiskCASMaxMappingSize = Size;
|
|
});
|
|
|
|
if (Err)
|
|
return std::move(Err);
|
|
|
|
if (OnDiskCASMaxMappingSize == 0)
|
|
return std::nullopt;
|
|
|
|
return OnDiskCASMaxMappingSize;
|
|
}
|
|
|
|
void cas::ondisk::setMaxMappingSize(uint64_t Size) {
|
|
OnDiskCASMaxMappingSize = Size;
|
|
}
|
|
|
|
std::error_code cas::ondisk::lockFileThreadSafe(int FD,
|
|
sys::fs::LockKind Kind) {
|
|
#if HAVE_FLOCK
|
|
if (sys::RetryAfterSignal(
|
|
-1, flock, FD,
|
|
Kind == sys::fs::LockKind::Exclusive ? LOCK_EX : LOCK_SH) == 0)
|
|
return std::error_code();
|
|
return std::error_code(errno, std::generic_category());
|
|
#elif defined(_WIN32)
|
|
// On Windows this implementation is thread-safe.
|
|
return sys::fs::lockFile(FD, Kind);
|
|
#else
|
|
return make_error_code(std::errc::no_lock_available);
|
|
#endif
|
|
}
|
|
|
|
std::error_code cas::ondisk::unlockFileThreadSafe(int FD) {
|
|
#if HAVE_FLOCK
|
|
if (sys::RetryAfterSignal(-1, flock, FD, LOCK_UN) == 0)
|
|
return std::error_code();
|
|
return std::error_code(errno, std::generic_category());
|
|
#elif defined(_WIN32)
|
|
// On Windows this implementation is thread-safe.
|
|
return sys::fs::unlockFile(FD);
|
|
#else
|
|
return make_error_code(std::errc::no_lock_available);
|
|
#endif
|
|
}
|
|
|
|
std::error_code
|
|
cas::ondisk::tryLockFileThreadSafe(int FD, std::chrono::milliseconds Timeout,
|
|
sys::fs::LockKind Kind) {
|
|
#if HAVE_FLOCK
|
|
auto Start = std::chrono::steady_clock::now();
|
|
auto End = Start + Timeout;
|
|
do {
|
|
if (sys::RetryAfterSignal(
|
|
-1, flock, FD,
|
|
(Kind == sys::fs::LockKind::Exclusive ? LOCK_EX : LOCK_SH) |
|
|
LOCK_NB) == 0)
|
|
return std::error_code();
|
|
int Error = errno;
|
|
if (Error == EWOULDBLOCK) {
|
|
if (Timeout.count() == 0)
|
|
break;
|
|
// Match sys::fs::tryLockFile, which sleeps for 1 ms per attempt.
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
|
continue;
|
|
}
|
|
return std::error_code(Error, std::generic_category());
|
|
} while (std::chrono::steady_clock::now() < End);
|
|
return make_error_code(std::errc::no_lock_available);
|
|
#elif defined(_WIN32)
|
|
// On Windows this implementation is thread-safe.
|
|
return sys::fs::tryLockFile(FD, Timeout, Kind);
|
|
#else
|
|
return make_error_code(std::errc::no_lock_available);
|
|
#endif
|
|
}
|
|
|
|
Expected<size_t> cas::ondisk::preallocateFileTail(int FD, size_t CurrentSize,
|
|
size_t NewSize) {
|
|
auto CreateError = [&](std::error_code EC) -> Expected<size_t> {
|
|
if (EC == std::errc::not_supported)
|
|
// Ignore ENOTSUP in case the filesystem cannot preallocate.
|
|
return NewSize;
|
|
#if defined(HAVE_POSIX_FALLOCATE)
|
|
if (EC == std::errc::invalid_argument && CurrentSize < NewSize && // len > 0
|
|
NewSize < std::numeric_limits<off_t>::max()) // 0 <= offset, len < max
|
|
// Prior to 2024, POSIX required EINVAL for cases that should be ENOTSUP,
|
|
// so handle it the same as above if it is not one of the other ways to
|
|
// get EINVAL.
|
|
return NewSize;
|
|
#endif
|
|
return createStringError(EC,
|
|
"failed to allocate to CAS file: " + EC.message());
|
|
};
|
|
#if defined(HAVE_POSIX_FALLOCATE)
|
|
// Note: posix_fallocate returns its error directly, not via errno.
|
|
int Err;
|
|
do {
|
|
Err = posix_fallocate(FD, CurrentSize, NewSize - CurrentSize);
|
|
} while (Err == EINTR);
|
|
if (Err)
|
|
return CreateError(std::error_code(Err, std::generic_category()));
|
|
return NewSize;
|
|
#elif defined(__APPLE__)
|
|
fstore_t FAlloc;
|
|
FAlloc.fst_flags = F_ALLOCATEALL;
|
|
#if defined(F_ALLOCATEPERSIST) && \
|
|
defined(__ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__) && \
|
|
__ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ >= 130000
|
|
// F_ALLOCATEPERSIST is introduced in macOS 13.
|
|
FAlloc.fst_flags |= F_ALLOCATEPERSIST;
|
|
#endif
|
|
FAlloc.fst_posmode = F_PEOFPOSMODE;
|
|
FAlloc.fst_offset = 0;
|
|
FAlloc.fst_length = NewSize - CurrentSize;
|
|
FAlloc.fst_bytesalloc = 0;
|
|
if (sys::RetryAfterSignal(-1, ::fcntl, FD, F_PREALLOCATE, &FAlloc) == -1)
|
|
return CreateError(errnoAsErrorCode());
|
|
assert(CurrentSize + FAlloc.fst_bytesalloc >= NewSize);
|
|
return CurrentSize + FAlloc.fst_bytesalloc;
|
|
#else
|
|
(void)CreateError; // Silence unused variable.
|
|
return NewSize; // Pretend it worked.
|
|
#endif
|
|
}
|
|
|
|
bool cas::ondisk::useSmallMappingSize(const Twine &P) {
|
|
// Add exceptions to use small database file here.
|
|
#if defined(__APPLE__) && __has_include(<sys/mount.h>)
|
|
// macOS tmpfs does not support sparse tails.
|
|
SmallString<128> PathStorage;
|
|
StringRef Path = P.toNullTerminatedStringRef(PathStorage);
|
|
struct statfs StatFS;
|
|
if (statfs(Path.data(), &StatFS) != 0)
|
|
return false;
|
|
|
|
if (strcmp(StatFS.f_fstypename, "tmpfs") == 0)
|
|
return true;
|
|
#endif
|
|
// Default to use regular database file.
|
|
return false;
|
|
}
|
|
|
|
Expected<uint64_t> cas::ondisk::getBootTime() {
|
|
#ifdef __APPLE__
|
|
#if __has_include(<sys/sysctl.h>) && defined(KERN_BOOTTIME)
|
|
struct timeval TV;
|
|
size_t TVLen = sizeof(TV);
|
|
int KernBoot[2] = {CTL_KERN, KERN_BOOTTIME};
|
|
if (sysctl(KernBoot, 2, &TV, &TVLen, nullptr, 0) < 0)
|
|
return createStringError(llvm::errnoAsErrorCode(),
|
|
"failed to get boottime");
|
|
if (TVLen != sizeof(TV))
|
|
return createStringError("sysctl kern.boottime unexpected format");
|
|
return TV.tv_sec;
|
|
#else
|
|
return 0;
|
|
#endif
|
|
#elif defined(__linux__)
|
|
// Use the mtime for /proc, which is recreated during system boot.
|
|
// We could also read /proc/stat and search for 'btime'.
|
|
sys::fs::file_status Status;
|
|
if (std::error_code EC = sys::fs::status("/proc", Status))
|
|
return createFileError("/proc", EC);
|
|
return Status.getLastModificationTime().time_since_epoch().count();
|
|
#else
|
|
return 0;
|
|
#endif
|
|
}
|
|
|
|
Expected<StringRef>
|
|
cas::ondisk::UniqueTempFile::createAndCopyFrom(StringRef ParentPath,
|
|
StringRef CopyFromPath) {
|
|
// \c clonefile requires that the destination path doesn't exist. We create
|
|
// a "placeholder" temporary file, then modify its path a bit and use that
|
|
// for \c clonefile to write to.
|
|
// FIXME: Instead of creating a dummy file, add a new file system API for
|
|
// copying to a unique path that can loop while checking EEXIST.
|
|
SmallString<256> UniqueTmpPath;
|
|
SmallString<256> Model;
|
|
Model += ParentPath;
|
|
sys::path::append(Model, "%%%%%%%.tmp");
|
|
if (std::error_code EC = sys::fs::createUniqueFile(Model, UniqueTmpPath))
|
|
return createFileError(Model, EC);
|
|
TmpPath = std::move(UniqueTmpPath);
|
|
TmpPath += ".tmp"; // modify so that there's no file at that path.
|
|
// \c copy_file will use \c clonefile when applicable.
|
|
if (std::error_code EC = sys::fs::copy_file(CopyFromPath, TmpPath))
|
|
return createFileError(TmpPath, EC);
|
|
|
|
return TmpPath;
|
|
}
|
|
|
|
Error cas::ondisk::UniqueTempFile::renameTo(StringRef RenameToPath) {
|
|
if (std::error_code EC = sys::fs::rename(TmpPath, RenameToPath))
|
|
return createFileError(RenameToPath, EC);
|
|
TmpPath.clear();
|
|
return Error::success();
|
|
}
|
|
|
|
cas::ondisk::UniqueTempFile::~UniqueTempFile() {
|
|
if (!TmpPath.empty())
|
|
sys::fs::remove(TmpPath);
|
|
if (!UniqueTmpPath.empty())
|
|
sys::fs::remove(UniqueTmpPath);
|
|
}
|