runtimes_cache.cpp 20 KB

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