// Part of the Carbon Language project, under the Apache License v2.0 with LLVM // Exceptions. See /LICENSE for license information. // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception #include "toolchain/driver/runtimes_cache.h" #include #include #include #include #include #include #include #include #include #include #include "common/filesystem.h" #include "common/version.h" #include "common/vlog.h" #include "llvm/ADT/ArrayRef.h" #include "llvm/ADT/ScopeExit.h" #include "llvm/ADT/StringExtras.h" #include "llvm/ADT/StringRef.h" #include "llvm/Support/FormatAdapters.h" #include "llvm/Support/Program.h" #include "llvm/Support/SHA256.h" namespace Carbon { auto Runtimes::OpenExisting(std::filesystem::path path, llvm::raw_ostream* vlog_stream) -> ErrorOr { CARBON_ASSIGN_OR_RETURN(Filesystem::Dir dir, Filesystem::Cwd().OpenDir(path)); return Runtimes(std::move(path), std::move(dir), {}, {}, vlog_stream); } auto Runtimes::Make(std::filesystem::path path, llvm::raw_ostream* vlog_stream) -> ErrorOr { CARBON_ASSIGN_OR_RETURN(Filesystem::Dir dir, Filesystem::Cwd().CreateDirectories(path)); return Runtimes(std::move(path), std::move(dir), {}, {}, vlog_stream); } auto Runtimes::Destroy() -> void { // Release the lock on the runtimes and close the lock file. flock_ = {}; auto close_result = std::move(lock_file_).Close(); if (!close_result.ok()) { // Log and continue on close errors. CARBON_VLOG("Error closing lock file for runtimes '{0}': {1}", base_path_, close_result.error()); } } auto Runtimes::Get(Component component) -> ErrorOr { std::filesystem::path path = base_path_ / ComponentPath(component); auto open_result = base_dir_.OpenDir(ComponentPath(component), Filesystem::OpenExisting); if (open_result.ok()) { return path; } return open_result.error().ToError(); } auto Runtimes::Build(Component component) -> ErrorOr> { return BuildImpl(component, BuildLockDeadline, BuildLockPollInterval); } auto Runtimes::Remove(Component component) -> ErrorOr { CARBON_RETURN_IF_ERROR(base_dir_.Rmtree(ComponentPath(component))); return Success(); } auto Runtimes::BuildImpl(Component component, Filesystem::Duration deadline, Filesystem::Duration poll_interval) -> ErrorOr> { // Try to get an existing resource directory first. auto existing_result = Get(component); if (existing_result.ok()) { return {*std::move(existing_result)}; } // Otherwise, we will need to build the runtimes and commit them into this // directory once ready. Try and acquire an advisory lock to avoid redundant // computation. std::string_view component_path = ComponentPath(component); CARBON_ASSIGN_OR_RETURN( Filesystem::ReadWriteFile lock_file, base_dir_.OpenReadWrite( llvm::formatv(LockFileFormat, component_path).str(), Filesystem::OpenAlways, /*creation_mode=*/0700)); CARBON_VLOG("PID {0} locking cache path: {1}\n", getpid(), base_path_ / component_path); Filesystem::FileLock flock; auto flock_result = lock_file.TryLock(Filesystem::FileLock::Exclusive, deadline, poll_interval); if (flock_result.ok()) { flock = *std::move(flock_result); CARBON_VLOG("Successfully locked cache path\n"); // As a debugging aid, write our PID into the lock file when we // successfully acquire it. Ignore errors here though. (void)lock_file.WriteFileFromString(std::to_string(getpid())); } else if (!flock_result.error().would_block()) { // Some unexpected filesystem error, report that rather than trying to // continue. return std::move(flock_result).error(); } else { CARBON_VLOG("Unable to lock cache path, held by: {1}\n", *lock_file.ReadFileToString()); (void)std::move(lock_file).Close(); } // See if another process has built the runtimes while we waited on the lock. // We do this even if we didn't successfully acquire the lock because we // ensure that a successful build atomically creates a viable directory. existing_result = Get(component); if (existing_result.ok()) { // Clear and close the lock file. (void)lock_file.WriteFileFromString(""); flock = {}; (void)std::move(lock_file).Close(); return {*std::move(existing_result)}; } // Whether we hold the lock file or not, we're going to now build these // runtimes. Create a temporary directory where we can do that safely // regardless of what else is happening. std::filesystem::path tmp_path = base_path_ / llvm::formatv(".{0}.tmp", component_path).str(); CARBON_ASSIGN_OR_RETURN(Filesystem::RemovingDir tmp_dir, Filesystem::MakeTmpDirWithPrefix(tmp_path)); return {Builder(*this, std::move(lock_file), std::move(flock), std::move(tmp_dir), component_path)}; } auto Runtimes::Cache::FindXdgCachePath() -> std::optional { if (const char* xdg_cache_home = getenv("XDG_CACHE_HOME"); xdg_cache_home != nullptr) { std::filesystem::path path = xdg_cache_home; if (path.is_absolute()) { CARBON_VLOG("Using '$XDG_CACHE_HOME' cache: {0}", path); return path; } } // Unable to use the standard environment variable. Try the designated // fallback of `$HOME/.cache`. const char* home = getenv("HOME"); if (home == nullptr) { return std::nullopt; } std::filesystem::path path = home; if (!path.is_absolute()) { return std::nullopt; } path /= ".cache"; CARBON_VLOG("Using '$HOME/.cache' cache: {0}", path); return path; } auto Runtimes::Cache::InitTmpSystemCache() -> ErrorOr { CARBON_ASSIGN_OR_RETURN(dir_owner_, Filesystem::MakeTmpDir()); path_ = std::get(dir_owner_).path(); dir_ = std::get(dir_owner_); CARBON_VLOG("Using temporary cache: {0}", path_); return Success(); } auto Runtimes::Cache::InitSystemCache(const InstallPaths& install) -> ErrorOr { constexpr llvm::StringLiteral CachePath = "carbon_runtimes"; // If we have a digest to use as the cache key, save it and we can try to // use persistent caches. auto read_digest_result = Filesystem::Cwd().ReadFileToString(install.digest_path()); if (!read_digest_result.ok()) { return InitTmpSystemCache(); } cache_key_ = *std::move(read_digest_result); auto xdg_path_result = FindXdgCachePath(); if (!xdg_path_result) { return InitTmpSystemCache(); } // We have a candidate XDG-based cache path. Try to open that, and a // directory below it for Carbon's runtimes. Note that we don't error on a // missing directory, we fall through to using a temporary directory. auto open_result = Filesystem::Cwd().OpenDir(*xdg_path_result); if (!open_result.ok()) { if (!open_result.error().no_entity()) { // Some other unexpected error in the filesystem, propagate that. return std::move(open_result).error(); } // Otherwise we fall back to a temporary system cache. return InitTmpSystemCache(); } path_ = *std::move(xdg_path_result); // Now open a subdirectory of the cache for Carbon's usage. This will // create a subdirectory if one doesn't yet exist. path_ /= std::string_view(CachePath); CARBON_ASSIGN_OR_RETURN( dir_owner_, open_result->OpenDir(CachePath.str(), Filesystem::OpenAlways, /*creation_mode=*/0700)); dir_ = std::get(dir_owner_); // Ensure the directory has narrow permissions so runtimes can't be // overwritten. CARBON_ASSIGN_OR_RETURN(auto dir_stat, dir_.Stat()); if (dir_stat.permissions() != 0700 || dir_stat.unix_uid() != geteuid()) { return Error(llvm::formatv( "Found runtimes cache path '{0}' with excessive permissions ({1}) " "or an invalid owning UID ({2})", path_, dir_stat.permissions(), dir_stat.unix_uid())); } return Success(); } auto Runtimes::Cache::InitCachePath(const InstallPaths& install, std::filesystem::path cache_path) -> ErrorOr { auto read_digest_result = Filesystem::Cwd().ReadFileToString(install.digest_path()); if (read_digest_result.ok()) { // If we have a digest to use as the cache key, save it and we can try to // use persistent caches. cache_key_ = *std::move(read_digest_result); } else { // Without a digest, use the path itself as the key. cache_key_ = cache_path.string(); } CARBON_ASSIGN_OR_RETURN(dir_owner_, Filesystem::Cwd().OpenDir(cache_path)); dir_ = std::get(dir_owner_); path_ = std::move(cache_path); CARBON_VLOG("Using custom cache: {0}", path_); return Success(); } auto Runtimes::Cache::Lookup(const Features& features) -> ErrorOr { // Compute the hash of the features. We'll use this to build the subdirectory // within the cache. llvm::SHA256 entry_hasher; // First incorporate our cache key that comes from the installation's digest. // This ensures we don't share a cache entry with any other Carbon // installations using different inputs. entry_hasher.update(cache_key_); // Then incorporate the specific features that are enabled in this entry. entry_hasher.update(features.target); std::array entry_digest = entry_hasher.final(); std::filesystem::path entry_path = llvm::formatv("runtimes-{0}-{1}", Version::String, llvm::toHex(entry_digest, /*LowerCase=*/true)) .str(); Filesystem::Dir entry_dir; auto open_result = dir_.OpenDir(entry_path, Filesystem::OpenExisting); if (open_result.ok()) { entry_dir = *std::move(open_result); } else { if (!open_result.error().no_entity()) { return std::move(open_result).error(); } // We're going to potentially create a new set of runtimes, prune the // existing runtimes first to provide a bound on the total size of runtimes. PruneStaleRuntimes(entry_path); // Now we can create or open, we don't care if a racing process created the // same runtime directory. CARBON_ASSIGN_OR_RETURN(entry_dir, dir_.OpenDir(entry_path, Filesystem::OpenAlways)); } CARBON_ASSIGN_OR_RETURN( auto lock_file, entry_dir.OpenWriteOnly(".lock", Filesystem::OpenAlways)); CARBON_RETURN_IF_ERROR(lock_file.UpdateTimes()); CARBON_ASSIGN_OR_RETURN( Filesystem::FileLock flock, lock_file.TryLock(Filesystem::FileLock::Shared, RuntimesLockDeadline, RuntimesLockPollInterval)); return Runtimes(path_ / entry_path, std::move(entry_dir), std::move(lock_file), std::move(flock), vlog_stream_); } auto Runtimes::Cache::ComputeEntryAges( llvm::SmallVector entry_paths) -> llvm::SmallVector { llvm::SmallVector entries; Filesystem::TimePoint now = Filesystem::Clock::now(); for (auto& path : entry_paths) { // We use the `mtime` from the lock file in the directory rather than the // directory itself to avoid any oddities with `mtime` on directories. // // Note that we also ignore errors here as if we can't read the stamp file // we will pick an arbitrary old time stamp, and we want pruning to be // maximally resilient to partially deleted or corrupted caches in order to // prune them back into a healthy state. auto stat_result = dir_.Lstat(path / ".lock"); auto mtime = stat_result.ok() ? stat_result->mtime() : Filesystem::TimePoint(Filesystem::Duration(0)); entries.push_back({.path = std::move(path), .age = now - mtime}); } return entries; } auto Runtimes::Cache::PruneStaleRuntimes( const std::filesystem::path& new_entry_path) -> void { llvm::SmallVector dir_entries; llvm::SmallVector non_dir_entries; auto read_result = dir_.AppendEntriesIf( dir_entries, non_dir_entries, [](llvm::StringRef name) { return name.starts_with("runtimes-"); }); if (!read_result.ok()) { CARBON_VLOG("Unable to read cache directory to prune stale entries: {0}", read_result.error()); return; } // Directly attempt to remove non-directory and bad directory entries. for (const auto& name : non_dir_entries) { CARBON_VLOG("Unlinking non-directory entry '{0}'", name); auto result = dir_.Unlink(name); if (!result.ok()) { CARBON_VLOG("Error unlinking non-directory entry '{0}': {1}", name, result.error()); } } // If we only have a small number of entries, no need to prune. if (dir_entries.size() < MinNumEntries) { return; } llvm::SmallVector entries = ComputeEntryAges(std::move(dir_entries)); auto rm_entry = [&](const std::filesystem::path& entry_name) { // Note that we don't propagate errors here because we want to prune as much // as possible. We do log them. CARBON_VLOG("Removing cache entry '{0}'", entry_name); auto rm_result = dir_.Rmtree(entry_name); if (!rm_result.ok() && !rm_result.error().no_entity()) { CARBON_VLOG("Unable to remove old runtimes '{0}': {1}", entry_name, rm_result.error()); return false; } return true; }; // Remove entries older than our max first. We don't need to check for locking // or other issues here given the age. llvm::erase_if(entries, [&](const Entry& entry) { return entry.age > MaxEntryAge && rm_entry(entry.path); }); // Sort the entries so that the oldest is first. llvm::sort(entries, [](const Entry& lhs, const Entry& rhs) { return lhs.age > rhs.age; }); // Now try to get the number of entries below our max target by removing the // least-recently used entries that are either more than our max locked age or // unlocked. auto rm_unlocked_entry = [&](const std::filesystem::path& name, Filesystem::Duration age) { // Past a certain age, bypass the locking for efficiency and to avoid // retaining entries with stale locks. if (age > MaxLockedEntryAge) { return rm_entry(name); } CARBON_VLOG("Attempting to lock cache entry '{0}'", name); auto lock_file_open_result = dir_.OpenReadOnly(name / ".lock", Filesystem::OpenAlways); if (!lock_file_open_result.ok()) { if (lock_file_open_result.error().no_entity() || lock_file_open_result.error().not_dir()) { // The only way these failures should be possible is if something // removed the cache directory between our read above and here. Assume // the entry is gone and continue. return true; } // For other errors, assume locked. CARBON_VLOG("Error opening lock file for cache entry '{0}': {1}", name, lock_file_open_result.error()); return false; } Filesystem::ReadFile lock_file = *std::move(lock_file_open_result); auto lock_result = lock_file.TryLock(Filesystem::FileLock::Exclusive, RuntimesLockDeadline, RuntimesLockPollInterval); if (!lock_result.ok()) { // The normal case is when locking would block, log anything else. if (!lock_result.error().would_block()) { CARBON_VLOG("Error locking cache entry '{0}': {1}", name, lock_result.error()); } // However, don't try to remove it as we didn't acquire the lock. return false; } // The lock is held, remove the entry. return rm_entry(name); }; int num_entries = entries.size(); for (const auto& [name, age] : entries) { if (num_entries < MaxNumEntries) { break; } // Don't prune the currently being built entry. We should only reach here // when some other process created this entry in a race, and we don't want // to remove it or trigger rebuilds. if (name == new_entry_path) { continue; } if (rm_unlocked_entry(name, age)) { --num_entries; } } if (num_entries >= MaxNumEntries) { CARBON_VLOG( "Unable to prune cache to our target size due to held locks on recent " "cache entries or removal errors, leaving {0} entries in the cache", num_entries); } } auto Runtimes::Builder::Commit() && -> ErrorOr { std::filesystem::path dest_path = runtimes_->base_path() / dest_; // First, try to do the atomic commit of the built runtimes into the final // location. CARBON_CHECK(dir_.path().parent_path() == runtimes_->base_path(), "Building a temporary directory '{0}' that is not in the " "runtimes tree '{1}'", dir_.path(), runtimes_->base_path()); auto rename_result = runtimes_->base_dir().Rename( dir_.path().filename(), runtimes_->base_dir(), dest_); // If the rename was successful, then we don't need to remove anything so // release that state. if (rename_result.ok()) { std::move(dir_).Release(); } else if (rename_result.error().not_empty()) { // Some other runtimes were successfully committed before ours, so we want // to discard ours. We report errors cleaning up here as we don't want to // pollute the filesystem excessively. // // TODO: Consider instead being more resilient to errors here and just log // them. CARBON_VLOG("PID {0} found racily built runtimes in cache path: {1}", getpid(), dest_path); CARBON_RETURN_IF_ERROR(std::move(dir_).Remove()); } else { // An unexpected error occurred, propagate it and let the normal cleanup // occur. // // TODO: It's possible we need to handle `EBUSY` here, likely by ensuring it // is the *destination* that is busy and an existing, valid directory built // concurrently. return std::move(rename_result).error(); } // Now that we've got a final path in place successfully, clear the flock if // it is currently held. ReleaseFileLock(); // Finally, the build is committed so finish putting this into the moved-from // state by clearing the runtimes pointer. runtimes_ = nullptr; return dest_path; } auto Runtimes::Builder::ReleaseFileLock() -> void { CARBON_CHECK(runtimes_ != nullptr); if (flock_.is_locked()) { std::filesystem::path dest_path = runtimes_->base_path() / dest_; CARBON_VLOG("PID {0} releasing lock on cache path: {1}", getpid(), dest_path); (void)lock_file_.WriteFileFromString(""); flock_ = {}; (void)std::move(lock_file_).Close(); } else { CARBON_CHECK(!lock_file_.is_valid()); } } auto Runtimes::Builder::Destroy() -> void { // If the runtimes are null, no in-flight build is owned so nothing to do. if (runtimes_ == nullptr) { CARBON_CHECK( !lock_file_.is_valid() && !flock_.is_locked() && !dir_.is_valid(), "Builder left in a partially cleared state!"); return; } // Otherwise we need to abandon an in-flight build. First release the lock. ReleaseFileLock(); // The rest of the cleanup is handled by the `RemovingDir` destructor. } } // namespace Carbon