Crafter.Build/tests/ConcurrentCacheRace/main.cpp

118 lines
4.2 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include <stdlib.h>
import std;
import Crafter.Build;
namespace fs = std::filesystem;
using namespace Crafter;
// Regression for the host-cache write race: when several crafter-build
// invocations shared the same XDG_CACHE_HOME, two LoadProject() calls would
// clobber each other's writes to `<cache>/<target>-<march>/std.pcm` and the
// `Crafter.Build-*.pcm` host modules. The loser's clang would then read a
// torn file via -fprebuilt-module-path and die with "malformed or corrupted
// precompiled file: 'can't skip to bit X from Y'". The fix serializes PCM
// (re)builds through an exclusive advisory lock on the cache dir: only one
// builder precompiles while the rest wait and then reuse the finished file, so
// no two writers ever touch the same path and a reader never sees a torn one.
//
// LoadProject() is the right surface to test because it runs the full host
// bootstrap (BuildStdPcm + EnsureCrafterBuildPcms × the Crafter.Build-*
// modules), giving every concurrent thread enough writing-to-the-same-paths
// time for the race to surface. A pure BuildStdPcm test wouldn't — the std
// PCM write is too short to overlap reliably between threads.
namespace {
fs::path FindCrafterBuildHome() {
if (const char* env = std::getenv("CRAFTER_BUILD_HOME"); env && *env) {
return env;
}
for (const char* candidate : {
"/usr/local/share/crafter-build",
"/usr/share/crafter-build",
}) {
if (fs::exists(fs::path(candidate) / "Crafter.Build.cppm")) {
return candidate;
}
}
return {};
}
void WriteFile(const fs::path& p, std::string_view contents) {
std::ofstream out(p);
out << contents;
}
}
int main() {
fs::path home = FindCrafterBuildHome();
if (home.empty()) {
std::println(std::cerr,
"SKIP: no installed share/crafter-build found and CRAFTER_BUILD_HOME unset");
return 77;
}
setenv("CRAFTER_BUILD_HOME", home.string().c_str(), 1);
fs::path scratch = fs::temp_directory_path() / "crafter-build-concurrent-cache-race";
std::error_code ec;
fs::remove_all(scratch, ec);
fs::create_directories(scratch);
// Cold scratch cache. setenv before any thread starts so every thread's
// GetCacheDir() call sees the same override.
fs::path cacheDir = scratch / "cache";
fs::create_directories(cacheDir);
setenv("XDG_CACHE_HOME", cacheDir.string().c_str(), 1);
constexpr int N = 4;
std::vector<fs::path> projects;
for (int i = 0; i < N; ++i) {
fs::path dir = scratch / std::format("p{}", i);
fs::create_directories(dir);
WriteFile(dir / "project.cpp", std::format(
"import std;\n"
"import Crafter.Build;\n"
"namespace fs = std::filesystem;\n"
"using namespace Crafter;\n"
"extern \"C\" Configuration CrafterBuildProject(std::span<const std::string_view>) {{\n"
" Configuration cfg;\n"
" cfg.path = \"./\";\n"
" cfg.name = \"p{}\";\n"
" cfg.outputName = \"p{}\";\n"
" cfg.target = HostTarget();\n"
" cfg.type = ConfigurationType::Executable;\n"
" return cfg;\n"
"}}\n", i, i));
projects.push_back(dir / "project.cpp");
}
std::array<std::string, N> errors;
std::vector<std::thread> threads;
threads.reserve(N);
for (int i = 0; i < N; ++i) {
threads.emplace_back([&, i]() {
try {
std::array<std::string_view, 0> args = {};
(void)LoadProject(projects[i], args);
} catch (const std::exception& e) {
errors[i] = e.what();
}
});
}
for (std::thread& t : threads) t.join();
int failures = 0;
for (int i = 0; i < N; ++i) {
if (!errors[i].empty()) {
std::println(std::cerr, "FAIL p{}: {}", i, errors[i]);
++failures;
}
}
fs::remove_all(scratch, ec);
if (failures > 0) {
std::println(std::cerr,
"{}/{} concurrent LoadProject() invocations failed (host-cache race)",
failures, N);
return 1;
}
return 0;
}