blob: 874dbe557838ad9d2eb35be60367c7f5190809d3 [file] [edit]
//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//
//
/// \file
/// Encapsulates \p OnDiskGraphDB and \p OnDiskKeyValueDB instances within one
/// directory while also restricting storage growth with a scheme of chaining
/// the two most recent directories (primary & upstream), where the primary
/// "faults-in" data from the upstream one. When the primary (most recent)
/// directory exceeds its intended limit a new empty directory becomes the
/// primary one.
///
/// Within the top-level directory (the path that \p UnifiedOnDiskCache::open
/// receives) there are directories named like this:
///
/// 'v<version>.<x>'
/// 'v<version>.<x+1>'
/// 'v<version>.<x+2>'
/// ...
///
/// 'version' is the version integer for this \p UnifiedOnDiskCache's scheme and
/// the part after the dot is an increasing integer. The primary directory is
/// the one with the highest integer and the upstream one is the directory
/// before it. For example, if the sub-directories contained are:
///
/// 'v1.5', 'v1.6', 'v1.7', 'v1.8'
///
/// Then the primary one is 'v1.8', the upstream one is 'v1.7', and the rest are
/// unused directories that can be safely deleted at any time and by any
/// process.
///
/// Contained within the top-level directory is a file named "lock" which is
/// used for processes to take shared or exclusive locks for the contents of the
/// top directory. While a \p UnifiedOnDiskCache is open it keeps a shared lock
/// for the top-level directory; when it closes, if the primary sub-directory
/// exceeded its limit, it attempts to get an exclusive lock in order to create
/// a new empty primary directory; if it can't get the exclusive lock it gives
/// up and lets the next \p UnifiedOnDiskCache instance that closes to attempt
/// again.
///
/// The downside of this scheme is that while \p UnifiedOnDiskCache is open on a
/// directory, by any process, the storage size in that directory will keep
/// growing unrestricted. But the major benefit is that garbage-collection can
/// be triggered on a directory concurrently, at any time and by any process,
/// without affecting any active readers/writers in the same process or other
/// processes.
///
/// The \c UnifiedOnDiskCache also provides validation and recovery on top of
/// the underlying on-disk storage. The low-level storage is designed to remain
/// coherent across regular process crashes, but may be invalid after power loss
/// or similar system failures. \c UnifiedOnDiskCache::validateIfNeeded allows
/// validating the contents once per boot (or every time, where the boot time is
/// not known), and if validation fails (or crashes,
/// when performed in a separate process) \c UnifiedOnDiskCache::recover can
/// recover by marking invalid data for garbage collection.
///
/// Validation and recovery are serialized by an exclusive lock on the
/// "v1.validation" file, which records the boot time of the last successful
/// validation or recovery. Before validating, the file is marked as validation
/// pending, and the boot time is only written once validation succeeds; a
/// validation that fails or crashes leaves it pending, so the next validation
/// is not skipped. Recovery only happens while validation is pending, so when
/// multiple processes attempt recovery after a failed validation only the first
/// one recovers.
///
/// The data recovery described above requires exclusive access to the CAS, and
/// it is an error to attempt recovery if the CAS is open in any process/thread.
/// In order to maximize backwards compatibility with tools that do not perform
/// validation before opening the CAS, we do not attempt to get exclusive access
/// until recovery is actually performed, meaning as long as the data is valid
/// it will not conflict with concurrent use.
//
//===----------------------------------------------------------------------===//
#include "llvm/CAS/UnifiedOnDiskCache.h"
#include "OnDiskCommon.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/ADT/ScopeExit.h"
#include "llvm/ADT/SmallString.h"
#include "llvm/ADT/SmallVector.h"
#include "llvm/ADT/StringExtras.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/CAS/OnDiskCASLogger.h"
#include "llvm/CAS/OnDiskGraphDB.h"
#include "llvm/CAS/OnDiskKeyValueDB.h"
#include "llvm/Support/Compiler.h"
#include "llvm/Support/Errc.h"
#include "llvm/Support/Error.h"
#include "llvm/Support/FileSystem.h"
#include "llvm/Support/IOSandbox.h"
#include "llvm/Support/Path.h"
#include "llvm/Support/raw_ostream.h"
#include <limits>
#include <optional>
using namespace llvm;
using namespace llvm::cas;
using namespace llvm::cas::ondisk;
/// FIXME: When the version of \p DBDirPrefix is bumped up we need to figure out
/// how to handle the leftover sub-directories of the previous version, within
/// the \p UnifiedOnDiskCache::collectGarbage function.
static constexpr StringLiteral DBDirPrefix = "v1.";
static constexpr StringLiteral ValidationFilename = "v1.validation";
static constexpr StringLiteral CorruptPrefix = "corrupt.";
ObjectID UnifiedOnDiskCache::getObjectIDFromValue(ArrayRef<char> Value) {
// little endian encoded.
assert(Value.size() == sizeof(uint64_t));
return ObjectID::fromOpaqueData(support::endian::read64le(Value.data()));
}
UnifiedOnDiskCache::ValueBytes
UnifiedOnDiskCache::getValueFromObjectID(ObjectID ID) {
// little endian encoded.
UnifiedOnDiskCache::ValueBytes ValBytes;
static_assert(ValBytes.size() == sizeof(ID.getOpaqueData()));
support::endian::write64le(ValBytes.data(), ID.getOpaqueData());
return ValBytes;
}
Expected<std::optional<ArrayRef<char>>>
UnifiedOnDiskCache::faultInFromUpstreamKV(ArrayRef<uint8_t> Key) {
assert(UpstreamGraphDB);
assert(UpstreamKVDB);
std::optional<ArrayRef<char>> UpstreamValue;
if (Error E = UpstreamKVDB->get(Key).moveInto(UpstreamValue))
return std::move(E);
if (!UpstreamValue)
return std::nullopt;
// The value is the \p ObjectID in the context of the upstream
// \p OnDiskGraphDB instance. Translate it to the context of the primary
// \p OnDiskGraphDB instance.
ObjectID UpstreamID = getObjectIDFromValue(*UpstreamValue);
auto PrimaryID =
PrimaryGraphDB->getReference(UpstreamGraphDB->getDigest(UpstreamID));
if (LLVM_UNLIKELY(!PrimaryID))
return PrimaryID.takeError();
return PrimaryKVDB->put(Key, getValueFromObjectID(*PrimaryID));
}
/// \returns all the 'v<version>.<x>' names of sub-directories, sorted with
/// ascending order of the integer after the dot. Corrupt directories, if
/// included, will come first.
static Expected<SmallVector<std::string, 4>>
getAllDBDirs(StringRef Path, bool IncludeCorrupt = false) {
struct DBDir {
uint64_t Order;
std::string Name;
};
SmallVector<DBDir> FoundDBDirs;
std::error_code EC;
for (sys::fs::directory_iterator DirI(Path, EC), DirE; !EC && DirI != DirE;
DirI.increment(EC)) {
if (DirI->type() != sys::fs::file_type::directory_file)
continue;
StringRef SubDir = sys::path::filename(DirI->path());
if (IncludeCorrupt && SubDir.starts_with(CorruptPrefix)) {
FoundDBDirs.push_back({0, std::string(SubDir)});
continue;
}
if (!SubDir.starts_with(DBDirPrefix))
continue;
uint64_t Order;
if (SubDir.substr(DBDirPrefix.size()).getAsInteger(10, Order))
return createStringError(inconvertibleErrorCode(),
"unexpected directory " + DirI->path());
FoundDBDirs.push_back({Order, std::string(SubDir)});
}
if (EC)
return createFileError(Path, EC);
llvm::sort(FoundDBDirs, [](const DBDir &LHS, const DBDir &RHS) -> bool {
return LHS.Order < RHS.Order;
});
SmallVector<std::string, 4> DBDirs;
for (DBDir &Dir : FoundDBDirs)
DBDirs.push_back(std::move(Dir.Name));
return DBDirs;
}
static Expected<SmallVector<std::string, 4>> getAllGarbageDirs(StringRef Path) {
auto DBDirs = getAllDBDirs(Path, /*IncludeCorrupt=*/true);
if (!DBDirs)
return DBDirs.takeError();
// FIXME: When the version of \p DBDirPrefix is bumped up we need to figure
// out how to handle the leftover sub-directories of the previous version.
for (unsigned Keep = 2; Keep > 0 && !DBDirs->empty(); --Keep) {
StringRef Back(DBDirs->back());
if (Back.starts_with(CorruptPrefix))
break;
DBDirs->pop_back();
}
return *DBDirs;
}
/// \returns Given a sub-directory named 'v<version>.<x>', it outputs the
/// 'v<version>.<x+1>' name.
static void getNextDBDirName(StringRef DBDir, llvm::raw_ostream &OS) {
assert(DBDir.starts_with(DBDirPrefix));
uint64_t Count;
bool Failed = DBDir.substr(DBDirPrefix.size()).getAsInteger(10, Count);
assert(!Failed);
(void)Failed;
OS << DBDirPrefix << Count + 1;
}
Error UnifiedOnDiskCache::validateActionCache() const {
return getKeyValueDB().validate();
}
static Error validateInProcess(StringRef RootPath, StringRef HashName,
unsigned HashByteSize, bool CheckHash,
OnDiskGraphDB::HashingFuncT HashFn) {
std::shared_ptr<UnifiedOnDiskCache> UniDB;
if (Error E = UnifiedOnDiskCache::open(RootPath, std::nullopt, HashName,
HashByteSize)
.moveInto(UniDB))
return E;
if (Error E = UniDB->getGraphDB().validate(CheckHash, HashFn))
return E;
if (Error E = UniDB->validateActionCache())
return E;
return Error::success();
}
/// \returns the boot time, or 0 if it is not known, including if getting it
/// failed. Validation is never skipped where the boot time is not known.
static uint64_t getCachedBootTime() {
static const uint64_t BootTime =
expectedToOptional(getBootTime()).value_or(0);
return BootTime;
}
namespace {
/// The validation file records the state of validation for the data:
/// - empty: never validated.
/// - \c ValidationPending: a validation started but did not yet succeed, i.e.
/// it is in progress, failed, or crashed, and the data has not been
/// recovered since.
/// - a boot time: the data was validated or recovered during that boot.
///
/// While this object is alive it holds an exclusive lock on the file, which
/// serializes validation and recovery across processes and threads.
///
/// Lock ordering: the validation file lock is always acquired before the
/// top-level "lock" file, and the latter is only ever acquired exclusively via
/// a non-blocking try-lock, so that validation and recovery cannot deadlock.
class LockedValidationFile {
public:
/// Written before validating. It is an integer so that older versions, which
/// only know about boot times, still parse the file and treat it as not
/// validated. It never matches a boot time.
static constexpr uint64_t ValidationPending =
std::numeric_limits<uint64_t>::max();
static Expected<std::unique_ptr<LockedValidationFile>>
open(StringRef RootPath) {
if (std::error_code EC = sys::fs::create_directories(RootPath))
return createFileError(RootPath, EC);
SmallString<256> PathBuf(RootPath);
sys::path::append(PathBuf, ValidationFilename);
int FD = -1;
if (std::error_code EC = sys::fs::openFileForReadWrite(
PathBuf, FD, sys::fs::CD_OpenAlways, sys::fs::OF_None))
return createFileError(PathBuf, EC);
assert(FD != -1);
std::unique_ptr<LockedValidationFile> VF(
new LockedValidationFile(PathBuf, FD));
if (std::error_code EC =
lockFileThreadSafe(FD, sys::fs::LockKind::Exclusive))
return createFileError(PathBuf, EC);
VF->Locked = true;
SmallString<8> Bytes;
if (Error E = sys::fs::readNativeFileToEOF(VF->File, Bytes))
return createFileError(PathBuf, std::move(E));
if (!Bytes.empty()) {
uint64_t Value;
if (StringRef(Bytes).trim().getAsInteger(10, Value))
return createFileError(PathBuf, errc::illegal_byte_sequence,
"expected integer");
VF->State = Value;
}
return std::move(VF);
}
~LockedValidationFile() {
if (Locked)
unlockFileThreadSafe(FD);
sys::fs::closeFile(File);
}
/// \returns the boot time of the last successful validation or recovery, or
/// 0 if there is none.
uint64_t getLastValidBootTime() const {
return isValidationPending() ? 0 : State.value_or(0);
}
/// Whether the data was validated or recovered during the boot with
/// \p BootTime. Always false where the boot time is not known, i.e. 0,
/// since it cannot be told whether that was during the current boot.
bool isValidAtBoot(uint64_t BootTime) const {
return BootTime != 0 && State == BootTime;
}
bool isValidationPending() const { return State == ValidationPending; }
Error setValidationPending() { return write(ValidationPending); }
Error setLastValidBootTime(uint64_t BootTime) { return write(BootTime); }
private:
LockedValidationFile(StringRef Path, int FD)
: Path(Path), FD(FD), File(sys::fs::convertFDToNativeFile(FD)) {}
Error write(uint64_t Value) {
if (State == Value)
return Error::success();
if (std::error_code EC = sys::fs::resize_file(FD, 0))
return createFileError(Path, EC);
raw_fd_ostream OS(FD, /*shouldClose=*/false);
OS.seek(0); // resize does not reset position
OS << Value << '\n';
if (OS.has_error())
return createFileError(Path, OS.error());
State = Value;
return Error::success();
}
SmallString<256> Path;
int FD;
sys::fs::file_t File;
bool Locked = false;
std::optional<uint64_t> State;
};
} // namespace
/// Marks all the database directories in \p RootPath as corrupt, which makes
/// them eligible for garbage collection. Requires exclusive access to the CAS.
static Error markAllDBDirsCorrupt(StringRef RootPath) {
SmallString<256> PathBuf(RootPath);
sys::path::append(PathBuf, "lock");
int LockFD = -1;
if (std::error_code EC = sys::fs::openFileForReadWrite(
PathBuf, LockFD, sys::fs::CD_OpenAlways, sys::fs::OF_None))
return createFileError(PathBuf, EC);
sys::fs::file_t LockFile = sys::fs::convertFDToNativeFile(LockFD);
llvm::scope_exit CloseLock([&]() { sys::fs::closeFile(LockFile); });
if (std::error_code EC = tryLockFileThreadSafe(LockFD)) {
if (EC == std::errc::no_lock_available)
return createFileError(
PathBuf, EC,
"CAS recovery requires exclusive access but CAS was in use");
return createFileError(PathBuf, EC);
}
llvm::scope_exit UnlockFD([&]() { unlockFileThreadSafe(LockFD); });
auto DBDirs = getAllDBDirs(RootPath);
if (!DBDirs)
return DBDirs.takeError();
for (StringRef DBDir : *DBDirs) {
sys::path::remove_filename(PathBuf);
sys::path::append(PathBuf, DBDir);
// Pick the first name not taken by earlier recoveries. Checking the error
// of the rename is not enough since Windows reports permission denied when
// the destination directory exists. The name cannot be taken concurrently
// since only garbage collection touches these directories, and it only
// removes them.
int Attempt = 0, MaxAttempts = 100;
SmallString<128> GCPath;
for (; Attempt < MaxAttempts; ++Attempt) {
GCPath.assign(RootPath);
sys::path::append(GCPath,
CorruptPrefix + std::to_string(Attempt) + "." + DBDir);
if (!sys::fs::exists(GCPath))
break;
}
if (Attempt == MaxAttempts)
return createStringError(
errc::file_exists,
"rename " + PathBuf +
" failed: too many CAS directories awaiting pruning");
if (std::error_code EC = sys::fs::rename(PathBuf, GCPath))
return createStringError(EC, "rename " + PathBuf + " to " + GCPath +
" failed: " + EC.message());
}
return Error::success();
}
Expected<ValidationResult> UnifiedOnDiskCache::validateIfNeeded(
StringRef RootPath, StringRef HashName, unsigned HashByteSize,
bool CheckHash, OnDiskGraphDB::HashingFuncT HashFn, bool ForceValidation) {
std::unique_ptr<LockedValidationFile> VF;
if (Error E = LockedValidationFile::open(RootPath).moveInto(VF))
return std::move(E);
std::shared_ptr<ondisk::OnDiskCASLogger> Logger;
#ifndef _WIN32
if (Error E =
ondisk::OnDiskCASLogger::openIfEnabled(RootPath).moveInto(Logger))
return std::move(E);
#endif
uint64_t BootTime = getCachedBootTime();
uint64_t ValidationBootTime = VF->getLastValidBootTime();
bool Skipped = false;
std::string LogValidationError;
llvm::scope_exit Log([&] {
if (!Logger)
return;
Logger->logUnifiedOnDiskCacheValidateIfNeeded(
RootPath, BootTime, ValidationBootTime, CheckHash, ForceValidation,
LogValidationError, Skipped);
});
if (VF->isValidAtBoot(BootTime) && !ForceValidation) {
Skipped = true;
return ValidationResult::Skipped;
}
// Mark validation as pending until it succeeds, so that a failed or crashed
// validation is detected by recovery and by the next validation.
if (Error E = VF->setValidationPending())
return std::move(E);
if (Error E = validateInProcess(RootPath, HashName, HashByteSize, CheckHash,
HashFn)) {
if (Logger)
LogValidationError = toStringWithoutConsuming(E);
return std::move(E);
}
if (Error E = VF->setLastValidBootTime(BootTime))
return std::move(E);
return ValidationResult::Valid;
}
Expected<ValidationResult> UnifiedOnDiskCache::recover(StringRef RootPath) {
std::unique_ptr<LockedValidationFile> VF;
if (Error E = LockedValidationFile::open(RootPath).moveInto(VF))
return std::move(E);
std::shared_ptr<ondisk::OnDiskCASLogger> Logger;
#ifndef _WIN32
if (Error E =
ondisk::OnDiskCASLogger::openIfEnabled(RootPath).moveInto(Logger))
return std::move(E);
#endif
uint64_t BootTime = getCachedBootTime();
bool Skipped = false;
std::string LogRecoveryError;
llvm::scope_exit Log([&] {
if (!Logger)
return;
Logger->logUnifiedOnDiskCacheRecover(RootPath, BootTime, LogRecoveryError,
Skipped);
});
// Unless validation is still pending, the data has been recovered or
// successfully validated since the failed validation, e.g. by a concurrent
// process.
if (!VF->isValidationPending()) {
Skipped = true;
return ValidationResult::Skipped;
}
if (Error E = markAllDBDirsCorrupt(RootPath)) {
if (Logger)
LogRecoveryError = toStringWithoutConsuming(E);
return std::move(E);
}
if (Error E = VF->setLastValidBootTime(BootTime))
return std::move(E);
return ValidationResult::Recovered;
}
Expected<std::unique_ptr<UnifiedOnDiskCache>>
UnifiedOnDiskCache::open(StringRef RootPath, std::optional<uint64_t> SizeLimit,
StringRef HashName, unsigned HashByteSize,
OnDiskGraphDB::FaultInPolicy FaultInPolicy) {
auto BypassSandbox = sys::sandbox::scopedDisable();
if (std::error_code EC = sys::fs::create_directories(RootPath))
return createFileError(RootPath, EC);
SmallString<256> PathBuf(RootPath);
sys::path::append(PathBuf, "lock");
int LockFD = -1;
if (std::error_code EC = sys::fs::openFileForReadWrite(
PathBuf, LockFD, sys::fs::CD_OpenAlways, sys::fs::OF_None))
return createFileError(PathBuf, EC);
assert(LockFD != -1);
// Locking the directory using shared lock, which will prevent other processes
// from creating a new chain (essentially while a \p UnifiedOnDiskCache
// instance holds a shared lock the storage for the primary directory will
// grow unrestricted).
if (std::error_code EC =
lockFileThreadSafe(LockFD, sys::fs::LockKind::Shared))
return createFileError(PathBuf, EC);
auto DBDirs = getAllDBDirs(RootPath);
if (!DBDirs)
return DBDirs.takeError();
if (DBDirs->empty())
DBDirs->push_back((Twine(DBDirPrefix) + "1").str());
std::shared_ptr<ondisk::OnDiskCASLogger> Logger;
#ifndef _WIN32
if (Error E =
ondisk::OnDiskCASLogger::openIfEnabled(RootPath).moveInto(Logger))
return std::move(E);
#endif
/// If there is only one directory open databases on it. If there are 2 or
/// more directories, get the most recent directories and chain them, with the
/// most recent being the primary one. The remaining directories are unused
/// data than can be garbage-collected.
auto UniDB = std::unique_ptr<UnifiedOnDiskCache>(new UnifiedOnDiskCache());
std::unique_ptr<OnDiskGraphDB> UpstreamGraphDB;
std::unique_ptr<OnDiskKeyValueDB> UpstreamKVDB;
if (DBDirs->size() > 1) {
StringRef UpstreamDir = *(DBDirs->end() - 2);
PathBuf = RootPath;
sys::path::append(PathBuf, UpstreamDir);
if (Error E =
OnDiskGraphDB::open(PathBuf, HashName, HashByteSize,
/*UpstreamDB=*/nullptr, Logger, FaultInPolicy)
.moveInto(UpstreamGraphDB))
return std::move(E);
if (Error E = OnDiskKeyValueDB::open(PathBuf, HashName, HashByteSize,
/*ValueName=*/"objectid",
/*ValueSize=*/sizeof(uint64_t),
/*UnifiedCache=*/nullptr, Logger)
.moveInto(UpstreamKVDB))
return std::move(E);
}
StringRef PrimaryDir = *(DBDirs->end() - 1);
PathBuf = RootPath;
sys::path::append(PathBuf, PrimaryDir);
std::unique_ptr<OnDiskGraphDB> PrimaryGraphDB;
if (Error E =
OnDiskGraphDB::open(PathBuf, HashName, HashByteSize,
UpstreamGraphDB.get(), Logger, FaultInPolicy)
.moveInto(PrimaryGraphDB))
return std::move(E);
std::unique_ptr<OnDiskKeyValueDB> PrimaryKVDB;
// \p UnifiedOnDiskCache does manual chaining for key-value requests,
// including an extra translation step of the value during fault-in.
if (Error E = OnDiskKeyValueDB::open(PathBuf, HashName, HashByteSize,
/*ValueName=*/"objectid",
/*ValueSize=*/sizeof(uint64_t),
UniDB.get(), Logger)
.moveInto(PrimaryKVDB))
return std::move(E);
UniDB->RootPath = RootPath;
UniDB->SizeLimit = SizeLimit.value_or(0);
UniDB->LockFD = LockFD;
UniDB->NeedsGarbageCollection = DBDirs->size() > 2;
UniDB->PrimaryDBDir = PrimaryDir;
UniDB->UpstreamGraphDB = std::move(UpstreamGraphDB);
UniDB->PrimaryGraphDB = std::move(PrimaryGraphDB);
UniDB->UpstreamKVDB = std::move(UpstreamKVDB);
UniDB->PrimaryKVDB = std::move(PrimaryKVDB);
UniDB->Logger = std::move(Logger);
return std::move(UniDB);
}
void UnifiedOnDiskCache::setSizeLimit(std::optional<uint64_t> SizeLimit) {
this->SizeLimit = SizeLimit.value_or(0);
}
uint64_t UnifiedOnDiskCache::getStorageSize() const {
uint64_t TotalSize = getPrimaryStorageSize();
if (UpstreamGraphDB)
TotalSize += UpstreamGraphDB->getStorageSize();
if (UpstreamKVDB)
TotalSize += UpstreamKVDB->getStorageSize();
return TotalSize;
}
uint64_t UnifiedOnDiskCache::getPrimaryStorageSize() const {
return PrimaryGraphDB->getStorageSize() + PrimaryKVDB->getStorageSize();
}
bool UnifiedOnDiskCache::hasExceededSizeLimit() const {
uint64_t CurSizeLimit = SizeLimit;
if (!CurSizeLimit)
return false;
// If the hard limit is beyond 85%, declare above limit and request clean up.
unsigned CurrentPercent =
std::max(PrimaryGraphDB->getHardStorageLimitUtilization(),
PrimaryKVDB->getHardStorageLimitUtilization());
if (CurrentPercent > 85)
return true;
// We allow each of the directories in the chain to reach up to half the
// intended size limit. Check whether the primary directory has exceeded half
// the limit or not, in order to decide whether we need to start a new chain.
//
// We could check the size limit against the sum of sizes of both the primary
// and upstream directories but then if the upstream is significantly larger
// than the intended limit, it would trigger a new chain to be created before
// the primary has reached its own limit. Essentially in such situation we
// prefer reclaiming the storage later in order to have more consistent cache
// hits behavior.
return (CurSizeLimit / 2) < getPrimaryStorageSize();
}
Error UnifiedOnDiskCache::close(bool CheckSizeLimit) {
auto BypassSandbox = sys::sandbox::scopedDisable();
if (LockFD == -1)
return Error::success(); // already closed.
llvm::scope_exit CloseLock([&]() {
assert(LockFD >= 0);
sys::fs::file_t LockFile = sys::fs::convertFDToNativeFile(LockFD);
sys::fs::closeFile(LockFile);
LockFD = -1;
});
bool ExceededSizeLimit = CheckSizeLimit ? hasExceededSizeLimit() : false;
UpstreamKVDB.reset();
PrimaryKVDB.reset();
UpstreamGraphDB.reset();
PrimaryGraphDB.reset();
if (std::error_code EC = unlockFileThreadSafe(LockFD))
return createFileError(RootPath, EC);
if (!ExceededSizeLimit)
return Error::success();
// The primary directory exceeded its intended size limit. Try to get an
// exclusive lock in order to create a new primary directory for next time
// this \p UnifiedOnDiskCache path is opened.
if (std::error_code EC = tryLockFileThreadSafe(
LockFD, std::chrono::milliseconds(0), sys::fs::LockKind::Exclusive)) {
if (EC == errc::no_lock_available)
return Error::success(); // couldn't get exclusive lock, give up.
return createFileError(RootPath, EC);
}
llvm::scope_exit UnlockFile([&]() { unlockFileThreadSafe(LockFD); });
// Managed to get an exclusive lock which means there are no other open
// \p UnifiedOnDiskCache instances for the same path, so we can safely start a
// new primary directory. To start a new primary directory we just have to
// create a new empty directory with the next consecutive index; since this is
// an atomic operation we will leave the top-level directory in a consistent
// state even if the process dies during this code-path.
SmallString<256> PathBuf(RootPath);
raw_svector_ostream OS(PathBuf);
OS << sys::path::get_separator();
getNextDBDirName(PrimaryDBDir, OS);
if (std::error_code EC = sys::fs::create_directory(PathBuf))
return createFileError(PathBuf, EC);
NeedsGarbageCollection = true;
return Error::success();
}
UnifiedOnDiskCache::UnifiedOnDiskCache() = default;
UnifiedOnDiskCache::~UnifiedOnDiskCache() { consumeError(close()); }
Error UnifiedOnDiskCache::collectGarbage(StringRef Path,
ondisk::OnDiskCASLogger *Logger) {
auto DBDirs = getAllGarbageDirs(Path);
if (!DBDirs)
return DBDirs.takeError();
SmallString<256> PathBuf(Path);
for (StringRef UnusedSubDir : *DBDirs) {
sys::path::append(PathBuf, UnusedSubDir);
if (Logger)
Logger->logUnifiedOnDiskCacheCollectGarbage(PathBuf);
if (std::error_code EC = sys::fs::remove_directories(PathBuf))
return createFileError(PathBuf, EC);
sys::path::remove_filename(PathBuf);
}
return Error::success();
}
Error UnifiedOnDiskCache::collectGarbage() {
return collectGarbage(RootPath, Logger.get());
}