runtimes_cache.cpp 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514
  1. // Part of the Carbon Language project, under the Apache License v2.0 with LLVM
  2. // Exceptions. See /LICENSE for license information.
  3. // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
  4. #include "toolchain/driver/runtimes_cache.h"
  5. #include <algorithm>
  6. #include <chrono>
  7. #include <filesystem>
  8. #include <memory>
  9. #include <numeric>
  10. #include <optional>
  11. #include <string>
  12. #include <system_error>
  13. #include <utility>
  14. #include <variant>
  15. #include "common/filesystem.h"
  16. #include "common/version.h"
  17. #include "common/vlog.h"
  18. #include "llvm/ADT/ArrayRef.h"
  19. #include "llvm/ADT/ScopeExit.h"
  20. #include "llvm/ADT/StringExtras.h"
  21. #include "llvm/ADT/StringRef.h"
  22. #include "llvm/Support/FormatAdapters.h"
  23. #include "llvm/Support/Program.h"
  24. #include "llvm/Support/SHA256.h"
  25. namespace Carbon {
  26. auto Runtimes::Make(std::filesystem::path path, llvm::raw_ostream* vlog_stream)
  27. -> ErrorOr<Runtimes> {
  28. if (!path.is_absolute()) {
  29. return Error("Runtimes require an absolute path");
  30. }
  31. CARBON_ASSIGN_OR_RETURN(
  32. Filesystem::Dir dir,
  33. Filesystem::Cwd().OpenDir(path, Filesystem::OpenExisting));
  34. return Runtimes(std::move(path), std::move(dir), {}, {}, vlog_stream);
  35. }
  36. auto Runtimes::Destroy() -> void {
  37. // Release the lock on the runtimes and close the lock file.
  38. flock_ = {};
  39. auto close_result = std::move(lock_file_).Close();
  40. if (!close_result.ok()) {
  41. // Log and continue on close errors.
  42. CARBON_VLOG("Error closing lock file for runtimes '{0}': {1}", base_path_,
  43. close_result.error());
  44. }
  45. }
  46. auto Runtimes::Get(Component component) -> ErrorOr<std::filesystem::path> {
  47. std::filesystem::path path = base_path_ / ComponentPath(component);
  48. auto open_result =
  49. base_dir_.OpenDir(ComponentPath(component), Filesystem::OpenExisting);
  50. if (open_result.ok()) {
  51. return path;
  52. }
  53. return open_result.error().ToError();
  54. }
  55. auto Runtimes::Build(Component component)
  56. -> ErrorOr<std::variant<std::filesystem::path, Builder>> {
  57. return BuildImpl(component, BuildLockDeadline, BuildLockPollInterval);
  58. }
  59. auto Runtimes::BuildImpl(Component component, Filesystem::Duration deadline,
  60. Filesystem::Duration poll_interval)
  61. -> ErrorOr<std::variant<std::filesystem::path, Builder>> {
  62. // Try to get an existing resource directory first.
  63. auto existing_result = Get(component);
  64. if (existing_result.ok()) {
  65. return {*std::move(existing_result)};
  66. }
  67. // Otherwise, we will need to build the runtimes and commit them into this
  68. // directory once ready. Try and acquire an advisory lock to avoid redundant
  69. // computation.
  70. std::string_view component_path = ComponentPath(component);
  71. CARBON_ASSIGN_OR_RETURN(
  72. Filesystem::ReadWriteFile lock_file,
  73. base_dir_.OpenReadWrite(
  74. llvm::formatv(LockFileFormat, component_path).str(),
  75. Filesystem::OpenAlways, /*creation_mode=*/0700));
  76. CARBON_VLOG("PID {0} locking cache path: {1}\n", getpid(),
  77. base_path_ / component_path);
  78. Filesystem::FileLock flock;
  79. auto flock_result = lock_file.TryLock(Filesystem::FileLock::Exclusive,
  80. deadline, poll_interval);
  81. if (flock_result.ok()) {
  82. flock = *std::move(flock_result);
  83. CARBON_VLOG("Successfully locked cache path\n");
  84. // As a debugging aid, write our PID into the lock file when we
  85. // successfully acquire it. Ignore errors here though.
  86. (void)lock_file.WriteFileFromString(std::to_string(getpid()));
  87. } else if (!flock_result.error().would_block()) {
  88. // Some unexpected filesystem error, report that rather than trying to
  89. // continue.
  90. return std::move(flock_result).error();
  91. } else {
  92. CARBON_VLOG("Unable to lock cache path, held by: {1}\n",
  93. *lock_file.ReadFileToString());
  94. (void)std::move(lock_file).Close();
  95. }
  96. // See if another process has built the runtimes while we waited on the lock.
  97. // We do this even if we didn't successfully acquire the lock because we
  98. // ensure that a successful build atomically creates a viable directory.
  99. existing_result = Get(component);
  100. if (existing_result.ok()) {
  101. // Clear and close the lock file.
  102. (void)lock_file.WriteFileFromString("");
  103. flock = {};
  104. (void)std::move(lock_file).Close();
  105. return {*std::move(existing_result)};
  106. }
  107. // Whether we hold the lock file or not, we're going to now build these
  108. // runtimes. Create a temporary directory where we can do that safely
  109. // regardless of what else is happening.
  110. std::filesystem::path tmp_path =
  111. base_path_ / llvm::formatv(".{0}.tmp", component_path).str();
  112. CARBON_ASSIGN_OR_RETURN(Filesystem::RemovingDir tmp_dir,
  113. Filesystem::MakeTmpDirWithPrefix(tmp_path));
  114. return {Builder(*this, std::move(lock_file), std::move(flock),
  115. std::move(tmp_dir), component_path)};
  116. }
  117. auto Runtimes::Cache::FindXdgCachePath()
  118. -> std::optional<std::filesystem::path> {
  119. if (const char* xdg_cache_home = getenv("XDG_CACHE_HOME");
  120. xdg_cache_home != nullptr) {
  121. std::filesystem::path path = xdg_cache_home;
  122. if (path.is_absolute()) {
  123. CARBON_VLOG("Using '$XDG_CACHE_HOME' cache: {0}", path);
  124. return path;
  125. }
  126. }
  127. // Unable to use the standard environment variable. Try the designated
  128. // fallback of `$HOME/.cache`.
  129. const char* home = getenv("HOME");
  130. if (home == nullptr) {
  131. return std::nullopt;
  132. }
  133. std::filesystem::path path = home;
  134. if (!path.is_absolute()) {
  135. return std::nullopt;
  136. }
  137. path /= ".cache";
  138. CARBON_VLOG("Using '$HOME/.cache' cache: {0}", path);
  139. return path;
  140. }
  141. auto Runtimes::Cache::InitTmpSystemCache() -> ErrorOr<Success> {
  142. CARBON_ASSIGN_OR_RETURN(dir_owner_, Filesystem::MakeTmpDir());
  143. path_ = std::get<Filesystem::RemovingDir>(dir_owner_).abs_path();
  144. dir_ = std::get<Filesystem::RemovingDir>(dir_owner_);
  145. CARBON_VLOG("Using temporary cache: {0}", path_);
  146. return Success();
  147. }
  148. auto Runtimes::Cache::InitSystemCache(const InstallPaths& install)
  149. -> ErrorOr<Success> {
  150. constexpr llvm::StringLiteral CachePath = "carbon_runtimes";
  151. // If we have a digest to use as the cache key, save it and we can try to
  152. // use persistent caches.
  153. auto read_digest_result =
  154. Filesystem::Cwd().ReadFileToString(install.digest_path());
  155. if (!read_digest_result.ok()) {
  156. return InitTmpSystemCache();
  157. }
  158. cache_key_ = *std::move(read_digest_result);
  159. auto xdg_path_result = FindXdgCachePath();
  160. if (!xdg_path_result) {
  161. return InitTmpSystemCache();
  162. }
  163. // We have a candidate XDG-based cache path. Try to open that, and a
  164. // directory below it for Carbon's runtimes. Note that we don't error on a
  165. // missing directory, we fall through to using a temporary directory.
  166. auto open_result = Filesystem::Cwd().OpenDir(*xdg_path_result);
  167. if (!open_result.ok()) {
  168. if (!open_result.error().no_entity()) {
  169. // Some other unexpected error in the filesystem, propagate that.
  170. return std::move(open_result).error();
  171. }
  172. // Otherwise we fall back to a temporary system cache.
  173. return InitTmpSystemCache();
  174. }
  175. path_ = *std::move(xdg_path_result);
  176. // Now open a subdirectory of the cache for Carbon's usage. This will
  177. // create a subdirectory if one doesn't yet exist.
  178. path_ /= std::string_view(CachePath);
  179. CARBON_ASSIGN_OR_RETURN(
  180. dir_owner_, open_result->OpenDir(CachePath.str(), Filesystem::OpenAlways,
  181. /*creation_mode=*/0700));
  182. dir_ = std::get<Filesystem::Dir>(dir_owner_);
  183. // Ensure the directory has narrow permissions so runtimes can't be
  184. // overwritten.
  185. CARBON_ASSIGN_OR_RETURN(auto dir_stat, dir_.Stat());
  186. if (dir_stat.permissions() != 0700 || dir_stat.unix_uid() != geteuid()) {
  187. return Error(llvm::formatv(
  188. "Found runtimes cache path '{0}' with excessive permissions ({1}) "
  189. "or an invalid owning UID ({2})",
  190. path_, dir_stat.permissions(), dir_stat.unix_uid()));
  191. }
  192. return Success();
  193. }
  194. auto Runtimes::Cache::InitCachePath(const InstallPaths& install,
  195. std::filesystem::path cache_path)
  196. -> ErrorOr<Success> {
  197. auto read_digest_result =
  198. Filesystem::Cwd().ReadFileToString(install.digest_path());
  199. if (read_digest_result.ok()) {
  200. // If we have a digest to use as the cache key, save it and we can try to
  201. // use persistent caches.
  202. cache_key_ = *std::move(read_digest_result);
  203. } else {
  204. // Without a digest, use the path itself as the key.
  205. cache_key_ = cache_path.string();
  206. }
  207. CARBON_ASSIGN_OR_RETURN(dir_owner_, Filesystem::Cwd().OpenDir(cache_path));
  208. dir_ = std::get<Filesystem::Dir>(dir_owner_);
  209. path_ = std::move(cache_path);
  210. CARBON_VLOG("Using custom cache: {0}", path_);
  211. return Success();
  212. }
  213. auto Runtimes::Cache::Lookup(const Features& features) -> ErrorOr<Runtimes> {
  214. // Compute the hash of the features. We'll use this to build the subdirectory
  215. // within the cache.
  216. llvm::SHA256 entry_hasher;
  217. // First incorporate our cache key that comes from the installation's digest.
  218. // This ensures we don't share a cache entry with any other Carbon
  219. // installations using different inputs.
  220. entry_hasher.update(cache_key_);
  221. // Then incorporate the specific features that are enabled in this entry.
  222. entry_hasher.update(features.target);
  223. std::array<uint8_t, 32> entry_digest = entry_hasher.final();
  224. std::filesystem::path entry_path =
  225. llvm::formatv("runtimes-{0}-{1}", Version::String,
  226. llvm::toHex(entry_digest, /*LowerCase=*/true))
  227. .str();
  228. Filesystem::Dir entry_dir;
  229. auto open_result = dir_.OpenDir(entry_path, Filesystem::OpenExisting);
  230. if (open_result.ok()) {
  231. entry_dir = *std::move(open_result);
  232. } else {
  233. if (!open_result.error().no_entity()) {
  234. return std::move(open_result).error();
  235. }
  236. // We're going to potentially create a new set of runtimes, prune the
  237. // existing runtimes first to provide a bound on the total size of runtimes.
  238. PruneStaleRuntimes(entry_path);
  239. // Now we can create or open, we don't care if a racing process created the
  240. // same runtime directory.
  241. CARBON_ASSIGN_OR_RETURN(entry_dir,
  242. dir_.OpenDir(entry_path, Filesystem::OpenAlways));
  243. }
  244. CARBON_ASSIGN_OR_RETURN(
  245. auto lock_file, entry_dir.OpenWriteOnly(".lock", Filesystem::OpenAlways));
  246. CARBON_RETURN_IF_ERROR(lock_file.UpdateTimes());
  247. CARBON_ASSIGN_OR_RETURN(
  248. Filesystem::FileLock flock,
  249. lock_file.TryLock(Filesystem::FileLock::Shared, RuntimesLockDeadline,
  250. RuntimesLockPollInterval));
  251. return Runtimes(path_ / entry_path, std::move(entry_dir),
  252. std::move(lock_file), std::move(flock), vlog_stream_);
  253. }
  254. auto Runtimes::Cache::ComputeEntryAges(
  255. llvm::SmallVector<std::filesystem::path> entry_paths)
  256. -> llvm::SmallVector<Entry> {
  257. llvm::SmallVector<Entry> entries;
  258. Filesystem::TimePoint now = Filesystem::Clock::now();
  259. for (auto& path : entry_paths) {
  260. // We use the `mtime` from the lock file in the directory rather than the
  261. // directory itself to avoid any oddities with `mtime` on directories.
  262. //
  263. // Note that we also ignore errors here as if we can't read the stamp file
  264. // we will pick an arbitrary old time stamp, and we want pruning to be
  265. // maximally resilient to partially deleted or corrupted caches in order to
  266. // prune them back into a healthy state.
  267. auto stat_result = dir_.Lstat(path / ".lock");
  268. auto mtime = stat_result.ok()
  269. ? stat_result->mtime()
  270. : Filesystem::TimePoint(Filesystem::Duration(0));
  271. entries.push_back({.path = std::move(path), .age = now - mtime});
  272. }
  273. return entries;
  274. }
  275. auto Runtimes::Cache::PruneStaleRuntimes(
  276. const std::filesystem::path& new_entry_path) -> void {
  277. llvm::SmallVector<std::filesystem::path> dir_entries;
  278. llvm::SmallVector<std::filesystem::path> non_dir_entries;
  279. auto read_result = dir_.AppendEntriesIf(
  280. dir_entries, non_dir_entries,
  281. [](llvm::StringRef name) { return name.starts_with("runtimes-"); });
  282. if (!read_result.ok()) {
  283. CARBON_VLOG("Unable to read cache directory to prune stale entries: {0}",
  284. read_result.error());
  285. return;
  286. }
  287. // Directly attempt to remove non-directory and bad directory entries.
  288. for (const auto& name : non_dir_entries) {
  289. CARBON_VLOG("Unlinking non-directory entry '{0}'", name);
  290. auto result = dir_.Unlink(name);
  291. if (!result.ok()) {
  292. CARBON_VLOG("Error unlinking non-directory entry '{0}': {1}", name,
  293. result.error());
  294. }
  295. }
  296. // If we only have a small number of entries, no need to prune.
  297. if (dir_entries.size() < MinNumEntries) {
  298. return;
  299. }
  300. llvm::SmallVector<Entry> entries = ComputeEntryAges(std::move(dir_entries));
  301. auto rm_entry = [&](const std::filesystem::path& entry_name) {
  302. // Note that we don't propagate errors here because we want to prune as much
  303. // as possible. We do log them.
  304. CARBON_VLOG("Removing cache entry '{0}'", entry_name);
  305. auto rm_result = dir_.Rmtree(entry_name);
  306. if (!rm_result.ok() && !rm_result.error().no_entity()) {
  307. CARBON_VLOG("Unable to remove old runtimes '{0}': {1}", entry_name,
  308. rm_result.error());
  309. return false;
  310. }
  311. return true;
  312. };
  313. // Remove entries older than our max first. We don't need to check for locking
  314. // or other issues here given the age.
  315. llvm::erase_if(entries, [&](const Entry& entry) {
  316. return entry.age > MaxEntryAge && rm_entry(entry.path);
  317. });
  318. // Sort the entries so that the oldest is first.
  319. llvm::sort(entries, [](const Entry& lhs, const Entry& rhs) {
  320. return lhs.age > rhs.age;
  321. });
  322. // Now try to get the number of entries below our max target by removing the
  323. // least-recently used entries that are either more than our max locked age or
  324. // unlocked.
  325. auto rm_unlocked_entry = [&](const std::filesystem::path& name,
  326. Filesystem::Duration age) {
  327. // Past a certain age, bypass the locking for efficiency and to avoid
  328. // retaining entries with stale locks.
  329. if (age > MaxLockedEntryAge) {
  330. return rm_entry(name);
  331. }
  332. CARBON_VLOG("Attempting to lock cache entry '{0}'", name);
  333. auto lock_file_open_result =
  334. dir_.OpenReadOnly(name / ".lock", Filesystem::OpenAlways);
  335. if (!lock_file_open_result.ok()) {
  336. if (lock_file_open_result.error().no_entity() ||
  337. lock_file_open_result.error().not_dir()) {
  338. // The only way these failures should be possible is if something
  339. // removed the cache directory between our read above and here. Assume
  340. // the entry is gone and continue.
  341. return true;
  342. }
  343. // For other errors, assume locked.
  344. CARBON_VLOG("Error opening lock file for cache entry '{0}': {1}", name,
  345. lock_file_open_result.error());
  346. return false;
  347. }
  348. Filesystem::ReadFile lock_file = *std::move(lock_file_open_result);
  349. auto lock_result =
  350. lock_file.TryLock(Filesystem::FileLock::Exclusive, RuntimesLockDeadline,
  351. RuntimesLockPollInterval);
  352. if (!lock_result.ok()) {
  353. // The normal case is when locking would block, log anything else.
  354. if (!lock_result.error().would_block()) {
  355. CARBON_VLOG("Error locking cache entry '{0}': {1}", name,
  356. lock_result.error());
  357. }
  358. // However, don't try to remove it as we didn't acquire the lock.
  359. return false;
  360. }
  361. // The lock is held, remove the entry.
  362. return rm_entry(name);
  363. };
  364. int num_entries = entries.size();
  365. for (const auto& [name, age] : entries) {
  366. if (num_entries < MaxNumEntries) {
  367. break;
  368. }
  369. // Don't prune the currently being built entry. We should only reach here
  370. // when some other process created this entry in a race, and we don't want
  371. // to remove it or trigger rebuilds.
  372. if (name == new_entry_path) {
  373. continue;
  374. }
  375. if (rm_unlocked_entry(name, age)) {
  376. --num_entries;
  377. }
  378. }
  379. if (num_entries >= MaxNumEntries) {
  380. CARBON_VLOG(
  381. "Unable to prune cache to our target size due to held locks on recent "
  382. "cache entries or removal errors, leaving {0} entries in the cache",
  383. num_entries);
  384. }
  385. }
  386. auto Runtimes::Builder::Commit() && -> ErrorOr<std::filesystem::path> {
  387. std::filesystem::path dest_path = runtimes_->base_path() / dest_;
  388. // First, try to do the atomic commit of the built runtimes into the final
  389. // location.
  390. CARBON_CHECK(dir_.abs_path().parent_path() == runtimes_->base_path(),
  391. "Building a temporary directory '{0}' that is not in the "
  392. "runtimes tree '{1}'",
  393. dir_.abs_path(), runtimes_->base_path());
  394. auto rename_result = runtimes_->base_dir().Rename(
  395. dir_.abs_path().filename(), runtimes_->base_dir(), dest_);
  396. // If the rename was successful, then we don't need to remove anything so
  397. // release that state.
  398. if (rename_result.ok()) {
  399. std::move(dir_).Release();
  400. } else if (rename_result.error().not_empty()) {
  401. // Some other runtimes were successfully committed before ours, so we want
  402. // to discard ours. We report errors cleaning up here as we don't want to
  403. // pollute the filesystem excessively.
  404. //
  405. // TODO: Consider instead being more resilient to errors here and just log
  406. // them.
  407. CARBON_VLOG("PID {0} found racily built runtimes in cache path: {1}",
  408. getpid(), dest_path);
  409. CARBON_RETURN_IF_ERROR(std::move(dir_).Remove());
  410. } else {
  411. // An unexpected error occurred, propagate it and let the normal cleanup
  412. // occur.
  413. //
  414. // TODO: It's possible we need to handle `EBUSY` here, likely by ensuring it
  415. // is the *destination* that is busy and an existing, valid directory built
  416. // concurrently.
  417. return std::move(rename_result).error();
  418. }
  419. // Now that we've got a final path in place successfully, clear the flock if
  420. // it is currently held.
  421. ReleaseFileLock();
  422. // Finally, the build is committed so finish putting this into the moved-from
  423. // state by clearing the runtimes pointer.
  424. runtimes_ = nullptr;
  425. return dest_path;
  426. }
  427. auto Runtimes::Builder::ReleaseFileLock() -> void {
  428. CARBON_CHECK(runtimes_ != nullptr);
  429. if (flock_.is_locked()) {
  430. std::filesystem::path dest_path = runtimes_->base_path() / dest_;
  431. CARBON_VLOG("PID {0} releasing lock on cache path: {1}", getpid(),
  432. dest_path);
  433. (void)lock_file_.WriteFileFromString("");
  434. flock_ = {};
  435. (void)std::move(lock_file_).Close();
  436. } else {
  437. CARBON_CHECK(!lock_file_.is_valid());
  438. }
  439. }
  440. auto Runtimes::Builder::Destroy() -> void {
  441. // If the runtimes are null, no in-flight build is owned so nothing to do.
  442. if (runtimes_ == nullptr) {
  443. CARBON_CHECK(
  444. !lock_file_.is_valid() && !flock_.is_locked() && !dir_.is_valid(),
  445. "Builder left in a partially cleared state!");
  446. return;
  447. }
  448. // Otherwise we need to abandon an in-flight build. First release the lock.
  449. ReleaseFileLock();
  450. // The rest of the cleanup is handled by the `RemovingDir` destructor.
  451. }
  452. } // namespace Carbon