| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521 |
- // 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 <algorithm>
- #include <chrono>
- #include <filesystem>
- #include <memory>
- #include <numeric>
- #include <optional>
- #include <string>
- #include <system_error>
- #include <utility>
- #include <variant>
- #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<Runtimes> {
- 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<Runtimes> {
- 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> {
- 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<std::variant<std::filesystem::path, Builder>> {
- return BuildImpl(component, BuildLockDeadline, BuildLockPollInterval);
- }
- auto Runtimes::Remove(Component component) -> ErrorOr<Success> {
- CARBON_RETURN_IF_ERROR(base_dir_.Rmtree(ComponentPath(component)));
- return Success();
- }
- auto Runtimes::BuildImpl(Component component, Filesystem::Duration deadline,
- Filesystem::Duration poll_interval)
- -> ErrorOr<std::variant<std::filesystem::path, Builder>> {
- // 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<std::filesystem::path> {
- 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<Success> {
- CARBON_ASSIGN_OR_RETURN(dir_owner_, Filesystem::MakeTmpDir());
- path_ = std::get<Filesystem::RemovingDir>(dir_owner_).path();
- dir_ = std::get<Filesystem::RemovingDir>(dir_owner_);
- CARBON_VLOG("Using temporary cache: {0}", path_);
- return Success();
- }
- auto Runtimes::Cache::InitSystemCache(const InstallPaths& install)
- -> ErrorOr<Success> {
- 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<Filesystem::Dir>(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<Success> {
- 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<Filesystem::Dir>(dir_owner_);
- path_ = std::move(cache_path);
- CARBON_VLOG("Using custom cache: {0}", path_);
- return Success();
- }
- auto Runtimes::Cache::Lookup(const Features& features) -> ErrorOr<Runtimes> {
- // 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<uint8_t, 32> 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<std::filesystem::path> entry_paths)
- -> llvm::SmallVector<Entry> {
- llvm::SmallVector<Entry> 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<std::filesystem::path> dir_entries;
- llvm::SmallVector<std::filesystem::path> 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<Entry> 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> {
- 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
|