mirror of
https://github.com/carbon-language/carbon-lang.git
synced 2026-09-25 10:50:12 +01:00
This layer allows runtimes to be built on-demand but cached in a consistent and re-usable location on the system. It handles careful filesystem operations to ensure consistency even in the face of multiple versions and build configurations. This addresses a number of TODOs from the initial runtimes building on-demand, and sets the stage to scale up to more runtimes. This doesn't switch on-demand runtimes to be on by default, I wanted to wait and make that change as a separate step. --------- Co-authored-by: Geoff Romer <gromer@google.com>
719 lines
32 KiB
C++
719 lines
32 KiB
C++
// 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 <gmock/gmock.h>
|
|
#include <gtest/gtest.h>
|
|
|
|
#include <chrono>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <limits>
|
|
#include <mutex>
|
|
#include <ratio>
|
|
#include <string>
|
|
#include <thread>
|
|
#include <utility>
|
|
#include <variant>
|
|
|
|
#include "common/check.h"
|
|
#include "common/error_test_helpers.h"
|
|
#include "common/filesystem.h"
|
|
#include "common/ostream.h"
|
|
#include "common/raw_string_ostream.h"
|
|
#include "common/version.h"
|
|
#include "llvm/ADT/ScopeExit.h"
|
|
#include "llvm/Support/SHA256.h"
|
|
#include "testing/base/capture_std_streams.h"
|
|
#include "testing/base/file_helpers.h"
|
|
#include "testing/base/global_exe_path.h"
|
|
|
|
namespace Carbon {
|
|
|
|
class RuntimesTestPeer {
|
|
public:
|
|
static auto LockFilePath(Runtimes::Component component) -> std::string {
|
|
return llvm::formatv(Runtimes::LockFileFormat,
|
|
Runtimes::ComponentPath(component))
|
|
.str();
|
|
}
|
|
|
|
static auto BuildImpl(Runtimes& runtimes, Runtimes::Component component,
|
|
Filesystem::Duration deadline,
|
|
Filesystem::Duration poll_interval)
|
|
-> ErrorOr<std::variant<std::filesystem::path, Runtimes::Builder>> {
|
|
return runtimes.BuildImpl(component, deadline, poll_interval);
|
|
}
|
|
|
|
static auto CacheMinNumEntries() -> int {
|
|
return Runtimes::Cache::MinNumEntries;
|
|
}
|
|
static auto CacheMaxNumEntries() -> int {
|
|
return Runtimes::Cache::MaxNumEntries;
|
|
}
|
|
};
|
|
|
|
namespace {
|
|
|
|
using ::testing::_;
|
|
using ::testing::AllOf;
|
|
using ::testing::AnyOf;
|
|
using ::testing::Eq;
|
|
using ::testing::Gt;
|
|
using Testing::IsError;
|
|
using Testing::IsSuccess;
|
|
using ::testing::Lt;
|
|
using ::testing::Ne;
|
|
using ::testing::Not;
|
|
using ::testing::StartsWith;
|
|
using ::testing::StrEq;
|
|
using ::testing::VariantWith;
|
|
|
|
class RuntimesCacheTest : public ::testing::Test {
|
|
public:
|
|
RuntimesCacheTest()
|
|
: cache_(*Runtimes::Cache::MakeCustom(install_, tmp_dir_.abs_path())) {}
|
|
|
|
auto LookupNRuntimes(int n) -> llvm::SmallVector<Runtimes> {
|
|
llvm::SmallVector<Runtimes> runtimes;
|
|
for (int i : llvm::seq(n)) {
|
|
runtimes.push_back(*cache_.Lookup(
|
|
{.target = llvm::formatv("aarch64-unknown-unknown{0}", i).str()}));
|
|
}
|
|
return runtimes;
|
|
}
|
|
|
|
InstallPaths install_ =
|
|
InstallPaths::MakeForBazelRunfiles(Testing::GetExePath());
|
|
Filesystem::RemovingDir tmp_dir_ = *Filesystem::MakeTmpDir();
|
|
std::string cache_key_ = "test cache";
|
|
Runtimes::Cache cache_;
|
|
};
|
|
|
|
TEST_F(RuntimesCacheTest, BuildSystemCache) {
|
|
// Create an install with a missing digest.
|
|
auto bad_install_dir = *tmp_dir_.CreateDirectories("bad_install/lib/carbon");
|
|
bad_install_dir.WriteFileFromString("carbon_install.txt", "no digest")
|
|
.Check();
|
|
InstallPaths bad_install =
|
|
InstallPaths::Make((tmp_dir_.abs_path() / "bad_install").native());
|
|
|
|
// Create directories to use in various environment variables.
|
|
auto xdg_dir = *tmp_dir_.CreateDirectories("xdg_cache_home");
|
|
std::filesystem::path xdg_path = tmp_dir_.abs_path() / "xdg_cache_home";
|
|
auto test_home = *tmp_dir_.CreateDirectories("test_home");
|
|
std::filesystem::path home_path = tmp_dir_.abs_path() / "test_home";
|
|
auto home_cache_dir = *test_home.CreateDirectories(".cache");
|
|
std::filesystem::path home_cache_path = home_path / ".cache";
|
|
|
|
// Save the environment variables we'll override for testing and restore them
|
|
// afterward to avoid test-to-test oddities.
|
|
constexpr const char* XdgCacheEnv = "XDG_CACHE_HOME";
|
|
constexpr const char* HomeEnv = "HOME";
|
|
const char* orig_xdg_cache = getenv(XdgCacheEnv);
|
|
const char* orig_home = getenv(HomeEnv);
|
|
auto restore_env = llvm::make_scope_exit([&] {
|
|
for (const auto [env, orig] : {std::pair{XdgCacheEnv, orig_xdg_cache},
|
|
std::pair{HomeEnv, orig_home}}) {
|
|
if (orig) {
|
|
setenv(env, orig, /*overwrite*/ true);
|
|
} else {
|
|
unsetenv(env);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Begin testing the basic logic of selecting different roots for the cache.
|
|
setenv(XdgCacheEnv, xdg_path.c_str(), /*overwrite*/ true);
|
|
setenv(HomeEnv, home_path.c_str(), /*overwrite*/ true);
|
|
|
|
// First check that even with all the environment set up, when we don't have a
|
|
// digest file available, we bypass those options and use a temporary cache
|
|
// path. This is the only safe approach as without a digest file we can't
|
|
// track whether it is correct to reuse a persistently cached entry.
|
|
auto result = Runtimes::Cache::MakeSystem(bad_install);
|
|
ASSERT_THAT(result, IsSuccess(_));
|
|
EXPECT_THAT(result->path(), Not(StartsWith(home_cache_path)));
|
|
EXPECT_THAT(result->path(), Not(StartsWith(xdg_path)));
|
|
|
|
// Once we have a digest, the main XDG cache logic should work.
|
|
result = Runtimes::Cache::MakeSystem(install_);
|
|
ASSERT_THAT(result, IsSuccess(_));
|
|
EXPECT_THAT(result->path(), StartsWith(xdg_path));
|
|
// Destruction shouldn't remove system cache directories.
|
|
result = Error("nothing");
|
|
EXPECT_TRUE(*tmp_dir_.Access("xdg_cache_home"));
|
|
|
|
// Remove the XDG cache directory, but leave the environment set. We want to
|
|
// be robust against this, but it isn't important *how* the fallback occurs,
|
|
// it could go to `$HOME/.cache`, or to a temporary directory.
|
|
tmp_dir_.Rmtree("xdg_cache_home").Check();
|
|
EXPECT_THAT(Runtimes::Cache::MakeSystem(install_), IsSuccess(_));
|
|
|
|
// Set the XDG environment to the empty string which should trigger using the
|
|
// home directory.
|
|
setenv(XdgCacheEnv, "", /*overwrite*/ true);
|
|
result = Runtimes::Cache::MakeSystem(install_);
|
|
ASSERT_THAT(result, IsSuccess(_));
|
|
EXPECT_THAT(result->path(), StartsWith(home_cache_path));
|
|
// Destruction shouldn't remove system cache directories.
|
|
result = Error("nothing");
|
|
EXPECT_TRUE(*tmp_dir_.Access("test_home"));
|
|
EXPECT_TRUE(*test_home.Access(".cache"));
|
|
|
|
// Same as with an empty string, but with a relative path instead.
|
|
setenv(XdgCacheEnv, "relative/cache/home", /*overwrite*/ true);
|
|
result = Runtimes::Cache::MakeSystem(install_);
|
|
ASSERT_THAT(result, IsSuccess(_));
|
|
EXPECT_THAT(result->path(), StartsWith(home_cache_path));
|
|
// Destruction shouldn't remove system cache directories.
|
|
result = Error("nothing");
|
|
EXPECT_TRUE(*tmp_dir_.Access("test_home"));
|
|
EXPECT_TRUE(*test_home.Access(".cache"));
|
|
|
|
// Same as with an empty string, but this time with an unset environment
|
|
// variable.
|
|
unsetenv(XdgCacheEnv);
|
|
result = Runtimes::Cache::MakeSystem(install_);
|
|
ASSERT_THAT(result, IsSuccess(_));
|
|
EXPECT_THAT(result->path(), StartsWith(home_cache_path));
|
|
// Destruction shouldn't remove system cache directories.
|
|
result = Error("nothing");
|
|
EXPECT_TRUE(*tmp_dir_.Access("test_home"));
|
|
EXPECT_TRUE(*test_home.Access(".cache"));
|
|
|
|
// Now check a bunch of different failure modes for the home directory
|
|
// fallback. These should all end up creating temporary directories which
|
|
// we'll test functionally at the end.
|
|
setenv(HomeEnv, "", /*overwrite*/ true);
|
|
EXPECT_THAT(Runtimes::Cache::MakeSystem(install_), IsSuccess(_));
|
|
setenv(HomeEnv, "relative/home", /*overwrite*/ true);
|
|
EXPECT_THAT(Runtimes::Cache::MakeSystem(install_), IsSuccess(_));
|
|
|
|
// Correct the path and make sure it works again.
|
|
setenv(HomeEnv, home_path.c_str(), /*overwrite*/ true);
|
|
result = Runtimes::Cache::MakeSystem(install_);
|
|
ASSERT_THAT(result, IsSuccess(_));
|
|
EXPECT_THAT(result->path(), StartsWith(home_cache_path));
|
|
|
|
// Now try removing directories around home.
|
|
test_home.Rmtree(".cache").Check();
|
|
EXPECT_THAT(Runtimes::Cache::MakeSystem(install_), IsSuccess(_));
|
|
tmp_dir_.Rmtree("test_home").Check();
|
|
EXPECT_THAT(Runtimes::Cache::MakeSystem(install_), IsSuccess(_));
|
|
|
|
// Finally, double check that these temporary caches still produce a writable
|
|
// directory.
|
|
result = Runtimes::Cache::MakeSystem(install_);
|
|
ASSERT_THAT(result, IsSuccess(_));
|
|
EXPECT_THAT(result->path(), Not(StartsWith(home_cache_path)));
|
|
EXPECT_THAT(result->path(), Not(StartsWith(xdg_path)));
|
|
ASSERT_THAT(Filesystem::Cwd().WriteFileFromString(
|
|
result->path() / "test_file", "test"),
|
|
IsSuccess(_));
|
|
ASSERT_THAT(Filesystem::Cwd().ReadFileToString(result->path() / "test_file"),
|
|
IsSuccess(StrEq("test")));
|
|
}
|
|
|
|
TEST_F(RuntimesCacheTest, BasicBuild) {
|
|
llvm::SmallVector<std::string> targets = {"aarch64-unknown-unknown",
|
|
"x86_64-unknown-unknown"};
|
|
llvm::SmallVector<std::filesystem::path> built_runtimes_paths;
|
|
|
|
for (const std::string& target : targets) {
|
|
SCOPED_TRACE(target);
|
|
auto lookup_result = cache_.Lookup({.target = target});
|
|
ASSERT_THAT(lookup_result, IsSuccess(_));
|
|
auto runtimes = *std::move(lookup_result);
|
|
|
|
auto build_result = runtimes.Build(Runtimes::ClangResourceDir);
|
|
ASSERT_THAT(build_result, IsSuccess(VariantWith<Runtimes::Builder>(_)));
|
|
auto builder = std::get<Runtimes::Builder>(*std::move(build_result));
|
|
EXPECT_TRUE(builder.path().is_absolute()) << builder.path();
|
|
|
|
// Create a file as our "runtime".
|
|
builder.dir().WriteFileFromString("runtime_file", target).Check();
|
|
// Make sure the builder's path finds this file.
|
|
EXPECT_THAT(
|
|
Filesystem::Cwd().ReadFileToString(builder.path() / "runtime_file"),
|
|
IsSuccess(StrEq(target)));
|
|
|
|
auto commit_result = std::move(builder).Commit();
|
|
ASSERT_THAT(commit_result, IsSuccess(_));
|
|
std::filesystem::path clang_runtimes_path = *std::move(commit_result);
|
|
|
|
EXPECT_THAT(
|
|
runtimes.Build(Runtimes::ClangResourceDir),
|
|
IsSuccess(VariantWith<std::filesystem::path>(Eq(clang_runtimes_path))));
|
|
built_runtimes_paths.push_back(clang_runtimes_path);
|
|
}
|
|
|
|
for (const auto& [target, built_runtimes_path] :
|
|
llvm::zip(targets, built_runtimes_paths)) {
|
|
SCOPED_TRACE(target);
|
|
auto lookup_result = cache_.Lookup({.target = target});
|
|
ASSERT_THAT(lookup_result, IsSuccess(_));
|
|
auto runtimes = *std::move(lookup_result);
|
|
|
|
EXPECT_THAT(
|
|
runtimes.Build(Runtimes::ClangResourceDir),
|
|
IsSuccess(VariantWith<std::filesystem::path>(Eq(built_runtimes_path))));
|
|
}
|
|
}
|
|
|
|
TEST_F(RuntimesCacheTest, DifferentKeys) {
|
|
const std::string target = "aarch64-unknown-unknown";
|
|
auto runtimes1 = *cache_.Lookup({.target = target});
|
|
|
|
// Build a second cache with a different key but pointing at the same
|
|
// directory and target to simulate two versions or builds of the Carbon
|
|
// toolchain.
|
|
auto custom_install_dir =
|
|
*tmp_dir_.CreateDirectories("custom_install/lib/carbon");
|
|
custom_install_dir.WriteFileFromString("carbon_install.txt", "diff digest")
|
|
.Check();
|
|
custom_install_dir.WriteFileFromString("install_digest.txt", "abcd").Check();
|
|
InstallPaths install2 =
|
|
InstallPaths::Make((tmp_dir_.abs_path() / "custom_install").native());
|
|
auto cache2 = *Runtimes::Cache::MakeCustom(install2, tmp_dir_.abs_path());
|
|
auto runtimes2 = *cache2.Lookup({.target = target});
|
|
|
|
// The parent paths of these runtimes should be the same.
|
|
EXPECT_THAT(runtimes1.base_path().parent_path(),
|
|
Eq(runtimes2.base_path().parent_path()));
|
|
|
|
// But the base paths for these two runtimes should differ due to cache key
|
|
// differences.
|
|
EXPECT_THAT(runtimes1.base_path(), Ne(runtimes2.base_path()));
|
|
}
|
|
|
|
TEST_F(RuntimesCacheTest, ConcurrentBuilds) {
|
|
const std::string target = "aarch64-unknown-unknown";
|
|
auto runtimes1 = *cache_.Lookup({.target = target});
|
|
|
|
// Build a second cache and runtimes pointing at the same directory and target
|
|
// to simulate concurrent processes.
|
|
auto cache2 = *Runtimes::Cache::MakeCustom(install_, tmp_dir_.abs_path());
|
|
auto runtimes2 = *cache2.Lookup({.target = target});
|
|
|
|
// Start the first build, this will lock the directory.
|
|
auto build_result1 = runtimes1.Build(Runtimes::ClangResourceDir);
|
|
ASSERT_THAT(build_result1, IsSuccess(VariantWith<Runtimes::Builder>(_)));
|
|
auto builder1 = std::get<Runtimes::Builder>(*std::move(build_result1));
|
|
EXPECT_THAT(builder1.dir().WriteFileFromString("runtime_file", "build1"),
|
|
IsSuccess(_));
|
|
|
|
// Start the second build in a separate thread so that it can block while we
|
|
// finish the first build. The only result we'll need at the end is the built
|
|
// path.
|
|
std::filesystem::path build2_path;
|
|
auto build2_lambda = [&build2_path, &runtimes2] {
|
|
// Typically building here will try to acquire the same file lock acquired
|
|
// with the first build. However, the file locking is always _advisory_ and
|
|
// may fail. As a consequence we can't make assumptions about whether this
|
|
// blocks or not.
|
|
auto build_result2 = runtimes2.Build(Runtimes::ClangResourceDir);
|
|
ASSERT_THAT(build_result2, IsSuccess(_));
|
|
if (std::holds_alternative<std::filesystem::path>(*build_result2)) {
|
|
// In the common case, we blocked on a file lock and find the first built
|
|
// result directly. Save it.
|
|
build2_path = std::get<std::filesystem::path>(*std::move(build_result2));
|
|
} else {
|
|
// In rare cases, the initial build will fail to acquire the file lock.
|
|
// The entire build process is designed specifically to be resilient to
|
|
// that so we should still succeed, but now we need to handle building in
|
|
// this thread as well. Note that a true failure here may only
|
|
// show up intermittently.
|
|
auto builder2 = std::get<Runtimes::Builder>(*std::move(build_result2));
|
|
builder2.dir().WriteFileFromString("runtime_file", "build2").Check();
|
|
auto commit2_result = std::move(builder2).Commit();
|
|
ASSERT_THAT(commit2_result, IsSuccess(_));
|
|
build2_path = *std::move(commit2_result);
|
|
}
|
|
};
|
|
std::thread build2_thread(build2_lambda);
|
|
// Use a scoped join to avoid leaking the thread as some platforms don't have
|
|
// `std::jthread`.
|
|
auto scoped_join =
|
|
llvm::make_scope_exit([&build2_thread] { build2_thread.join(); });
|
|
|
|
// Commit the first built runtime.
|
|
auto commit_result = std::move(builder1).Commit();
|
|
ASSERT_THAT(commit_result, IsSuccess(_));
|
|
std::filesystem::path build1_path = *std::move(commit_result);
|
|
|
|
// Even though there may be is another thread running, we should now get
|
|
// non-blocking access directly to the built runtime.
|
|
EXPECT_THAT(runtimes1.Build(Runtimes::ClangResourceDir),
|
|
IsSuccess(VariantWith<std::filesystem::path>(Eq(build1_path))));
|
|
|
|
// Now join the second cache's build thread to ensure it completes and verify
|
|
// that it produces the same path fully-built path.
|
|
build2_thread.join();
|
|
scoped_join.release();
|
|
EXPECT_THAT(build2_path, Eq(build1_path));
|
|
|
|
// Note that we don't know which build actually ended up committed here so
|
|
// accept either. The first one is much more common, but in rare cases it will
|
|
// fail to acquire its file lock and we will have racing builds. In that case
|
|
// the second build may commit first.
|
|
EXPECT_THAT(*Filesystem::Cwd().ReadFileToString(build1_path / "runtime_file"),
|
|
AnyOf(StrEq("build1"), StrEq("build2")));
|
|
}
|
|
|
|
TEST_F(RuntimesCacheTest, ConcurrentBuildsWithFailedLocking) {
|
|
// This test is very similar to `ConcurrentBuild` in terms of what can happen.
|
|
// But here, we intentionally subvert the file locking and even us
|
|
// synchronization to maximize the chance of racing commits.
|
|
//
|
|
// The goal here is to do two things:
|
|
// 1) Provide more direct stress testing of lock-file-failure modes and racing
|
|
// commits to catch any consistent bugs that emerge.
|
|
// 2) Ensure that a removed lock file specifically is handled gracefully, both
|
|
// by a build with the file open and locked, and by a racing build.
|
|
const std::string target = "aarch64-unknown-unknown";
|
|
auto runtimes1 = *cache_.Lookup({.target = target});
|
|
|
|
// Build a second cache and runtimes pointing at the same directory and target
|
|
// to simulate concurrent processes.
|
|
auto cache2 = *Runtimes::Cache::MakeCustom(install_, tmp_dir_.abs_path());
|
|
auto runtimes2 = *cache2.Lookup({.target = target});
|
|
|
|
// Start the first build, this will lock the directory.
|
|
auto build_result1 = runtimes1.Build(Runtimes::ClangResourceDir);
|
|
ASSERT_THAT(build_result1, IsSuccess(VariantWith<Runtimes::Builder>(_)));
|
|
auto builder1 = std::get<Runtimes::Builder>(*std::move(build_result1));
|
|
builder1.dir().WriteFileFromString("runtime_file", "build1").Check();
|
|
|
|
// Now sneakily remove the lock file from the runtimes directory in the cache.
|
|
// This is something that could happen, for example from temporary directories
|
|
// being cleaned. The cache should be resilient against this and it gives us a
|
|
// good way to have two racing builds of the same directory.
|
|
std::filesystem::path lock_file_path =
|
|
RuntimesTestPeer::LockFilePath(Runtimes::ClangResourceDir);
|
|
ASSERT_THAT(runtimes1.base_dir().Unlink(lock_file_path), IsSuccess(_));
|
|
|
|
// We will synchronize with the thread to ensure we _actually_ have two
|
|
// parallel builds rather than accidentally having a fully serial execution.
|
|
std::mutex m;
|
|
std::condition_variable cv;
|
|
bool build_started = false;
|
|
|
|
// Start the second build in a separate thread. The only result we'll need at
|
|
// the end is the built path.
|
|
std::filesystem::path build2_path;
|
|
auto build2_lambda = [&build2_path, &runtimes2, target, &m, &cv,
|
|
&build_started] {
|
|
auto build_result2 = runtimes2.Build(Runtimes::ClangResourceDir);
|
|
ASSERT_THAT(build_result2, IsSuccess(VariantWith<Runtimes::Builder>(_)));
|
|
auto builder2 = std::get<Runtimes::Builder>(*std::move(build_result2));
|
|
builder2.dir().WriteFileFromString("runtime_file", "build2").Check();
|
|
|
|
// Notify the first thread to commit its build and concurrently commit this
|
|
// built runtime. The goal is to get as close as we can to having these
|
|
// commits actually race so that a failure in that mode would emerge as a
|
|
// flake of the test. None of this is providing correctness.
|
|
{
|
|
std::unique_lock lock(m);
|
|
build_started = true;
|
|
cv.notify_one();
|
|
}
|
|
auto commit_result = std::move(builder2).Commit();
|
|
ASSERT_THAT(commit_result, IsSuccess(_));
|
|
build2_path = *std::move(commit_result);
|
|
|
|
// Even though there may be another thread running, and even holding a lock
|
|
// file, we should now get non-blocking access directly to the built
|
|
// runtime. This is mostly added for completeness, a held lock is more
|
|
// directly tested in `CurrentBuildsLockTimeout`.
|
|
EXPECT_THAT(runtimes2.Build(Runtimes::ClangResourceDir),
|
|
IsSuccess(VariantWith<std::filesystem::path>(Eq(build2_path))));
|
|
};
|
|
std::thread build2_thread(build2_lambda);
|
|
// Use a scoped join to avoid leaking the thread as some platforms don't have
|
|
// `std::jthread`.
|
|
auto scoped_join =
|
|
llvm::make_scope_exit([&build2_thread] { build2_thread.join(); });
|
|
|
|
// As soon as the second thread notifies that its build is started and ready
|
|
// to commit, also commit the first built runtime.
|
|
{
|
|
std::unique_lock lock(m);
|
|
cv.wait(lock, [&build_started] { return build_started; });
|
|
}
|
|
auto commit_result = std::move(builder1).Commit();
|
|
ASSERT_THAT(commit_result, IsSuccess(_));
|
|
std::filesystem::path build1_path = *std::move(commit_result);
|
|
|
|
// Even though there may be another thread running, we should now get
|
|
// non-blocking access directly to the built runtime.
|
|
EXPECT_THAT(runtimes1.Build(Runtimes::ClangResourceDir),
|
|
IsSuccess(VariantWith<std::filesystem::path>(Eq(build1_path))));
|
|
|
|
// Now join the second cache's build thread to ensure it completes and verify
|
|
// that it produces the same path fully-built path.
|
|
build2_thread.join();
|
|
scoped_join.release();
|
|
EXPECT_THAT(build2_path, Eq(build1_path));
|
|
|
|
// Much like the simple concurrent build, we can't know which build finished
|
|
// first so we need to accept either build's runtime file.
|
|
EXPECT_THAT(*Filesystem::Cwd().ReadFileToString(build1_path / "runtime_file"),
|
|
AnyOf(StrEq("build1"), StrEq("build2")));
|
|
}
|
|
|
|
TEST_F(RuntimesCacheTest, ConcurrentBuildsLockTimeout) {
|
|
// Another test designed to be similar to `ConcurrentBuilds` but stressing a
|
|
// failure path. Here, we want to reliably exercise the code path where a lock
|
|
// file is held when a second build begins and it polls and times out. This
|
|
// can happen naturally, even with very large timeouts under sufficient system
|
|
// load. Here, we artificially make it as likely as possible for better stress
|
|
// testing and easier debugging of problems with this situation.
|
|
const std::string target = "aarch64-unknown-unknown";
|
|
auto runtimes1 = *cache_.Lookup({.target = target});
|
|
|
|
// Build a second cache and runtimes pointing at the same directory and target
|
|
// to simulate concurrent processes.
|
|
auto cache2 = *Runtimes::Cache::MakeCustom(install_, tmp_dir_.abs_path());
|
|
auto runtimes2 = *cache2.Lookup({.target = target});
|
|
|
|
// Start the first build, this will lock the directory.
|
|
auto build_result1 = runtimes1.Build(Runtimes::ClangResourceDir);
|
|
ASSERT_THAT(build_result1, IsSuccess(VariantWith<Runtimes::Builder>(_)));
|
|
auto builder1 = std::get<Runtimes::Builder>(*std::move(build_result1));
|
|
builder1.dir().WriteFileFromString("runtime_file", "build1").Check();
|
|
|
|
// Directly simulate a second thread or process timing out on acquiring the
|
|
// file-based advisory lock by giving it an artificially short timeout and
|
|
// running it in the same thread. This should only poll for 50ms before
|
|
// proceeding without the lock.
|
|
//
|
|
// However, note that this is not *guaranteed* -- the first build may have
|
|
// exhausted the much higher default poll timeout and failed to acquire a file
|
|
// lock at all. When that happens, this path may in turn succeed at acquiring
|
|
// the file lock. All of that is fine, and the test even remains effective as
|
|
// either way we have successfully exercised the code path with lock file
|
|
// timeout. The lowered time here just ensures that the test finishes promptly
|
|
// relative to the system load.
|
|
auto build_result2 = RuntimesTestPeer::BuildImpl(
|
|
runtimes2, Runtimes::ClangResourceDir, std::chrono::milliseconds(50),
|
|
std::chrono::milliseconds(10));
|
|
ASSERT_THAT(build_result2, IsSuccess(VariantWith<Runtimes::Builder>(_)));
|
|
auto builder2 = std::get<Runtimes::Builder>(*std::move(build_result2));
|
|
builder2.dir().WriteFileFromString("runtime_file", "build2").Check();
|
|
|
|
// Commit the second runtime, as this one *doesn't* hold any lock. This leaves
|
|
// the lock present and held, but creates a valid runtimes directory.
|
|
auto commit2_result = std::move(builder2).Commit();
|
|
ASSERT_THAT(commit2_result, IsSuccess(_));
|
|
std::filesystem::path build2_path = *std::move(commit2_result);
|
|
|
|
// Now, even though we still have the lock file held, repeatedly building
|
|
// proceeds without blocking.
|
|
EXPECT_THAT(runtimes2.Build(Runtimes::ClangResourceDir),
|
|
IsSuccess(VariantWith<std::filesystem::path>(Eq(build2_path))));
|
|
|
|
// Finally, commit the lock-holding build to ensure it also succeeds, even
|
|
// though it will reliably discard its built cache.
|
|
auto commit1_result = std::move(builder1).Commit();
|
|
ASSERT_THAT(commit1_result, IsSuccess(_));
|
|
std::filesystem::path build1_path = *std::move(commit1_result);
|
|
|
|
// And ensure that we got the same path and the second build's contents.
|
|
EXPECT_THAT(build1_path, Eq(build2_path));
|
|
EXPECT_THAT(*Filesystem::Cwd().ReadFileToString(build1_path / "runtime_file"),
|
|
StrEq("build2"));
|
|
}
|
|
|
|
TEST_F(RuntimesCacheTest, Lookup) {
|
|
// Basic successful lookup of a new runtimes.
|
|
auto lookup_result = cache_.Lookup({.target = "aarch64-unknown-unknown"});
|
|
ASSERT_THAT(lookup_result, IsSuccess(_));
|
|
auto runtimes = *std::move(lookup_result);
|
|
|
|
auto lock_stat = runtimes.base_dir().Stat(".lock");
|
|
ASSERT_THAT(lock_stat, IsSuccess(_));
|
|
EXPECT_TRUE(lock_stat->is_file());
|
|
|
|
// Looking up the same target should return the same runtimes.
|
|
lookup_result = cache_.Lookup({.target = "aarch64-unknown-unknown"});
|
|
ASSERT_THAT(lookup_result, IsSuccess(_));
|
|
auto runtimes2 = *std::move(lookup_result);
|
|
EXPECT_THAT(runtimes2.base_path(), Eq(runtimes.base_path()));
|
|
|
|
EXPECT_THAT(runtimes.base_dir().Stat()->unix_inode(),
|
|
Eq(runtimes.base_dir().Stat()->unix_inode()));
|
|
}
|
|
|
|
TEST_F(RuntimesCacheTest, LookupFailsIfCannotCreateDir) {
|
|
// Create a read-only directory with the cache in it to cause failures.
|
|
std::filesystem::path ro_cache_path = tmp_dir_.abs_path() / "ro_cache";
|
|
tmp_dir_.CreateDirectories("ro_cache", /*creation_mode=*/0500).Check();
|
|
auto ro_cache = *Runtimes::Cache::MakeCustom(install_, ro_cache_path);
|
|
|
|
auto lookup_result = ro_cache.Lookup({.target = "aarch64-unknown-unknown"});
|
|
EXPECT_THAT(lookup_result, IsError(_));
|
|
}
|
|
|
|
TEST_F(RuntimesCacheTest, LookupWithSmallNumberOfStaleRuntimes) {
|
|
// Lookup two runtimes to populate the cache.
|
|
auto runtimes1 = *cache_.Lookup({.target = "aarch64-unknown-unknown1"});
|
|
auto runtimes2 = *cache_.Lookup({.target = "aarch64-unknown-unknown2"});
|
|
|
|
// Get the Unix-like inode of the directories so we can check whether
|
|
// subsequent lookups create a new directory.
|
|
auto runtimes1_inode = runtimes1.base_dir().Stat()->unix_inode();
|
|
auto runtimes2_inode = runtimes2.base_dir().Stat()->unix_inode();
|
|
|
|
// Now adjust their age backwards in time by two years to make them very, very
|
|
// stale.
|
|
auto now = Filesystem::Clock::now();
|
|
auto two_years_ago = now - std::chrono::years(2);
|
|
runtimes1.base_dir().UpdateTimes(".lock", two_years_ago).Check();
|
|
runtimes2.base_dir().UpdateTimes(".lock", two_years_ago).Check();
|
|
|
|
// Close the runtimes, releasing any locks.
|
|
runtimes1 = {};
|
|
runtimes2 = {};
|
|
|
|
// Lookup a new runtime, potentially pruning stale ones.
|
|
auto runtimes3 = *cache_.Lookup({.target = "aarch64-unknown-unknown3"});
|
|
|
|
// Redo the previous lookups and ensure they found the original directories as
|
|
// we don't have enough runtimes to prune.
|
|
runtimes1 = *cache_.Lookup({.target = "aarch64-unknown-unknown1"});
|
|
runtimes2 = *cache_.Lookup({.target = "aarch64-unknown-unknown2"});
|
|
EXPECT_THAT(runtimes1.base_dir().Stat()->unix_inode(), Eq(runtimes1_inode));
|
|
EXPECT_THAT(runtimes2.base_dir().Stat()->unix_inode(), Eq(runtimes2_inode));
|
|
|
|
// The timestamp on the lock file should also be updated. We can't assume the
|
|
// filesystem clock is monotonic, so it is possible an adjustment occurs while
|
|
// this test is running. We check that the updated time is within 2 days of
|
|
// `now` to minimize flake risks, which should be completely fine to detect
|
|
// bugs as we set the time to 2 years in the past above.
|
|
EXPECT_THAT(
|
|
runtimes1.base_dir().Stat(".lock")->mtime(),
|
|
AllOf(Gt(now - std::chrono::days(2)), Lt(now + std::chrono::days(2))));
|
|
EXPECT_THAT(
|
|
runtimes2.base_dir().Stat(".lock")->mtime(),
|
|
AllOf(Gt(now - std::chrono::days(2)), Lt(now + std::chrono::days(2))));
|
|
}
|
|
|
|
TEST_F(RuntimesCacheTest, LookupWithManyStaleRuntimes) {
|
|
auto runtimes1 = *cache_.Lookup({.target = "aarch64-unknown-unknown-fresh1"});
|
|
auto stale_runtimes = LookupNRuntimes(RuntimesTestPeer::CacheMinNumEntries());
|
|
auto runtimes2 = *cache_.Lookup({.target = "aarch64-unknown-unknown-fresh2"});
|
|
|
|
// Get the Unix-like inode of the directories so we can check whether
|
|
// subsequent lookups create a new directory.
|
|
auto runtimes1_inode = runtimes1.base_dir().Stat()->unix_inode();
|
|
auto stale_runtimes_0_inode =
|
|
stale_runtimes[0].base_dir().Stat()->unix_inode();
|
|
auto runtimes2_inode = runtimes2.base_dir().Stat()->unix_inode();
|
|
|
|
// Now adjust their age backwards in time by two years to make them very, very
|
|
// stale.
|
|
auto now = Filesystem::Clock::now();
|
|
auto two_years_ago = now - std::chrono::years(2);
|
|
for (auto& stale_runtime : stale_runtimes) {
|
|
stale_runtime.base_dir().UpdateTimes(".lock", two_years_ago).Check();
|
|
}
|
|
|
|
// Close the runtimes, releasing any locks.
|
|
runtimes1 = {};
|
|
stale_runtimes.clear();
|
|
runtimes2 = {};
|
|
|
|
// Lookup a new runtime, potentially pruning stale ones.
|
|
auto runtimes3 = *cache_.Lookup({.target = "aarch64-unknown-unknown-fresh3"});
|
|
|
|
// Re-lookup three of the original runtimes.
|
|
runtimes1 = *cache_.Lookup({.target = "aarch64-unknown-unknown-fresh1"});
|
|
auto stale_runtimes_0 =
|
|
*cache_.Lookup({.target = "aarch64-unknown-unknown0"});
|
|
runtimes2 = *cache_.Lookup({.target = "aarch64-unknown-unknown-fresh2"});
|
|
|
|
// The first and last should have been preserved as they were not stale.
|
|
EXPECT_THAT(runtimes1.base_dir().Stat()->unix_inode(), Eq(runtimes1_inode));
|
|
EXPECT_THAT(runtimes2.base_dir().Stat()->unix_inode(), Eq(runtimes2_inode));
|
|
|
|
// One of the stale runtimes should be freshly created though.
|
|
EXPECT_THAT(stale_runtimes_0.base_dir().Stat()->unix_inode(),
|
|
Ne(stale_runtimes_0_inode));
|
|
}
|
|
|
|
TEST_F(RuntimesCacheTest, LookupWithTooManyRuntimes) {
|
|
auto runtimes1 = *cache_.Lookup({.target = "aarch64-unknown-unknown-fresh1"});
|
|
auto runtimes2 = *cache_.Lookup({.target = "aarch64-unknown-unknown-fresh2"});
|
|
int n = RuntimesTestPeer::CacheMaxNumEntries();
|
|
auto stale_runtimes = LookupNRuntimes(n);
|
|
|
|
// Compute stale target strings.
|
|
auto stale_runtimes_n_1_target =
|
|
llvm::formatv("aarch64-unknown-unknown{0}", n - 1).str();
|
|
auto stale_runtimes_n_2_target =
|
|
llvm::formatv("aarch64-unknown-unknown{0}", n - 2).str();
|
|
|
|
// Get the Unix-like inode of the directories so we can check whether
|
|
// subsequent lookups create a new directory.
|
|
auto runtimes1_inode = runtimes1.base_dir().Stat()->unix_inode();
|
|
auto runtimes2_inode = runtimes2.base_dir().Stat()->unix_inode();
|
|
auto stale_runtimes_0_inode =
|
|
stale_runtimes[0].base_dir().Stat()->unix_inode();
|
|
auto stale_runtimes_n_inode =
|
|
stale_runtimes.back().base_dir().Stat()->unix_inode();
|
|
auto stale_runtimes_n_1_inode =
|
|
std::prev(stale_runtimes.end(), 2)->base_dir().Stat()->unix_inode();
|
|
|
|
// Now manually set all the timestamps. We do this manually to avoid any
|
|
// reliance on the clock behavior or the amount of time passing between lookup
|
|
// calls.
|
|
auto now = Filesystem::Clock::now();
|
|
runtimes1.base_dir().UpdateTimes(".lock", now).Check();
|
|
runtimes2.base_dir().UpdateTimes(".lock", now).Check();
|
|
// Now set the stale runtimes to times further and further in the past.
|
|
now -= std::chrono::milliseconds(1);
|
|
for (auto [i, stale_runtime] : llvm::enumerate(stale_runtimes)) {
|
|
stale_runtime.base_dir()
|
|
.UpdateTimes(".lock", now - std::chrono::milliseconds(i * i))
|
|
.Check();
|
|
}
|
|
|
|
// Close most of the runtimes to release the locks, but keep the oldest stale
|
|
// runtime locked along with a fresh one to exercise the locking path.
|
|
runtimes1 = {};
|
|
auto stale_runtime_n_orig = stale_runtimes.pop_back_val();
|
|
stale_runtimes.clear();
|
|
|
|
// Lookup a new runtime, potentially pruning stale ones.
|
|
auto runtimes3 = *cache_.Lookup({.target = "aarch64-unknown-unknown-fresh3"});
|
|
|
|
// Re-lookup three of the original runtimes.
|
|
runtimes1 = *cache_.Lookup({.target = "aarch64-unknown-unknown-fresh1"});
|
|
runtimes2 = *cache_.Lookup({.target = "aarch64-unknown-unknown-fresh2"});
|
|
auto stale_runtimes_0 =
|
|
*cache_.Lookup({.target = "aarch64-unknown-unknown0"});
|
|
auto stale_runtimes_n = *cache_.Lookup({.target = stale_runtimes_n_1_target});
|
|
auto stale_runtimes_n_1 =
|
|
*cache_.Lookup({.target = stale_runtimes_n_2_target});
|
|
|
|
// The fresh runtimes should be preserved.
|
|
EXPECT_THAT(runtimes1.base_dir().Stat()->unix_inode(), Eq(runtimes1_inode));
|
|
EXPECT_THAT(runtimes2.base_dir().Stat()->unix_inode(), Eq(runtimes2_inode));
|
|
EXPECT_THAT(stale_runtimes_0.base_dir().Stat()->unix_inode(),
|
|
Eq(stale_runtimes_0_inode));
|
|
|
|
// THe last stale runtime should have been locked and so should remain.
|
|
EXPECT_THAT(stale_runtimes_n.base_dir().Stat()->unix_inode(),
|
|
Eq(stale_runtimes_n_inode));
|
|
|
|
// The next to last should have been pruned and re-created though.
|
|
EXPECT_THAT(stale_runtimes_n.base_dir().Stat()->unix_inode(),
|
|
Ne(stale_runtimes_n_1_inode));
|
|
}
|
|
|
|
} // namespace
|
|
} // namespace Carbon
|