[llvm] [CAS] Support validation and recovery for plugin CAS in llvm-cas (PR #226291)
Steven Wu via llvm-commits
llvm-commits at lists.llvm.org
Thu Sep 24 14:14:58 PDT 2026
https://github.com/cachemeifyoucan updated https://github.com/llvm/llvm-project/pull/226291
>From f665aafddbdf68ed04e993aa93ed27ac790c03e0 Mon Sep 17 00:00:00 2001
From: Steven Wu <stevenwu at apple.com>
Date: Thu, 24 Sep 2026 13:13:50 -0700
Subject: [PATCH 1/2] =?UTF-8?q?[=F0=9D=98=80=F0=9D=97=BD=F0=9D=97=BF]=20in?=
=?UTF-8?q?itial=20version?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Created using spr 1.3.7
---
llvm/include/llvm-c/CAS/PluginAPI_functions.h | 47 +++
llvm/include/llvm-c/CAS/PluginAPI_types.h | 28 +-
.../llvm/CAS/BuiltinUnifiedCASDatabases.h | 29 +-
llvm/include/llvm/CAS/ObjectStore.h | 39 ++
llvm/include/llvm/CAS/OnDiskCASLogger.h | 7 +-
llvm/include/llvm/CAS/UnifiedOnDiskCache.h | 43 +-
llvm/include/llvm/CAS/ValidationResult.h | 7 +-
llvm/lib/CAS/BuiltinUnifiedCASDatabases.cpp | 19 +-
llvm/lib/CAS/OnDiskCASLogger.cpp | 22 +-
llvm/lib/CAS/PluginAPI.h | 7 +
llvm/lib/CAS/PluginAPI_functions.def | 2 +
llvm/lib/CAS/PluginCAS.cpp | 102 ++++-
llvm/lib/CAS/UnifiedOnDiskCache.cpp | 376 +++++++++++-------
llvm/test/CMakeLists.txt | 12 +
llvm/test/lit.site.cfg.py.in | 1 +
llvm/test/tools/llvm-cas/lit.local.cfg | 4 +
llvm/test/tools/llvm-cas/logging.test | 8 +-
.../llvm-cas/plugin-validation-crash.test | 59 +++
.../tools/llvm-cas/plugin-validation.test | 73 ++++
.../libCASPluginTest/libCASPluginTest.cpp | 65 +++
.../libCASPluginTest/libCASPluginTest.exports | 4 +
llvm/tools/llvm-cas/Options.td | 8 +
llvm/tools/llvm-cas/llvm-cas.cpp | 125 ++++--
llvm/unittests/CAS/PluginCASTest.cpp | 91 +++++
llvm/unittests/CAS/UnifiedOnDiskCacheTest.cpp | 104 ++++-
25 files changed, 1036 insertions(+), 246 deletions(-)
create mode 100644 llvm/test/tools/llvm-cas/plugin-validation-crash.test
create mode 100644 llvm/test/tools/llvm-cas/plugin-validation.test
diff --git a/llvm/include/llvm-c/CAS/PluginAPI_functions.h b/llvm/include/llvm-c/CAS/PluginAPI_functions.h
index 94ef0759873b3..b73872655c690 100644
--- a/llvm/include/llvm-c/CAS/PluginAPI_functions.h
+++ b/llvm/include/llvm-c/CAS/PluginAPI_functions.h
@@ -154,6 +154,53 @@ LLCAS_PUBLIC bool llcas_cas_prune_ondisk_data(llcas_cas_t, char **error);
LLCAS_PUBLIC bool llcas_cas_validate(llcas_cas_t, bool check_hash,
char **error);
+/**
+ * Validate the on-disk CAS and action cache contents in-process, if needed.
+ *
+ * This is called without a \c llcas_cas_t being created for the given
+ * options. The implementation decides whether validation is needed, e.g.
+ * whether the contents have already been validated since the last system boot,
+ * and should record a successful validation so that it can be skipped next
+ * time. A validation that fails or crashes should be detectable by
+ * \c llcas_cas_recover_ondisk_data, e.g. by recording that validation is
+ * pending before validating and only clearing it once validation succeeds.
+ *
+ * Validation may crash on invalid data, so clients may call this from a
+ * separate process and call \c llcas_cas_recover_ondisk_data if it fails or
+ * crashes.
+ *
+ * \param check_hash if true, the hash of each object is recomputed and compared
+ * against the one it is stored under.
+ * \param force if true, validation is performed even if it is not needed.
+ * \param error optional pointer to receive an error message if an error
+ * occurred. If set, the memory it points to needs to be released via
+ * \c llcas_string_dispose.
+ * \returns \c LLCAS_VALIDATION_RESULT_VALID or
+ * \c LLCAS_VALIDATION_RESULT_SKIPPED, or \c LLCAS_VALIDATION_RESULT_ERROR if
+ * validation could not be performed or the data is invalid.
+ */
+LLCAS_PUBLIC llcas_validation_result_t llcas_cas_validate_if_needed(
+ llcas_cas_options_t, bool check_hash, bool force, char **error);
+
+/**
+ * Recover from invalid on-disk CAS and action cache contents after
+ * \c llcas_cas_validate_if_needed failed or crashed, e.g. by discarding them.
+ *
+ * This is called without a \c llcas_cas_t being created for the given
+ * options. Multiple processes may attempt recovery concurrently after a failed
+ * validation; the implementation should skip recovery if the contents have
+ * been recovered or successfully validated in the meantime.
+ *
+ * \param error optional pointer to receive an error message if an error
+ * occurred. If set, the memory it points to needs to be released via
+ * \c llcas_string_dispose.
+ * \returns \c LLCAS_VALIDATION_RESULT_RECOVERED or
+ * \c LLCAS_VALIDATION_RESULT_SKIPPED, or \c LLCAS_VALIDATION_RESULT_ERROR if
+ * recovery could not be performed.
+ */
+LLCAS_PUBLIC llcas_validation_result_t
+llcas_cas_recover_ondisk_data(llcas_cas_options_t, char **error);
+
/**
* \returns the hash schema name that the plugin is using. The string memory it
* points to needs to be released via \c llcas_string_dispose.
diff --git a/llvm/include/llvm-c/CAS/PluginAPI_types.h b/llvm/include/llvm-c/CAS/PluginAPI_types.h
index 2d45461aea28a..19fc2a27ec222 100644
--- a/llvm/include/llvm-c/CAS/PluginAPI_types.h
+++ b/llvm/include/llvm-c/CAS/PluginAPI_types.h
@@ -20,7 +20,7 @@
#include <stdint.h>
#define LLCAS_VERSION_MAJOR 0
-#define LLCAS_VERSION_MINOR 2
+#define LLCAS_VERSION_MINOR 3
typedef struct llcas_cas_options_s *llcas_cas_options_t;
typedef struct llcas_cas_s *llcas_cas_t;
@@ -84,6 +84,32 @@ typedef enum {
LLCAS_LOOKUP_RESULT_ERROR = 2,
} llcas_lookup_result_t;
+/**
+ * Return values for \c llcas_cas_validate_if_needed and
+ * \c llcas_cas_recover_ondisk_data.
+ */
+typedef enum {
+ /**
+ * The data is valid.
+ */
+ LLCAS_VALIDATION_RESULT_VALID = 0,
+
+ /**
+ * The data was invalid, but was recovered.
+ */
+ LLCAS_VALIDATION_RESULT_RECOVERED = 1,
+
+ /**
+ * Validation or recovery was skipped, as it was not needed.
+ */
+ LLCAS_VALIDATION_RESULT_SKIPPED = 2,
+
+ /**
+ * An error occurred.
+ */
+ LLCAS_VALIDATION_RESULT_ERROR = 3,
+} llcas_validation_result_t;
+
/**
* Callback for \c llcas_cas_load_object_async.
*
diff --git a/llvm/include/llvm/CAS/BuiltinUnifiedCASDatabases.h b/llvm/include/llvm/CAS/BuiltinUnifiedCASDatabases.h
index e95e50b46e863..3a317f7177a8f 100644
--- a/llvm/include/llvm/CAS/BuiltinUnifiedCASDatabases.h
+++ b/llvm/include/llvm/CAS/BuiltinUnifiedCASDatabases.h
@@ -23,26 +23,17 @@ LLVM_ABI
Expected<std::pair<std::unique_ptr<ObjectStore>, std::unique_ptr<ActionCache>>>
createOnDiskUnifiedCASDatabases(StringRef Path);
-/// Validate the data in \p Path, if needed to ensure correctness.
-///
-/// \param Path directory for the on-disk database.
-/// \param CheckHash Whether to validate hashes match the data.
-/// \param AllowRecovery Whether to automatically recover from invalid data by
-/// marking the files for garbage collection.
-/// \param ForceValidation Whether to force validation to occur even if it
-/// should not be necessary.
-/// \param LLVMCasBinaryPath If provided, validation is performed out-of-process
-/// using the given \c llvm-cas executable which protects against crashes
-/// during validation. Otherwise validation is performed in-process.
-///
-/// \returns \c Valid if the data is already valid, \c Recovered if data
-/// was invalid but has been cleared, \c Skipped if validation is not needed,
-/// or an \c Error if validation cannot be performed or if the data is left
-/// in an invalid state because \p AllowRecovery is false.
+/// Validate the builtin on-disk CAS in \p Path in-process, if needed. See
+/// \c ondisk::UnifiedOnDiskCache::validateIfNeeded.
LLVM_ABI
-Expected<ValidationResult> validateOnDiskUnifiedCASDatabasesIfNeeded(
- StringRef Path, bool CheckHash, bool AllowRecovery, bool ForceValidation,
- std::optional<StringRef> LLVMCasBinaryPath);
+Expected<ValidationResult>
+validateOnDiskUnifiedCASDatabasesIfNeeded(StringRef Path, bool CheckHash,
+ bool ForceValidation);
+
+/// Recover the builtin on-disk CAS in \p Path after a failed validation. See
+/// \c ondisk::UnifiedOnDiskCache::recover.
+LLVM_ABI
+Expected<ValidationResult> recoverOnDiskUnifiedCASDatabases(StringRef Path);
} // namespace llvm::cas
diff --git a/llvm/include/llvm/CAS/ObjectStore.h b/llvm/include/llvm/CAS/ObjectStore.h
index 972d8eb02b724..394eb9d53d7a6 100644
--- a/llvm/include/llvm/CAS/ObjectStore.h
+++ b/llvm/include/llvm/CAS/ObjectStore.h
@@ -17,6 +17,7 @@
#include "llvm/ADT/StringRef.h"
#include "llvm/CAS/CASID.h"
#include "llvm/CAS/CASReference.h"
+#include "llvm/CAS/ValidationResult.h"
#include "llvm/Support/Error.h"
#include "llvm/Support/FileSystem.h"
#include <cstddef>
@@ -408,6 +409,44 @@ createPluginCASDatabases(
StringRef PluginPath, StringRef OnDiskPath,
ArrayRef<std::pair<std::string, std::string>> PluginArgs);
+/// Validate the on-disk data of a plugin-backed CAS in-process if needed, by
+/// calling the plugin's \c llcas_cas_validate_if_needed. The plugin decides
+/// whether validation is needed.
+///
+/// Validation can crash on invalid data. Clients that want to be resilient to
+/// that should call this from a separate process (e.g. via
+/// \c llvm-cas -validate-if-needed) and call \c recoverPluginCASDatabases if
+/// it fails.
+///
+/// \param PluginPath path of the dynamic library to load.
+/// \param OnDiskPath local path that the plugin uses for any on-disk
+/// resources/caches.
+/// \param PluginArgs name/value pairs passed to the plugin as custom options;
+/// they are opaque to the client.
+/// \param CheckHash Whether to validate hashes match the data.
+/// \param ForceValidation Whether to force validation to occur even if it
+/// should not be necessary.
+///
+/// \returns \c Valid if the data is valid, \c Skipped if validation is not
+/// needed, or an \c Error if validation cannot be performed (including if the
+/// plugin does not support it) or the data is invalid.
+LLVM_ABI Expected<ValidationResult> validatePluginCASDatabasesIfNeeded(
+ StringRef PluginPath, StringRef OnDiskPath,
+ ArrayRef<std::pair<std::string, std::string>> PluginArgs, bool CheckHash,
+ bool ForceValidation);
+
+/// Recover the on-disk data of a plugin-backed CAS after a failed
+/// \c validatePluginCASDatabasesIfNeeded, by calling the plugin's
+/// \c llcas_cas_recover_ondisk_data.
+///
+/// \returns \c Recovered if the data has been recovered, \c Skipped if
+/// recovery is not needed (e.g. a concurrent process already recovered), or an
+/// \c Error if recovery cannot be performed (including if the plugin does not
+/// support it).
+LLVM_ABI Expected<ValidationResult> recoverPluginCASDatabases(
+ StringRef PluginPath, StringRef OnDiskPath,
+ ArrayRef<std::pair<std::string, std::string>> PluginArgs);
+
} // namespace cas
} // namespace llvm
diff --git a/llvm/include/llvm/CAS/OnDiskCASLogger.h b/llvm/include/llvm/CAS/OnDiskCASLogger.h
index 1bea1e082c2f9..d54bdf7258cdf 100644
--- a/llvm/include/llvm/CAS/OnDiskCASLogger.h
+++ b/llvm/include/llvm/CAS/OnDiskCASLogger.h
@@ -72,9 +72,10 @@ class OnDiskCASLogger {
LLVM_ABI void logUnifiedOnDiskCacheCollectGarbage(StringRef Path);
LLVM_ABI void logUnifiedOnDiskCacheValidateIfNeeded(
StringRef Path, uint64_t BootTime, uint64_t ValidationTime,
- bool CheckHash, bool AllowRecovery, bool Force,
- std::optional<StringRef> LLVMCas, StringRef ValidationError, bool Skipped,
- bool Recovered);
+ bool CheckHash, bool Force, StringRef ValidationError, bool Skipped);
+ LLVM_ABI void logUnifiedOnDiskCacheRecover(StringRef Path, uint64_t BootTime,
+ StringRef RecoveryError,
+ bool Skipped);
LLVM_ABI void logTempFileCreate(StringRef Name);
LLVM_ABI void logTempFileKeep(StringRef TmpName, StringRef Name,
std::error_code EC);
diff --git a/llvm/include/llvm/CAS/UnifiedOnDiskCache.h b/llvm/include/llvm/CAS/UnifiedOnDiskCache.h
index 24bc4a36883a2..554ca5e0cceba 100644
--- a/llvm/include/llvm/CAS/UnifiedOnDiskCache.h
+++ b/llvm/include/llvm/CAS/UnifiedOnDiskCache.h
@@ -70,34 +70,47 @@ class UnifiedOnDiskCache {
OnDiskGraphDB::FaultInPolicy FaultInPolicy =
OnDiskGraphDB::FaultInPolicy::FullTree);
- /// Validate the data in \p Path, if needed to ensure correctness.
+ /// Validate the data in \p Path in-process, if it has not been validated
+ /// since the last system boot. A successful validation is recorded so that
+ /// subsequent calls can skip it; a failed or crashed one is recorded as
+ /// pending for \c recover, and is not skipped by subsequent calls.
///
- /// Note: if invalid data is detected and \p AllowRecovery is true, then
- /// recovery requires exclusive access to the CAS and it is an error to
- /// attempt recovery if there is concurrent use of the CAS.
+ /// Validation can crash on invalid data. Clients that want to be resilient
+ /// to that should call this from a separate process (e.g. via
+ /// \c llvm-cas -validate-if-needed) and call \c recover if it fails.
///
/// \param Path directory for the on-disk database.
/// \param HashName Identifier name for the hashing algorithm that is going to
/// be used.
/// \param HashByteSize Size for the object digest hash bytes.
/// \param CheckHash Whether to validate hashes match the data.
- /// \param AllowRecovery Whether to automatically recover from invalid data by
- /// marking the files for garbage collection.
/// \param ForceValidation Whether to force validation to occur even if it
/// should not be necessary.
- /// \param LLVMCasBinary If provided, validation is performed out-of-process
- /// using the given \c llvm-cas executable which protects against crashes
- /// during validation. Otherwise validation is performed in-process.
///
- /// \returns \c Valid if the data is already valid, \c Recovered if data
- /// was invalid but has been cleared, \c Skipped if validation is not needed,
- /// or an \c Error if validation cannot be performed or if the data is left
- /// in an invalid state because \p AllowRecovery is false.
+ /// \returns \c Valid if the data is valid, \c Skipped if validation is not
+ /// needed, or an \c Error if validation cannot be performed or the data is
+ /// invalid.
LLVM_ABI static Expected<ValidationResult>
validateIfNeeded(StringRef Path, StringRef HashName, unsigned HashByteSize,
bool CheckHash, OnDiskGraphDB::HashingFuncT HashFn,
- bool AllowRecovery, bool ForceValidation,
- std::optional<StringRef> LLVMCasBinary);
+ bool ForceValidation);
+
+ /// Recover from invalid data in \p Path after a failed \c validateIfNeeded,
+ /// by marking all the data for garbage collection.
+ ///
+ /// Recovery requires exclusive access to the CAS and it is an error to
+ /// attempt recovery if there is concurrent use of the CAS.
+ ///
+ /// Recovery is serialized with \c validateIfNeeded, and only happens if the
+ /// last validation failed or crashed. If the data has been recovered or
+ /// validated successfully since, e.g. by a concurrent process, recovery is
+ /// skipped.
+ ///
+ /// \param Path directory for the on-disk database.
+ ///
+ /// \returns \c Recovered if the data has been cleared, \c Skipped if
+ /// recovery is not needed, or an \c Error if recovery cannot be performed.
+ LLVM_ABI static Expected<ValidationResult> recover(StringRef Path);
/// Validate the action cache only.
LLVM_ABI Error validateActionCache() const;
diff --git a/llvm/include/llvm/CAS/ValidationResult.h b/llvm/include/llvm/CAS/ValidationResult.h
index bd3d2999bda9e..81b25d8a3b170 100644
--- a/llvm/include/llvm/CAS/ValidationResult.h
+++ b/llvm/include/llvm/CAS/ValidationResult.h
@@ -11,8 +11,9 @@
namespace llvm::cas {
-/// Represents the result of validating the contents using
-/// \c validateOnDiskUnifiedCASDatabasesIfNeeded.
+/// Represents the result of validating the contents, e.g. using
+/// \c validateOnDiskUnifiedCASDatabasesIfNeeded, or of recovering from a
+/// failed validation, e.g. using \c recoverOnDiskUnifiedCASDatabases.
///
/// Note: invalid results are handled as an \c Error.
enum class ValidationResult {
@@ -20,7 +21,7 @@ enum class ValidationResult {
Valid,
/// The data was invalid, but was recovered.
Recovered,
- /// Validation was skipped, as it was not needed.
+ /// Validation or recovery was skipped, as it was not needed.
Skipped,
};
diff --git a/llvm/lib/CAS/BuiltinUnifiedCASDatabases.cpp b/llvm/lib/CAS/BuiltinUnifiedCASDatabases.cpp
index 8283e981b1099..11fd2507bcb3a 100644
--- a/llvm/lib/CAS/BuiltinUnifiedCASDatabases.cpp
+++ b/llvm/lib/CAS/BuiltinUnifiedCASDatabases.cpp
@@ -24,14 +24,23 @@ cas::createOnDiskUnifiedCASDatabases(StringRef Path) {
return std::make_pair(std::move(CAS), std::move(AC));
}
-Expected<ValidationResult> cas::validateOnDiskUnifiedCASDatabasesIfNeeded(
- StringRef Path, bool CheckHash, bool AllowRecovery, bool ForceValidation,
- std::optional<StringRef> LLVMCasBinary) {
+Expected<ValidationResult>
+cas::validateOnDiskUnifiedCASDatabasesIfNeeded(StringRef Path, bool CheckHash,
+ bool ForceValidation) {
#if LLVM_ENABLE_ONDISK_CAS
return ondisk::UnifiedOnDiskCache::validateIfNeeded(
Path, builtin::BuiltinCASContext::getHashName(),
- sizeof(builtin::HashType), CheckHash, builtin::hashingFunc, AllowRecovery,
- ForceValidation, LLVMCasBinary);
+ sizeof(builtin::HashType), CheckHash, builtin::hashingFunc,
+ ForceValidation);
+#else
+ return createStringError(inconvertibleErrorCode(), "OnDiskCache is disabled");
+#endif
+}
+
+Expected<ValidationResult>
+cas::recoverOnDiskUnifiedCASDatabases(StringRef Path) {
+#if LLVM_ENABLE_ONDISK_CAS
+ return ondisk::UnifiedOnDiskCache::recover(Path);
#else
return createStringError(inconvertibleErrorCode(), "OnDiskCache is disabled");
#endif
diff --git a/llvm/lib/CAS/OnDiskCASLogger.cpp b/llvm/lib/CAS/OnDiskCASLogger.cpp
index 8ff63a8f85248..33d09489b6dc4 100644
--- a/llvm/lib/CAS/OnDiskCASLogger.cpp
+++ b/llvm/lib/CAS/OnDiskCASLogger.cpp
@@ -225,23 +225,29 @@ void OnDiskCASLogger::logUnifiedOnDiskCacheCollectGarbage(StringRef Path) {
void OnDiskCASLogger::logUnifiedOnDiskCacheValidateIfNeeded(
StringRef Path, uint64_t BootTime, uint64_t ValidationTime, bool CheckHash,
- bool AllowRecovery, bool Force, std::optional<StringRef> LLVMCas,
- StringRef ValidationError, bool Skipped, bool Recovered) {
+ bool Force, StringRef ValidationError, bool Skipped) {
TextLogLine Log(OS);
Log << "validate-if-needed '" << Path << "'";
Log << " boot=" << BootTime << " last-valid=" << ValidationTime;
- Log << " check-hash=" << CheckHash << " allow-recovery=" << AllowRecovery;
- Log << " force=" << Force;
- if (LLVMCas)
- Log << " llvm-cas=" << *LLVMCas;
+ Log << " check-hash=" << CheckHash << " force=" << Force;
if (Skipped)
Log << " skipped";
- if (Recovered)
- Log << " recovered";
if (!ValidationError.empty())
Log << " data was invalid " << ValidationError;
}
+void OnDiskCASLogger::logUnifiedOnDiskCacheRecover(StringRef Path,
+ uint64_t BootTime,
+ StringRef RecoveryError,
+ bool Skipped) {
+ TextLogLine Log(OS);
+ Log << "recover '" << Path << "' boot=" << BootTime;
+ if (Skipped)
+ Log << " skipped";
+ if (!RecoveryError.empty())
+ Log << " failed " << RecoveryError;
+}
+
void OnDiskCASLogger::logTempFileCreate(StringRef Name) {
TextLogLine Log(OS);
Log << "standalone file create '" << Name << "'";
diff --git a/llvm/lib/CAS/PluginAPI.h b/llvm/lib/CAS/PluginAPI.h
index 769020403464b..28a7931f6f4e2 100644
--- a/llvm/lib/CAS/PluginAPI.h
+++ b/llvm/lib/CAS/PluginAPI.h
@@ -53,6 +53,13 @@ struct llcas_functions_t {
bool (*cas_validate)(llcas_cas_t, bool check_hash, char **error);
+ llcas_validation_result_t (*cas_validate_if_needed)(llcas_cas_options_t,
+ bool check_hash,
+ bool force, char **error);
+
+ llcas_validation_result_t (*cas_recover_ondisk_data)(llcas_cas_options_t,
+ char **error);
+
unsigned (*digest_parse)(llcas_cas_t, const char *printed_digest,
uint8_t *bytes, size_t bytes_size, char **error);
diff --git a/llvm/lib/CAS/PluginAPI_functions.def b/llvm/lib/CAS/PluginAPI_functions.def
index 8e779b68d8288..c12ccaf1a3714 100644
--- a/llvm/lib/CAS/PluginAPI_functions.def
+++ b/llvm/lib/CAS/PluginAPI_functions.def
@@ -36,10 +36,12 @@ CASPLUGINAPI_FUNCTION(cas_options_set_client_version, true)
CASPLUGINAPI_FUNCTION(cas_options_set_ondisk_path, true)
CASPLUGINAPI_FUNCTION(cas_options_set_option, true)
CASPLUGINAPI_FUNCTION(cas_prune_ondisk_data, false)
+CASPLUGINAPI_FUNCTION(cas_recover_ondisk_data, false)
CASPLUGINAPI_FUNCTION(cas_set_ondisk_size_limit, false)
CASPLUGINAPI_FUNCTION(cas_store_from_filepath, false)
CASPLUGINAPI_FUNCTION(cas_store_object, true)
CASPLUGINAPI_FUNCTION(cas_validate, false)
+CASPLUGINAPI_FUNCTION(cas_validate_if_needed, false)
CASPLUGINAPI_FUNCTION(digest_parse, true)
CASPLUGINAPI_FUNCTION(digest_print, true)
CASPLUGINAPI_FUNCTION(get_plugin_version, true)
diff --git a/llvm/lib/CAS/PluginCAS.cpp b/llvm/lib/CAS/PluginCAS.cpp
index 945a5ac43f56e..682a674a984dc 100644
--- a/llvm/lib/CAS/PluginCAS.cpp
+++ b/llvm/lib/CAS/PluginCAS.cpp
@@ -17,6 +17,7 @@
//===----------------------------------------------------------------------===//
#include "PluginAPI.h"
+#include "llvm/ADT/STLFunctionalExtras.h"
#include "llvm/ADT/ScopeExit.h"
#include "llvm/CAS/ActionCache.h"
#include "llvm/CAS/ObjectStore.h"
@@ -69,9 +70,8 @@ void PluginCASContext::printIDImpl(raw_ostream &OS, const CASID &ID) const {
Functions.string_dispose(c_printed_id);
}
-Expected<std::shared_ptr<PluginCASContext>> PluginCASContext::create(
- StringRef PluginPath, StringRef OnDiskPath,
- ArrayRef<std::pair<std::string, std::string>> PluginArgs) {
+/// Loads the plugin library at \p PluginPath and looks up its functions.
+static Expected<llcas_functions_t> loadPluginFunctions(StringRef PluginPath) {
auto reportError = [PluginPath](const Twine &Description) -> Error {
std::error_code EC = inconvertibleErrorCode();
return createStringError(EC, "error loading '" + PluginPath +
@@ -96,9 +96,15 @@ Expected<std::shared_ptr<PluginCASContext>> PluginCASContext::create(
#include "PluginAPI_functions.def"
#undef CASPLUGINAPI_FUNCTION
- llcas_cas_options_t c_opts = Functions.cas_options_create();
- scope_exit DisposeOptions([&]() { Functions.cas_options_dispose(c_opts); });
+ return Functions;
+}
+/// Creates a \c llcas_cas_options_t for \p OnDiskPath and \p PluginArgs. On
+/// success the caller is responsible for disposing it.
+static Expected<llcas_cas_options_t>
+createPluginOptions(const llcas_functions_t &Functions, StringRef OnDiskPath,
+ ArrayRef<std::pair<std::string, std::string>> PluginArgs) {
+ llcas_cas_options_t c_opts = Functions.cas_options_create();
Functions.cas_options_set_client_version(c_opts, LLCAS_VERSION_MAJOR,
LLCAS_VERSION_MINOR);
SmallString<256> OnDiskPathBuf = OnDiskPath;
@@ -106,9 +112,26 @@ Expected<std::shared_ptr<PluginCASContext>> PluginCASContext::create(
for (const auto &Pair : PluginArgs) {
char *c_err = nullptr;
if (Functions.cas_options_set_option(c_opts, Pair.first.c_str(),
- Pair.second.c_str(), &c_err))
- return errorAndDispose(c_err, Functions);
+ Pair.second.c_str(), &c_err)) {
+ Functions.cas_options_dispose(c_opts);
+ return PluginCASContext::errorAndDispose(c_err, Functions);
+ }
}
+ return c_opts;
+}
+
+Expected<std::shared_ptr<PluginCASContext>> PluginCASContext::create(
+ StringRef PluginPath, StringRef OnDiskPath,
+ ArrayRef<std::pair<std::string, std::string>> PluginArgs) {
+ llcas_functions_t Functions{};
+ if (Error E = loadPluginFunctions(PluginPath).moveInto(Functions))
+ return std::move(E);
+
+ llcas_cas_options_t c_opts = nullptr;
+ if (Error E = createPluginOptions(Functions, OnDiskPath, PluginArgs)
+ .moveInto(c_opts))
+ return std::move(E);
+ scope_exit DisposeOptions([&]() { Functions.cas_options_dispose(c_opts); });
char *c_err = nullptr;
llcas_cas_t c_cas = Functions.cas_create(c_opts, &c_err);
@@ -563,3 +586,68 @@ cas::createPluginCASDatabases(
auto AC = std::make_shared<PluginActionCache>(std::move(Ctx));
return std::make_pair(std::move(CAS), std::move(AC));
}
+
+/// Loads the plugin and calls \p Fn with a \c llcas_cas_options_t for
+/// \p OnDiskPath and \p PluginArgs, converting the returned
+/// \c llcas_validation_result_t.
+static Expected<ValidationResult> callPluginValidationFunction(
+ StringRef PluginPath, StringRef OnDiskPath,
+ ArrayRef<std::pair<std::string, std::string>> PluginArgs,
+ function_ref<Expected<llcas_validation_result_t>(
+ const llcas_functions_t &, llcas_cas_options_t, char **)>
+ Fn) {
+ llcas_functions_t Functions{};
+ if (Error E = loadPluginFunctions(PluginPath).moveInto(Functions))
+ return std::move(E);
+
+ llcas_cas_options_t c_opts = nullptr;
+ if (Error E = createPluginOptions(Functions, OnDiskPath, PluginArgs)
+ .moveInto(c_opts))
+ return std::move(E);
+ scope_exit DisposeOptions([&]() { Functions.cas_options_dispose(c_opts); });
+
+ char *c_err = nullptr;
+ llcas_validation_result_t Result;
+ if (Error E = Fn(Functions, c_opts, &c_err).moveInto(Result))
+ return std::move(E);
+ switch (Result) {
+ case LLCAS_VALIDATION_RESULT_VALID:
+ return ValidationResult::Valid;
+ case LLCAS_VALIDATION_RESULT_RECOVERED:
+ return ValidationResult::Recovered;
+ case LLCAS_VALIDATION_RESULT_SKIPPED:
+ return ValidationResult::Skipped;
+ case LLCAS_VALIDATION_RESULT_ERROR:
+ return PluginCASContext::errorAndDispose(c_err, Functions);
+ }
+ llvm_unreachable("unknown llcas_validation_result_t value");
+}
+
+Expected<ValidationResult> cas::validatePluginCASDatabasesIfNeeded(
+ StringRef PluginPath, StringRef OnDiskPath,
+ ArrayRef<std::pair<std::string, std::string>> PluginArgs, bool CheckHash,
+ bool ForceValidation) {
+ return callPluginValidationFunction(
+ PluginPath, OnDiskPath, PluginArgs,
+ [&](const llcas_functions_t &Functions, llcas_cas_options_t c_opts,
+ char **c_err) -> Expected<llcas_validation_result_t> {
+ if (!Functions.cas_validate_if_needed)
+ return createStringError(
+ "plugin cas doesn't support validate-if-needed");
+ return Functions.cas_validate_if_needed(c_opts, CheckHash,
+ ForceValidation, c_err);
+ });
+}
+
+Expected<ValidationResult> cas::recoverPluginCASDatabases(
+ StringRef PluginPath, StringRef OnDiskPath,
+ ArrayRef<std::pair<std::string, std::string>> PluginArgs) {
+ return callPluginValidationFunction(
+ PluginPath, OnDiskPath, PluginArgs,
+ [&](const llcas_functions_t &Functions, llcas_cas_options_t c_opts,
+ char **c_err) -> Expected<llcas_validation_result_t> {
+ if (!Functions.cas_recover_ondisk_data)
+ return createStringError("plugin cas doesn't support recovery");
+ return Functions.cas_recover_ondisk_data(c_opts, c_err);
+ });
+}
diff --git a/llvm/lib/CAS/UnifiedOnDiskCache.cpp b/llvm/lib/CAS/UnifiedOnDiskCache.cpp
index 3d0a8b3eba135..ce921a1ef7f43 100644
--- a/llvm/lib/CAS/UnifiedOnDiskCache.cpp
+++ b/llvm/lib/CAS/UnifiedOnDiskCache.cpp
@@ -53,8 +53,18 @@
/// the underlying on-disk storage. The low-level storage is designed to remain
/// coherent across regular process crashes, but may be invalid after power loss
/// or similar system failures. \c UnifiedOnDiskCache::validateIfNeeded allows
-/// validating the contents once per boot and can recover by marking invalid
-/// data for garbage collection.
+/// validating the contents once per boot, and if validation fails (or crashes,
+/// when performed in a separate process) \c UnifiedOnDiskCache::recover can
+/// recover by marking invalid data for garbage collection.
+///
+/// Validation and recovery are serialized by an exclusive lock on the
+/// "v1.validation" file, which records the boot time of the last successful
+/// validation or recovery. Before validating, the file is marked as validation
+/// pending, and the boot time is only written once validation succeeds; a
+/// validation that fails or crashes leaves it pending, so the next validation
+/// is not skipped. Recovery only happens while validation is pending, so when
+/// multiple processes attempt recovery after a failed validation only the first
+/// one recovers.
///
/// The data recovery described above requires exclusive access to the CAS, and
/// it is an error to attempt recovery if the CAS is open in any process/thread.
@@ -80,11 +90,8 @@
#include "llvm/Support/Errc.h"
#include "llvm/Support/Error.h"
#include "llvm/Support/FileSystem.h"
-#include "llvm/Support/FileUtilities.h"
#include "llvm/Support/IOSandbox.h"
-#include "llvm/Support/MemoryBuffer.h"
#include "llvm/Support/Path.h"
-#include "llvm/Support/Program.h"
#include "llvm/Support/raw_ostream.h"
#include <optional>
@@ -207,50 +214,6 @@ static void getNextDBDirName(StringRef DBDir, llvm::raw_ostream &OS) {
OS << DBDirPrefix << Count + 1;
}
-static Error validateOutOfProcess(StringRef LLVMCasBinary, StringRef RootPath,
- bool CheckHash) {
- SmallVector<StringRef> Args{LLVMCasBinary, "-cas", RootPath, "-validate"};
- if (CheckHash)
- Args.push_back("-check-hash");
-
- llvm::SmallString<128> StdErrPath;
- int StdErrFD = -1;
- if (std::error_code EC = sys::fs::createTemporaryFile(
- "llvm-cas-validate-stderr", "txt", StdErrFD, StdErrPath,
- llvm::sys::fs::OF_Text))
- return createStringError(EC, "failed to create temporary file");
- FileRemover OutputRemover(StdErrPath.c_str());
-
- std::optional<llvm::StringRef> Redirects[] = {
- {""}, // stdin = /dev/null
- {""}, // stdout = /dev/null
- StdErrPath.str(),
- };
-
- std::string ErrMsg;
- int Result =
- sys::ExecuteAndWait(LLVMCasBinary, Args, /*Env=*/std::nullopt, Redirects,
- /*SecondsToWait=*/120, /*MemoryLimit=*/0, &ErrMsg);
-
- if (Result == -1)
- return createStringError("failed to exec " + join(Args, " ") + ": " +
- ErrMsg);
- if (Result != 0) {
- llvm::SmallString<64> Err("cas contents invalid");
- if (!ErrMsg.empty()) {
- Err += ": ";
- Err += ErrMsg;
- }
- auto StdErrBuf = MemoryBuffer::getFile(StdErrPath.c_str());
- if (StdErrBuf && !(*StdErrBuf)->getBuffer().empty()) {
- Err += ": ";
- Err += (*StdErrBuf)->getBuffer();
- }
- return createStringError(Err);
- }
- return Error::success();
-}
-
Error UnifiedOnDiskCache::validateActionCache() const {
return getKeyValueDB().validate();
}
@@ -270,27 +233,166 @@ static Error validateInProcess(StringRef RootPath, StringRef HashName,
return Error::success();
}
-Expected<ValidationResult> UnifiedOnDiskCache::validateIfNeeded(
- StringRef RootPath, StringRef HashName, unsigned HashByteSize,
- bool CheckHash, OnDiskGraphDB::HashingFuncT HashFn, bool AllowRecovery,
- bool ForceValidation, std::optional<StringRef> LLVMCasBinaryPath) {
- if (std::error_code EC = sys::fs::create_directories(RootPath))
- return createFileError(RootPath, EC);
+static Expected<uint64_t> getCachedBootTime() {
+ static uint64_t BootTime = 0;
+ if (BootTime == 0)
+ if (Error E = getBootTime().moveInto(BootTime))
+ return std::move(E);
+ return BootTime;
+}
+
+namespace {
+/// The validation file records the state of validation for the data:
+/// - empty: never validated.
+/// - \c ValidationPending: a validation started but did not succeed, i.e. it
+/// failed or crashed, and the data has not been recovered since.
+/// - a boot time: the data was validated or recovered during that boot.
+///
+/// While this object is alive it holds an exclusive lock on the file, which
+/// serializes validation and recovery across processes and threads.
+///
+/// Lock ordering: the validation file lock is always acquired before the
+/// top-level "lock" file, and the latter is only ever acquired exclusively via
+/// a non-blocking try-lock, so that validation and recovery cannot deadlock.
+class LockedValidationFile {
+public:
+ /// Written before validating. It is an integer so that older versions, which
+ /// only know about boot times, still parse the file and treat it as not
+ /// validated. It never matches a boot time.
+ static constexpr uint64_t ValidationPending = 0;
+
+ static Expected<std::unique_ptr<LockedValidationFile>>
+ open(StringRef RootPath) {
+ if (std::error_code EC = sys::fs::create_directories(RootPath))
+ return createFileError(RootPath, EC);
+
+ SmallString<256> PathBuf(RootPath);
+ sys::path::append(PathBuf, ValidationFilename);
+ int FD = -1;
+ if (std::error_code EC = sys::fs::openFileForReadWrite(
+ PathBuf, FD, sys::fs::CD_OpenAlways, sys::fs::OF_None))
+ return createFileError(PathBuf, EC);
+ assert(FD != -1);
+ std::unique_ptr<LockedValidationFile> VF(
+ new LockedValidationFile(PathBuf, FD));
+
+ if (std::error_code EC =
+ lockFileThreadSafe(FD, sys::fs::LockKind::Exclusive))
+ return createFileError(PathBuf, EC);
+ VF->Locked = true;
+
+ SmallString<8> Bytes;
+ if (Error E = sys::fs::readNativeFileToEOF(VF->File, Bytes))
+ return createFileError(PathBuf, std::move(E));
+ if (!Bytes.empty()) {
+ uint64_t Value;
+ if (StringRef(Bytes).trim().getAsInteger(10, Value))
+ return createFileError(PathBuf, errc::illegal_byte_sequence,
+ "expected integer");
+ VF->State = Value;
+ }
+ return std::move(VF);
+ }
+ ~LockedValidationFile() {
+ if (Locked)
+ unlockFileThreadSafe(FD);
+ sys::fs::closeFile(File);
+ }
+
+ /// \returns the boot time of the last successful validation or recovery, or
+ /// 0 if there is none.
+ uint64_t getLastValidBootTime() const { return State.value_or(0); }
+
+ bool isValidationPending() const { return State == ValidationPending; }
+
+ Error setValidationPending() { return write(ValidationPending); }
+
+ Error setLastValidBootTime(uint64_t BootTime) { return write(BootTime); }
+
+private:
+ LockedValidationFile(StringRef Path, int FD)
+ : Path(Path), FD(FD), File(sys::fs::convertFDToNativeFile(FD)) {}
+
+ Error write(uint64_t Value) {
+ if (State == Value)
+ return Error::success();
+ if (std::error_code EC = sys::fs::resize_file(FD, 0))
+ return createFileError(Path, EC);
+ raw_fd_ostream OS(FD, /*shouldClose=*/false);
+ OS.seek(0); // resize does not reset position
+ OS << Value << '\n';
+ if (OS.has_error())
+ return createFileError(Path, OS.error());
+ State = Value;
+ return Error::success();
+ }
+
+ SmallString<256> Path;
+ int FD;
+ sys::fs::file_t File;
+ bool Locked = false;
+ std::optional<uint64_t> State;
+};
+} // namespace
+
+/// Marks all the database directories in \p RootPath as corrupt, which makes
+/// them eligible for garbage collection. Requires exclusive access to the CAS.
+static Error markAllDBDirsCorrupt(StringRef RootPath) {
SmallString<256> PathBuf(RootPath);
- sys::path::append(PathBuf, ValidationFilename);
- int FD = -1;
+ sys::path::append(PathBuf, "lock");
+
+ int LockFD = -1;
if (std::error_code EC = sys::fs::openFileForReadWrite(
- PathBuf, FD, sys::fs::CD_OpenAlways, sys::fs::OF_None))
+ PathBuf, LockFD, sys::fs::CD_OpenAlways, sys::fs::OF_None))
return createFileError(PathBuf, EC);
- assert(FD != -1);
+ sys::fs::file_t LockFile = sys::fs::convertFDToNativeFile(LockFD);
+ llvm::scope_exit CloseLock([&]() { sys::fs::closeFile(LockFile); });
+ if (std::error_code EC = tryLockFileThreadSafe(LockFD)) {
+ if (EC == std::errc::no_lock_available)
+ return createFileError(
+ PathBuf, EC,
+ "CAS recovery requires exclusive access but CAS was in use");
+ return createFileError(PathBuf, EC);
+ }
+ llvm::scope_exit UnlockFD([&]() { unlockFileThreadSafe(LockFD); });
- sys::fs::file_t File = sys::fs::convertFDToNativeFile(FD);
- llvm::scope_exit CloseFile([&]() { sys::fs::closeFile(File); });
+ auto DBDirs = getAllDBDirs(RootPath);
+ if (!DBDirs)
+ return DBDirs.takeError();
- if (std::error_code EC = lockFileThreadSafe(FD, sys::fs::LockKind::Exclusive))
- return createFileError(PathBuf, EC);
- llvm::scope_exit UnlockFD([&]() { unlockFileThreadSafe(FD); });
+ for (StringRef DBDir : *DBDirs) {
+ sys::path::remove_filename(PathBuf);
+ sys::path::append(PathBuf, DBDir);
+ std::error_code EC;
+ int Attempt = 0, MaxAttempts = 100;
+ SmallString<128> GCPath;
+ for (; Attempt < MaxAttempts; ++Attempt) {
+ GCPath.assign(RootPath);
+ sys::path::append(GCPath,
+ CorruptPrefix + std::to_string(Attempt) + "." + DBDir);
+ EC = sys::fs::rename(PathBuf, GCPath);
+ // Darwin uses ENOTEMPTY. Linux may return either ENOTEMPTY or EEXIST.
+ if (EC != errc::directory_not_empty && EC != errc::file_exists)
+ break;
+ }
+ if (Attempt == MaxAttempts)
+ return createStringError(
+ EC, "rename " + PathBuf +
+ " failed: too many CAS directories awaiting pruning");
+ if (EC)
+ return createStringError(EC, "rename " + PathBuf + " to " + GCPath +
+ " failed: " + EC.message());
+ }
+ return Error::success();
+}
+
+Expected<ValidationResult> UnifiedOnDiskCache::validateIfNeeded(
+ StringRef RootPath, StringRef HashName, unsigned HashByteSize,
+ bool CheckHash, OnDiskGraphDB::HashingFuncT HashFn, bool ForceValidation) {
+ std::unique_ptr<LockedValidationFile> VF;
+ if (Error E = LockedValidationFile::open(RootPath).moveInto(VF))
+ return std::move(E);
std::shared_ptr<ondisk::OnDiskCASLogger> Logger;
#ifndef _WIN32
@@ -299,22 +401,11 @@ Expected<ValidationResult> UnifiedOnDiskCache::validateIfNeeded(
return std::move(E);
#endif
- SmallString<8> Bytes;
- if (Error E = sys::fs::readNativeFileToEOF(File, Bytes))
- return createFileError(PathBuf, std::move(E));
-
- uint64_t ValidationBootTime = 0;
- if (!Bytes.empty() &&
- StringRef(Bytes).trim().getAsInteger(10, ValidationBootTime))
- return createFileError(PathBuf, errc::illegal_byte_sequence,
- "expected integer");
-
- static uint64_t BootTime = 0;
- if (BootTime == 0)
- if (Error E = getBootTime().moveInto(BootTime))
- return std::move(E);
+ uint64_t BootTime = 0;
+ if (Error E = getCachedBootTime().moveInto(BootTime))
+ return std::move(E);
+ uint64_t ValidationBootTime = VF->getLastValidBootTime();
- bool Recovered = false;
bool Skipped = false;
std::string LogValidationError;
@@ -322,9 +413,8 @@ Expected<ValidationResult> UnifiedOnDiskCache::validateIfNeeded(
if (!Logger)
return;
Logger->logUnifiedOnDiskCacheValidateIfNeeded(
- RootPath, BootTime, ValidationBootTime, CheckHash, AllowRecovery,
- ForceValidation, LLVMCasBinaryPath, LogValidationError, Skipped,
- Recovered);
+ RootPath, BootTime, ValidationBootTime, CheckHash, ForceValidation,
+ LogValidationError, Skipped);
});
if (ValidationBootTime == BootTime && !ForceValidation) {
@@ -332,86 +422,66 @@ Expected<ValidationResult> UnifiedOnDiskCache::validateIfNeeded(
return ValidationResult::Skipped;
}
- // Validate!
- bool NeedsRecovery = false;
- Error E = LLVMCasBinaryPath
- ? validateOutOfProcess(*LLVMCasBinaryPath, RootPath, CheckHash)
- : validateInProcess(RootPath, HashName, HashByteSize, CheckHash,
- HashFn);
- if (E) {
+ // Mark validation as pending until it succeeds, so that a failed or crashed
+ // validation is detected by recovery and by the next validation.
+ if (Error E = VF->setValidationPending())
+ return std::move(E);
+
+ if (Error E = validateInProcess(RootPath, HashName, HashByteSize, CheckHash,
+ HashFn)) {
if (Logger)
LogValidationError = toStringWithoutConsuming(E);
- if (AllowRecovery) {
- consumeError(std::move(E));
- NeedsRecovery = true;
- } else {
- return std::move(E);
- }
+ return std::move(E);
}
- if (NeedsRecovery) {
- sys::path::remove_filename(PathBuf);
- sys::path::append(PathBuf, "lock");
+ if (Error E = VF->setLastValidBootTime(BootTime))
+ return std::move(E);
+ return ValidationResult::Valid;
+}
- int LockFD = -1;
- if (std::error_code EC = sys::fs::openFileForReadWrite(
- PathBuf, LockFD, sys::fs::CD_OpenAlways, sys::fs::OF_None))
- return createFileError(PathBuf, EC);
- sys::fs::file_t LockFile = sys::fs::convertFDToNativeFile(LockFD);
- llvm::scope_exit CloseLock([&]() { sys::fs::closeFile(LockFile); });
- if (std::error_code EC = tryLockFileThreadSafe(LockFD)) {
- if (EC == std::errc::no_lock_available)
- return createFileError(
- PathBuf, EC,
- "CAS validation requires exclusive access but CAS was in use");
- return createFileError(PathBuf, EC);
- }
- llvm::scope_exit UnlockFD([&]() { unlockFileThreadSafe(LockFD); });
-
- auto DBDirs = getAllDBDirs(RootPath);
- if (!DBDirs)
- return DBDirs.takeError();
-
- for (StringRef DBDir : *DBDirs) {
- sys::path::remove_filename(PathBuf);
- sys::path::append(PathBuf, DBDir);
- std::error_code EC;
- int Attempt = 0, MaxAttempts = 100;
- SmallString<128> GCPath;
- for (; Attempt < MaxAttempts; ++Attempt) {
- GCPath.assign(RootPath);
- sys::path::append(GCPath, CorruptPrefix + std::to_string(Attempt) +
- "." + DBDir);
- EC = sys::fs::rename(PathBuf, GCPath);
- // Darwin uses ENOTEMPTY. Linux may return either ENOTEMPTY or EEXIST.
- if (EC != errc::directory_not_empty && EC != errc::file_exists)
- break;
- }
- if (Attempt == MaxAttempts)
- return createStringError(
- EC, "rename " + PathBuf +
- " failed: too many CAS directories awaiting pruning");
- if (EC)
- return createStringError(EC, "rename " + PathBuf + " to " + GCPath +
- " failed: " + EC.message());
- }
- Recovered = true;
+Expected<ValidationResult> UnifiedOnDiskCache::recover(StringRef RootPath) {
+ std::unique_ptr<LockedValidationFile> VF;
+ if (Error E = LockedValidationFile::open(RootPath).moveInto(VF))
+ return std::move(E);
+
+ std::shared_ptr<ondisk::OnDiskCASLogger> Logger;
+#ifndef _WIN32
+ if (Error E =
+ ondisk::OnDiskCASLogger::openIfEnabled(RootPath).moveInto(Logger))
+ return std::move(E);
+#endif
+
+ uint64_t BootTime = 0;
+ if (Error E = getCachedBootTime().moveInto(BootTime))
+ return std::move(E);
+
+ bool Skipped = false;
+ std::string LogRecoveryError;
+
+ llvm::scope_exit Log([&] {
+ if (!Logger)
+ return;
+ Logger->logUnifiedOnDiskCacheRecover(RootPath, BootTime, LogRecoveryError,
+ Skipped);
+ });
+
+ // Unless validation is still pending, the data has been recovered or
+ // successfully validated since the failed validation, e.g. by a concurrent
+ // process.
+ if (!VF->isValidationPending()) {
+ Skipped = true;
+ return ValidationResult::Skipped;
}
- if (ValidationBootTime != BootTime) {
- // Fix filename in case we have error to report.
- sys::path::remove_filename(PathBuf);
- sys::path::append(PathBuf, ValidationFilename);
- if (std::error_code EC = sys::fs::resize_file(FD, 0))
- return createFileError(PathBuf, EC);
- raw_fd_ostream OS(FD, /*shouldClose=*/false);
- OS.seek(0); // resize does not reset position
- OS << BootTime << '\n';
- if (OS.has_error())
- return createFileError(PathBuf, OS.error());
+ if (Error E = markAllDBDirsCorrupt(RootPath)) {
+ if (Logger)
+ LogRecoveryError = toStringWithoutConsuming(E);
+ return std::move(E);
}
- return NeedsRecovery ? ValidationResult::Recovered : ValidationResult::Valid;
+ if (Error E = VF->setLastValidBootTime(BootTime))
+ return std::move(E);
+ return ValidationResult::Recovered;
}
Expected<std::unique_ptr<UnifiedOnDiskCache>>
diff --git a/llvm/test/CMakeLists.txt b/llvm/test/CMakeLists.txt
index 18f176c3fe17f..56f96b4f3de92 100644
--- a/llvm/test/CMakeLists.txt
+++ b/llvm/test/CMakeLists.txt
@@ -46,6 +46,14 @@ llvm_canonicalize_cmake_booleans(
LLVM_HAVE_OPENCSD
)
+# libCASPluginTest is placed next to the CASTests executable, see
+# unittests/CAS/CMakeLists.txt.
+if (TARGET CASPluginTest)
+ set_llvm_build_mode()
+ set(LLVM_CAS_PLUGIN_TEST_PATH
+ "${LLVM_BINARY_DIR}/unittests/CAS/${LLVM_BUILD_MODE}/${CMAKE_SHARED_LIBRARY_PREFIX}CASPluginTest${LLVM_PLUGIN_EXT}")
+endif()
+
configure_lit_site_cfg(
${CMAKE_CURRENT_SOURCE_DIR}/lit.site.cfg.py.in
${CMAKE_CURRENT_BINARY_DIR}/lit.site.cfg.py
@@ -207,6 +215,10 @@ if (TARGET CGTestPlugin)
list(APPEND LLVM_TEST_DEPENDS CGTestPlugin)
endif()
+if (TARGET CASPluginTest)
+ list(APPEND LLVM_TEST_DEPENDS CASPluginTest)
+endif()
+
if(LLVM_INCLUDE_EXAMPLES)
list(APPEND LLVM_TEST_DEPENDS
Kaleidoscope-Ch3
diff --git a/llvm/test/lit.site.cfg.py.in b/llvm/test/lit.site.cfg.py.in
index 60b22ef5a7a85..f4821eea6d95b 100644
--- a/llvm/test/lit.site.cfg.py.in
+++ b/llvm/test/lit.site.cfg.py.in
@@ -75,6 +75,7 @@ config.have_vc_rev = @LLVM_APPEND_VC_REV@
config.force_vc_rev = "@LLVM_FORCE_VC_REVISION@"
config.has_logf128 = @LLVM_HAS_LOGF128@
config.have_ondisk_cas = @LLVM_ENABLE_ONDISK_CAS@
+config.llvm_cas_plugin_test_path = lit_config.substitute(r"@LLVM_CAS_PLUGIN_TEST_PATH@")
config.have_opencsd = @LLVM_HAVE_OPENCSD@
import lit.llvm
diff --git a/llvm/test/tools/llvm-cas/lit.local.cfg b/llvm/test/tools/llvm-cas/lit.local.cfg
index 379945b68925d..0f6e925d6b4c7 100644
--- a/llvm/test/tools/llvm-cas/lit.local.cfg
+++ b/llvm/test/tools/llvm-cas/lit.local.cfg
@@ -1,2 +1,6 @@
if not config.have_ondisk_cas:
config.unsupported = True
+
+if config.llvm_cas_plugin_test_path:
+ config.available_features.add("cas-plugin")
+ config.substitutions.append(("%cas_plugin", config.llvm_cas_plugin_test_path))
diff --git a/llvm/test/tools/llvm-cas/logging.test b/llvm/test/tools/llvm-cas/logging.test
index 5dc0955eabcb8..df7fc43048ae2 100644
--- a/llvm/test/tools/llvm-cas/logging.test
+++ b/llvm/test/tools/llvm-cas/logging.test
@@ -7,6 +7,8 @@ RUN: env LLVM_CAS_LOG=2 llvm-cas --cas %t/cas --make-blob --data %t/input/a
RUN: env LLVM_CAS_LOG=2 llvm-cas --cas %t/cas --make-blob --data %t/input/large
RUN: env LLVM_CAS_LOG=2 llvm-cas --cas %t/cas --validate-if-needed -check-hash
RUN: env LLVM_CAS_LOG=2 llvm-cas --cas %t/cas --validate-if-needed -force -allow-recovery
+RUN: rm %t/cas/v1.1/data.v1
+RUN: env LLVM_CAS_LOG=2 llvm-cas --cas %t/cas --validate-if-needed -force -allow-recovery
RUN: FileCheck %s --input-file %t/cas/v1.log
RUN: FileCheck %s --input-file %t/cas/v1.log --check-prefix=STANDALONE
@@ -30,8 +32,10 @@ RUN: FileCheck %s --input-file %t/cas/v1.log --check-prefix=STANDALONE
// CHECK: resize mapped file '{{.*}}index.v{{[0-9]+}}'
// CHECK: close mmap '{{.*}}index.v{{[0-9]+}}'
-// CHECK: validate-if-needed '{{.*}}cas' boot=[[BOOT:[0-9]+]] last-valid=0 check-hash=1 allow-recovery=0 force=0 llvm-cas={{.*}}llvm-cas
-// CHECK: validate-if-needed '{{.*}}cas' boot=[[BOOT]] last-valid=[[BOOT]] check-hash=0 allow-recovery=1 force=1 llvm-cas={{.*}}llvm-cas
+// CHECK: validate-if-needed '{{.*}}cas' boot=[[BOOT:[0-9]+]] last-valid=0 check-hash=1 force=0{{$}}
+// CHECK: validate-if-needed '{{.*}}cas' boot=[[BOOT]] last-valid=[[BOOT]] check-hash=0 force=1{{$}}
+// CHECK: validate-if-needed '{{.*}}cas' boot=[[BOOT]] last-valid=[[BOOT]] check-hash=0 force=1 data was invalid
+// CHECK: recover '{{.*}}cas' boot=[[BOOT]]{{$}}
// STANDALONE: standalone file create '[[PATH:.*leaf.[0-9a-f]*.v[0-9]+]].[[SUFFIX:[0-9a-f]*]]'
// STANDALONE: standalone file rename '[[PATH]].[[SUFFIX]]' to '[[PATH]]'
diff --git a/llvm/test/tools/llvm-cas/plugin-validation-crash.test b/llvm/test/tools/llvm-cas/plugin-validation-crash.test
new file mode 100644
index 0000000000000..7f55f98a14e70
--- /dev/null
+++ b/llvm/test/tools/llvm-cas/plugin-validation-crash.test
@@ -0,0 +1,59 @@
+REQUIRES: cas-plugin
+# HWASan does not tag the globals of a dlopen'ed library with glibc, see
+# https://github.com/llvm/llvm-project/issues/57206.
+UNSUPPORTED: hwasan
+
+# Tests recovery from a validation that crashes, using the test plugin's
+# crash-on-validate option which crashes while checking hashes.
+
+RUN: rm -rf %t
+RUN: mkdir %t
+
+RUN: echo "abc" | llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin \
+RUN: --make-blob --data - > %t/abc.casid
+RUN: llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin \
+RUN: --put-cache-key @%t/abc.casid @%t/abc.casid
+RUN: llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin \
+RUN: --validate-if-needed | FileCheck %s --check-prefix=VALID
+
+# Without --allow-recovery a crash in the validation process is an error.
+RUN: not llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin \
+RUN: --fcas-plugin-option crash-on-validate \
+RUN: --validate-if-needed --force --check-hash 2>&1 \
+RUN: | FileCheck %s --check-prefix=INVALID
+RUN: FileCheck %s --check-prefix=PENDING --input-file %t/cas/v1.validation
+
+# The crashed validation is not skipped even though the data was validated
+# earlier during this boot.
+RUN: llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin \
+RUN: --validate-if-needed | FileCheck %s --check-prefix=VALID
+RUN: FileCheck %s --check-prefix=VALIDATED --input-file %t/cas/v1.validation
+RUN: llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin \
+RUN: --validate-if-needed | FileCheck %s --check-prefix=SKIPPED
+
+# An in-process validation that crashes also leaves validation pending.
+RUN: not --crash llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin \
+RUN: --fcas-plugin-option crash-on-validate \
+RUN: --validate-if-needed --in-process --force --check-hash
+RUN: FileCheck %s --check-prefix=PENDING --input-file %t/cas/v1.validation
+
+# With --allow-recovery, llvm-cas recovers after the validation process crashes.
+RUN: llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin \
+RUN: --fcas-plugin-option crash-on-validate \
+RUN: --validate-if-needed --force --check-hash --allow-recovery \
+RUN: | FileCheck %s --check-prefix=RECOVERED
+RUN: ls %t/cas | FileCheck %s --check-prefix=RECOVERED-DIRS
+RUN: FileCheck %s --check-prefix=VALIDATED --input-file %t/cas/v1.validation
+RUN: llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin \
+RUN: --validate-if-needed | FileCheck %s --check-prefix=SKIPPED
+RUN: not llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin \
+RUN: --get-cache-result @%t/abc.casid | FileCheck %s --check-prefix=NOT-FOUND
+
+VALID: validated successfully
+SKIPPED: validation skipped
+INVALID: llvm-cas: validate-if-needed: cas contents invalid
+PENDING: {{^0$}}
+VALIDATED: {{^[1-9][0-9]*$}}
+RECOVERED: recovered from invalid data
+RECOVERED-DIRS: corrupt.0.v1.1
+NOT-FOUND: result not found
diff --git a/llvm/test/tools/llvm-cas/plugin-validation.test b/llvm/test/tools/llvm-cas/plugin-validation.test
new file mode 100644
index 0000000000000..6c7ef67a0024e
--- /dev/null
+++ b/llvm/test/tools/llvm-cas/plugin-validation.test
@@ -0,0 +1,73 @@
+REQUIRES: cas-plugin
+# HWASan does not tag the globals of a dlopen'ed library with glibc, see
+# https://github.com/llvm/llvm-project/issues/57206.
+UNSUPPORTED: hwasan
+
+RUN: rm -rf %t
+RUN: mkdir %t
+
+RUN: echo "abc" | llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin \
+RUN: --make-blob --data - > %t/abc.casid
+RUN: llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin \
+RUN: --make-node --data /dev/null @%t/abc.casid > %t/node.casid
+RUN: llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin \
+RUN: --put-cache-key @%t/abc.casid @%t/node.casid
+
+RUN: llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin --validate \
+RUN: | FileCheck %s --check-prefix=VALID
+RUN: llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin --validate \
+RUN: --check-hash | FileCheck %s --check-prefix=VALID
+
+# Validate once per boot unless forced. The output of the validation process
+# is forwarded.
+RUN: llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin \
+RUN: --validate-if-needed --check-hash | FileCheck %s --check-prefix=VALID
+RUN: llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin \
+RUN: --validate-if-needed --check-hash | FileCheck %s --check-prefix=SKIPPED
+RUN: llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin \
+RUN: --validate-if-needed --force | FileCheck %s --check-prefix=VALID
+RUN: llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin \
+RUN: --validate-if-needed --in-process | FileCheck %s --check-prefix=SKIPPED
+RUN: llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin \
+RUN: --validate-if-needed --in-process --force \
+RUN: | FileCheck %s --check-prefix=VALID
+
+# Nothing to recover from.
+RUN: llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin \
+RUN: --validate-if-needed --force --allow-recovery \
+RUN: | FileCheck %s --check-prefix=VALID
+
+# Plugin options are passed through.
+RUN: not llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin \
+RUN: --fcas-plugin-option bogus=1 --validate-if-needed 2>&1 \
+RUN: | FileCheck %s --check-prefix=BAD-OPTION
+
+RUN: rm %t/cas/v1.1/data.v1
+RUN: not llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin --validate
+RUN: not llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin \
+RUN: --validate-if-needed --force 2>&1 | FileCheck %s --check-prefix=INVALID
+RUN: not llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin \
+RUN: --validate-if-needed --in-process --force 2>&1 \
+RUN: | FileCheck %s --check-prefix=INVALID
+RUN: llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin \
+RUN: --validate-if-needed --force --allow-recovery \
+RUN: | FileCheck %s --check-prefix=RECOVERED
+RUN: ls %t/cas | FileCheck %s --check-prefix=CORRUPT-DIR
+
+# Recovery also counts as validation for this boot.
+RUN: llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin \
+RUN: --validate-if-needed | FileCheck %s --check-prefix=SKIPPED
+
+# The invalid data was discarded.
+RUN: llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin --validate \
+RUN: | FileCheck %s --check-prefix=VALID
+RUN: not llvm-cas --cas %t/cas --fcas-plugin-path %cas_plugin \
+RUN: --get-cache-result @%t/abc.casid | FileCheck %s --check-prefix=NOT-FOUND
+
+VALID: validated successfully
+SKIPPED: validation skipped
+BAD-OPTION: llvm-cas: validate-if-needed: unknown option: bogus
+INVALID: llvm-cas: validate-if-needed:
+RECOVERED: recovered from invalid data
+CORRUPT-DIR: corrupt.0.v1.1
+NOT-FOUND: result not found
diff --git a/llvm/tools/libCASPluginTest/libCASPluginTest.cpp b/llvm/tools/libCASPluginTest/libCASPluginTest.cpp
index 282760073962c..ec71dc3a57c6c 100644
--- a/llvm/tools/libCASPluginTest/libCASPluginTest.cpp
+++ b/llvm/tools/libCASPluginTest/libCASPluginTest.cpp
@@ -100,6 +100,9 @@ struct CASPluginOptions {
std::string SecondPrefix;
bool SimulateMissingObjects = false;
bool Logging = true;
+ /// If true, \c llcas_cas_validate_if_needed crashes while checking hashes,
+ /// for testing recovery from a crashed validation.
+ bool CrashOnValidate = false;
Error setOption(StringRef Name, StringRef Value);
};
@@ -119,6 +122,8 @@ Error CASPluginOptions::setOption(StringRef Name, StringRef Value) {
SimulateMissingObjects = true;
else if (Name == "no-logging")
Logging = false;
+ else if (Name == "crash-on-validate")
+ CrashOnValidate = true;
else
return createStringError(errc::invalid_argument,
Twine("unknown option: ") + Name);
@@ -449,6 +454,60 @@ bool llcas_cas_prune_ondisk_data(llcas_cas_t c_cas, char **error) {
return false;
}
+static void hashObject(ArrayRef<ArrayRef<uint8_t>> Refs, ArrayRef<char> Data,
+ SmallVectorImpl<uint8_t> &Result) {
+ HashType Digest = BuiltinObjectHasher<HasherT>::hashObject(Refs, Data);
+ Result.assign(Digest.begin(), Digest.end());
+}
+
+bool llcas_cas_validate(llcas_cas_t c_cas, bool check_hash, char **error) {
+ if (Error E =
+ unwrap(c_cas)->DB->getGraphDB().validate(check_hash, hashObject))
+ return reportError(std::move(E), error, true);
+ return false;
+}
+
+static llcas_validation_result_t
+toValidationResult(Expected<ValidationResult> Result, char **error) {
+ if (!Result)
+ return reportError(Result.takeError(), error,
+ LLCAS_VALIDATION_RESULT_ERROR);
+ switch (*Result) {
+ case ValidationResult::Valid:
+ return LLCAS_VALIDATION_RESULT_VALID;
+ case ValidationResult::Recovered:
+ return LLCAS_VALIDATION_RESULT_RECOVERED;
+ case ValidationResult::Skipped:
+ return LLCAS_VALIDATION_RESULT_SKIPPED;
+ }
+ llvm_unreachable("unknown ValidationResult value");
+}
+
+llcas_validation_result_t
+llcas_cas_validate_if_needed(llcas_cas_options_t c_opts, bool check_hash,
+ bool force, char **error) {
+ auto &Opts = *unwrap(c_opts);
+ setSmallMaxMappingSize();
+ auto HashFn = [&](ArrayRef<ArrayRef<uint8_t>> Refs, ArrayRef<char> Data,
+ SmallVectorImpl<uint8_t> &Result) {
+ if (Opts.CrashOnValidate)
+ abort();
+ hashObject(Refs, Data, Result);
+ };
+ return toValidationResult(UnifiedOnDiskCache::validateIfNeeded(
+ Opts.OnDiskPath,
+ PluginCASContext::getHashName(),
+ sizeof(HashType), check_hash, HashFn, force),
+ error);
+}
+
+llcas_validation_result_t
+llcas_cas_recover_ondisk_data(llcas_cas_options_t c_opts, char **error) {
+ auto &Opts = *unwrap(c_opts);
+ return toValidationResult(UnifiedOnDiskCache::recover(Opts.OnDiskPath),
+ error);
+}
+
void llcas_cas_options_set_client_version(llcas_cas_options_t, unsigned major,
unsigned minor) {
// Ignore for now.
@@ -820,3 +879,9 @@ void llcas_actioncache_put_for_digest_async(
cb(ctx_cb, failed, c_err);
});
}
+
+bool llcas_actioncache_validate(llcas_cas_t c_cas, char **error) {
+ if (Error E = unwrap(c_cas)->DB->validateActionCache())
+ return reportError(std::move(E), error, true);
+ return false;
+}
diff --git a/llvm/tools/libCASPluginTest/libCASPluginTest.exports b/llvm/tools/libCASPluginTest/libCASPluginTest.exports
index 1c292e4da1ccf..6ecfae9093aa7 100644
--- a/llvm/tools/libCASPluginTest/libCASPluginTest.exports
+++ b/llvm/tools/libCASPluginTest/libCASPluginTest.exports
@@ -2,6 +2,7 @@ llcas_actioncache_get_for_digest
llcas_actioncache_get_for_digest_async
llcas_actioncache_put_for_digest
llcas_actioncache_put_for_digest_async
+llcas_actioncache_validate
llcas_cancellable_cancel
llcas_cancellable_dispose
llcas_cas_contains_object
@@ -18,8 +19,11 @@ llcas_cas_options_set_client_version
llcas_cas_options_set_ondisk_path
llcas_cas_options_set_option
llcas_cas_prune_ondisk_data
+llcas_cas_recover_ondisk_data
llcas_cas_set_ondisk_size_limit
llcas_cas_store_object
+llcas_cas_validate
+llcas_cas_validate_if_needed
llcas_digest_parse
llcas_digest_print
llcas_get_plugin_version
diff --git a/llvm/tools/llvm-cas/Options.td b/llvm/tools/llvm-cas/Options.td
index 5ae64c104fdb6..9e8eb0a242f10 100644
--- a/llvm/tools/llvm-cas/Options.td
+++ b/llvm/tools/llvm-cas/Options.td
@@ -44,6 +44,14 @@ def cas_path : Separate<["-", "--"], "cas">,
MetaVarName<"<path>">,
HelpText<"Path to CAS on disk">;
+def cas_plugin_path : Separate<["-", "--"], "fcas-plugin-path">,
+ MetaVarName<"<path>">,
+ HelpText<"Path to plugin CAS library">;
+
+def cas_plugin_option : Separate<["-", "--"], "fcas-plugin-option">,
+ MetaVarName<"<name>=<value>">,
+ HelpText<"Option passed to the plugin CAS">;
+
def upstream_cas : Separate<["-", "--"], "upstream-cas">,
MetaVarName<"<path>">,
HelpText<"Path to another upstream CAS">;
diff --git a/llvm/tools/llvm-cas/llvm-cas.cpp b/llvm/tools/llvm-cas/llvm-cas.cpp
index d4d79336b5d75..f996c3d3a79f3 100644
--- a/llvm/tools/llvm-cas/llvm-cas.cpp
+++ b/llvm/tools/llvm-cas/llvm-cas.cpp
@@ -10,6 +10,7 @@
///
//===----------------------------------------------------------------------===//
+#include "llvm/ADT/StringExtras.h"
#include "llvm/CAS/ActionCache.h"
#include "llvm/CAS/BuiltinUnifiedCASDatabases.h"
#include "llvm/CAS/ObjectStore.h"
@@ -20,6 +21,7 @@
#include "llvm/Support/Error.h"
#include "llvm/Support/InitLLVM.h"
#include "llvm/Support/MemoryBuffer.h"
+#include "llvm/Support/Program.h"
#include "llvm/Support/raw_ostream.h"
using namespace llvm;
@@ -62,6 +64,8 @@ struct CommandOptions {
CommandKind Command = CommandKind::Invalid;
std::vector<std::string> Inputs;
std::string CASPath;
+ std::string CASPluginPath;
+ SmallVector<std::pair<std::string, std::string>> CASPluginOpts;
std::string UpstreamCASPath;
std::string DataPath;
bool CheckHash;
@@ -122,9 +126,7 @@ static int putCacheKey(ObjectStore &CAS, ActionCache &AC,
static int getCacheResult(ObjectStore &CAS, ActionCache &AC, const CASID &ID);
static int validateObject(ObjectStore &CAS, const CASID &ID);
static int validate(ObjectStore &CAS, ActionCache &AC, bool CheckHash);
-static int validateIfNeeded(StringRef Path, bool CheckHash, bool Force,
- bool AllowRecovery, bool InProcess,
- const char *Argv0);
+static int validateIfNeeded(const CommandOptions &Opts, const char *Argv0);
static int prune(cas::ObjectStore &CAS);
static Expected<CommandOptions> parseOptions(int Argc, char **Argv) {
@@ -160,6 +162,11 @@ static Expected<CommandOptions> parseOptions(int Argc, char **Argv) {
for (auto *File : Args.filtered(OPT_INPUT))
Opts.Inputs.push_back(File->getValue());
Opts.CASPath = Args.getLastArgValue(OPT_cas_path);
+ Opts.CASPluginPath = Args.getLastArgValue(OPT_cas_plugin_path);
+ for (StringRef PluginOpt : Args.getAllArgValues(OPT_cas_plugin_option)) {
+ auto [Name, Value] = PluginOpt.split('=');
+ Opts.CASPluginOpts.emplace_back(Name, Value);
+ }
Opts.UpstreamCASPath = Args.getLastArgValue(OPT_upstream_cas);
Opts.DataPath = Args.getLastArgValue(OPT_data);
Opts.CheckHash = Args.hasArg(OPT_check_hash);
@@ -184,10 +191,16 @@ int main(int Argc, char **Argv) {
auto Opts = ExitOnErr(parseOptions(Argc, Argv));
if (Opts.Command == CommandKind::ValidateIfNeeded)
- return validateIfNeeded(Opts.CASPath, Opts.CheckHash, Opts.Force,
- Opts.AllowRecovery, Opts.InProcess, Argv[0]);
-
- auto [CAS, AC] = ExitOnErr(createOnDiskUnifiedCASDatabases(Opts.CASPath));
+ return validateIfNeeded(Opts, Argv[0]);
+
+ std::shared_ptr<ObjectStore> CAS;
+ std::shared_ptr<ActionCache> AC;
+ if (!Opts.CASPluginPath.empty())
+ std::tie(CAS, AC) = ExitOnErr(createPluginCASDatabases(
+ Opts.CASPluginPath, Opts.CASPath, Opts.CASPluginOpts));
+ else
+ std::tie(CAS, AC) =
+ ExitOnErr(createOnDiskUnifiedCASDatabases(Opts.CASPath));
assert(CAS);
if (Opts.Command == CommandKind::Dump)
@@ -361,28 +374,86 @@ int validate(ObjectStore &CAS, ActionCache &AC, bool CheckHash) {
return 0;
}
-int validateIfNeeded(StringRef Path, bool CheckHash, bool Force,
- bool AllowRecovery, bool InProcess, const char *Argv0) {
- ExitOnError ExitOnErr("llvm-cas: validate-if-needed: ");
- std::string ExecStorage;
- std::optional<StringRef> Exec;
- if (!InProcess) {
- ExecStorage = sys::fs::getMainExecutable(Argv0, (void *)validateIfNeeded);
- Exec = ExecStorage;
+/// Validates the CAS in this process and prints the result.
+static Error validateInProcess(const CommandOptions &Opts) {
+ ValidationResult Result;
+ if (Error E = (Opts.CASPluginPath.empty()
+ ? validateOnDiskUnifiedCASDatabasesIfNeeded(
+ Opts.CASPath, Opts.CheckHash, Opts.Force)
+ : validatePluginCASDatabasesIfNeeded(
+ Opts.CASPluginPath, Opts.CASPath, Opts.CASPluginOpts,
+ Opts.CheckHash, Opts.Force))
+ .moveInto(Result))
+ return E;
+ outs() << (Result == ValidationResult::Skipped ? "validation skipped\n"
+ : "validated successfully\n");
+ return Error::success();
+}
+
+/// Validates the CAS by re-executing llvm-cas with --in-process, which
+/// protects against crashes during validation. The output of the child process
+/// is forwarded.
+///
+/// \returns false if validation failed or crashed.
+static Expected<bool> validateOutOfProcess(const CommandOptions &Opts,
+ const char *Argv0) {
+ std::string Exec =
+ sys::fs::getMainExecutable(Argv0, (void *)validateOutOfProcess);
+ SmallVector<std::string> PluginOpts;
+ for (const auto &[Name, Value] : Opts.CASPluginOpts)
+ PluginOpts.push_back(Name + "=" + Value);
+
+ SmallVector<StringRef> Args{Exec, "--cas", Opts.CASPath};
+ if (!Opts.CASPluginPath.empty()) {
+ Args.append({"--fcas-plugin-path", Opts.CASPluginPath});
+ for (StringRef PluginOpt : PluginOpts)
+ Args.append({"--fcas-plugin-option", PluginOpt});
}
- ValidationResult Result = ExitOnErr(validateOnDiskUnifiedCASDatabasesIfNeeded(
- Path, CheckHash, AllowRecovery, Force, Exec));
- switch (Result) {
- case ValidationResult::Valid:
- outs() << "validated successfully\n";
- break;
- case ValidationResult::Recovered:
- outs() << "recovered from invalid data\n";
- break;
- case ValidationResult::Skipped:
- outs() << "validation skipped\n";
- break;
+ Args.append({"--validate-if-needed", "--in-process"});
+ if (Opts.CheckHash)
+ Args.push_back("--check-hash");
+ if (Opts.Force)
+ Args.push_back("--force");
+
+ outs().flush();
+ std::string ErrMsg;
+ int Result = sys::ExecuteAndWait(Exec, Args, /*Env=*/std::nullopt,
+ /*Redirects=*/{}, /*SecondsToWait=*/120,
+ /*MemoryLimit=*/0, &ErrMsg);
+ if (Result == -1)
+ return createStringError("failed to exec " + join(Args, " ") + ": " +
+ ErrMsg);
+ if (Result == -2)
+ errs() << "llvm-cas: validate-if-needed: validation crashed: " << ErrMsg
+ << "\n";
+ return Result == 0;
+}
+
+int validateIfNeeded(const CommandOptions &Opts, const char *Argv0) {
+ ExitOnError ExitOnErr("llvm-cas: validate-if-needed: ");
+ if (Opts.InProcess) {
+ Error E = validateInProcess(Opts);
+ if (!E)
+ return 0;
+ if (!Opts.AllowRecovery)
+ ExitOnErr(std::move(E));
+ errs() << "llvm-cas: validate-if-needed: " << toString(std::move(E))
+ << "\n";
+ } else {
+ if (ExitOnErr(validateOutOfProcess(Opts, Argv0)))
+ return 0;
+ if (!Opts.AllowRecovery)
+ ExitOnErr(createStringError("cas contents invalid"));
}
+
+ ValidationResult Result = ExitOnErr(
+ Opts.CASPluginPath.empty()
+ ? recoverOnDiskUnifiedCASDatabases(Opts.CASPath)
+ : recoverPluginCASDatabases(Opts.CASPluginPath, Opts.CASPath,
+ Opts.CASPluginOpts));
+ outs() << (Result == ValidationResult::Skipped
+ ? "recovery skipped\n"
+ : "recovered from invalid data\n");
return 0;
}
diff --git a/llvm/unittests/CAS/PluginCASTest.cpp b/llvm/unittests/CAS/PluginCASTest.cpp
index 376779a387a01..d417d6c6b7bf5 100644
--- a/llvm/unittests/CAS/PluginCASTest.cpp
+++ b/llvm/unittests/CAS/PluginCASTest.cpp
@@ -131,4 +131,95 @@ TEST(PluginCASTest, isMaterialized) {
}
}
+TEST(PluginCASTest, validate) {
+ unittest::TempDir Temp("plugin-cas", /*Unique=*/true);
+
+ auto validateIfNeeded = [&](bool Force) {
+ return validatePluginCASDatabasesIfNeeded(getCASPluginPath(), Temp.path(),
+ /*PluginArgs=*/{},
+ /*CheckHash=*/true, Force);
+ };
+ auto recover = [&]() {
+ return recoverPluginCASDatabases(getCASPluginPath(), Temp.path(),
+ /*PluginArgs=*/{});
+ };
+ auto openCAS = [&]() {
+ std::optional<
+ std::pair<std::shared_ptr<ObjectStore>, std::shared_ptr<ActionCache>>>
+ DBs;
+ EXPECT_THAT_ERROR(createPluginCASDatabases(getCASPluginPath(), Temp.path(),
+ /*PluginArgs=*/{})
+ .moveInto(DBs),
+ Succeeded());
+ return DBs;
+ };
+
+ std::optional<ValidationResult> Result;
+ ASSERT_THAT_ERROR(validateIfNeeded(/*Force=*/false).moveInto(Result),
+ Succeeded());
+ EXPECT_EQ(Result, ValidationResult::Valid);
+
+ {
+ auto DBs = openCAS();
+ ASSERT_TRUE(DBs);
+ auto &[CAS, AC] = *DBs;
+
+ std::optional<CASID> ID1, ID2;
+ ASSERT_THAT_ERROR(CAS->createProxy({}, "1").moveInto(ID1), Succeeded());
+ ASSERT_THAT_ERROR(CAS->createProxy({}, "2").moveInto(ID2), Succeeded());
+ ASSERT_THAT_ERROR(AC->put(*ID1, *ID2), Succeeded());
+
+ EXPECT_THAT_ERROR(CAS->validate(/*CheckHash=*/true), Succeeded());
+ EXPECT_THAT_ERROR(AC->validate(), Succeeded());
+ }
+
+ // Already validated since boot.
+ ASSERT_THAT_ERROR(validateIfNeeded(/*Force=*/false).moveInto(Result),
+ Succeeded());
+ EXPECT_EQ(Result, ValidationResult::Skipped);
+
+ ASSERT_THAT_ERROR(validateIfNeeded(/*Force=*/true).moveInto(Result),
+ Succeeded());
+ EXPECT_EQ(Result, ValidationResult::Valid);
+
+ // Nothing to recover from after a successful validation.
+ ASSERT_THAT_ERROR(recover().moveInto(Result), Succeeded());
+ EXPECT_EQ(Result, ValidationResult::Skipped);
+ EXPECT_TRUE(sys::fs::exists(Temp.path("v1.1")));
+
+ // Invalidate the data.
+ ASSERT_FALSE(sys::fs::remove(Temp.path("v1.1/data.v1")));
+ EXPECT_THAT_EXPECTED(validateIfNeeded(/*Force=*/true), Failed());
+
+ // Recovery requires exclusive access, and fails rather than waiting for it.
+ {
+ auto DBs = openCAS();
+ ASSERT_TRUE(DBs);
+ EXPECT_THAT_EXPECTED(recover(), Failed());
+ }
+
+ ASSERT_THAT_ERROR(recover().moveInto(Result), Succeeded());
+ EXPECT_EQ(Result, ValidationResult::Recovered);
+ EXPECT_FALSE(sys::fs::exists(Temp.path("v1.1")));
+
+ // A concurrent recovery for the same failed validation is skipped.
+ ASSERT_THAT_ERROR(recover().moveInto(Result), Succeeded());
+ EXPECT_EQ(Result, ValidationResult::Skipped);
+
+ // Recovery counts as validation for this boot.
+ ASSERT_THAT_ERROR(validateIfNeeded(/*Force=*/false).moveInto(Result),
+ Succeeded());
+ EXPECT_EQ(Result, ValidationResult::Skipped);
+
+ std::pair<std::string, std::string> BadOpts[] = {{"bogus", ""}};
+ EXPECT_THAT_EXPECTED(
+ validatePluginCASDatabasesIfNeeded(getCASPluginPath(), Temp.path(),
+ BadOpts, /*CheckHash=*/false,
+ /*ForceValidation=*/true),
+ Failed());
+ EXPECT_THAT_EXPECTED(
+ recoverPluginCASDatabases(getCASPluginPath(), Temp.path(), BadOpts),
+ Failed());
+}
+
#endif /* !LLVM_HWADDRESS_SANITIZER_BUILD */
diff --git a/llvm/unittests/CAS/UnifiedOnDiskCacheTest.cpp b/llvm/unittests/CAS/UnifiedOnDiskCacheTest.cpp
index 084a61d90e879..800760ac569ca 100644
--- a/llvm/unittests/CAS/UnifiedOnDiskCacheTest.cpp
+++ b/llvm/unittests/CAS/UnifiedOnDiskCacheTest.cpp
@@ -9,6 +9,7 @@
#include "llvm/CAS/UnifiedOnDiskCache.h"
#include "CASTestConfig.h"
#include "OnDiskCommonUtils.h"
+#include "llvm/Support/ThreadPool.h"
#include "llvm/Testing/Support/Error.h"
#include "llvm/Testing/Support/SupportHelpers.h"
#include "gtest/gtest.h"
@@ -134,9 +135,7 @@ TEST_P(CustomHasherOnDiskCASTest, UnifiedOnDiskCacheTest) {
std::optional<ValidationResult> ValidationRes;
ASSERT_THAT_ERROR(UnifiedOnDiskCache::validateIfNeeded(
Temp.path(), HashName, HashSize, /*CheckHash=*/true,
- HashFn, /*AllowRecovery=*/false,
- /*ForceValidation=*/true,
- /*LLVMCasBinary=*/std::nullopt)
+ HashFn, /*ForceValidation=*/true)
.moveInto(ValidationRes),
Succeeded());
ASSERT_EQ(ValidationRes, ValidationResult::Valid);
@@ -211,3 +210,102 @@ TEST_P(CustomHasherOnDiskCASTest, UnifiedOnDiskCacheTest) {
EXPECT_FALSE(Val.has_value());
}
}
+
+TEST_P(CustomHasherOnDiskCASTest, UnifiedOnDiskCacheConcurrentValidation) {
+ auto HashFn = GetParam().HashFn;
+ StringRef HashName = GetParam().HashName;
+ size_t HashSize = GetParam().HashSize;
+
+ auto createCAS = [&](StringRef Path) {
+ std::unique_ptr<UnifiedOnDiskCache> UniDB;
+ ASSERT_THAT_ERROR(UnifiedOnDiskCache::open(Path, /*SizeLimit=*/std::nullopt,
+ HashName, HashSize)
+ .moveInto(UniDB),
+ Succeeded());
+ std::optional<ObjectID> ID;
+ ASSERT_THAT_ERROR(store(UniDB->getGraphDB(), "1", {}).moveInto(ID),
+ Succeeded());
+ };
+ auto validate = [&](StringRef Path, bool Force) {
+ return UnifiedOnDiskCache::validateIfNeeded(
+ Path, HashName, HashSize, /*CheckHash=*/true, HashFn, Force);
+ };
+ auto countCorruptDirs = [](StringRef Path) {
+ unsigned Count = 0;
+ std::error_code EC;
+ for (sys::fs::directory_iterator DirI(Path, EC), DirE; !EC && DirI != DirE;
+ DirI.increment(EC))
+ if (sys::path::filename(DirI->path()).starts_with("corrupt."))
+ ++Count;
+ EXPECT_FALSE(EC);
+ return Count;
+ };
+
+ // Runs \p Fn from multiple threads concurrently, and returns the number of
+ // times each result occurred. Errors are reported as test failures.
+ constexpr unsigned NumTasks = 16;
+ auto runConcurrently = [&](function_ref<Expected<ValidationResult>()> Fn) {
+ std::optional<ValidationResult> Results[NumTasks];
+ std::string Errors[NumTasks];
+ DefaultThreadPool Pool;
+ for (unsigned I = 0; I != NumTasks; ++I)
+ Pool.async([&, I] {
+ if (Error E = Fn().moveInto(Results[I]))
+ Errors[I] = toString(std::move(E));
+ });
+ Pool.wait();
+
+ std::map<ValidationResult, unsigned> Counts;
+ for (unsigned I = 0; I != NumTasks; ++I) {
+ EXPECT_EQ(Errors[I], "");
+ if (Results[I])
+ ++Counts[*Results[I]];
+ }
+ return Counts;
+ };
+
+ // Only one of the concurrent validations of valid data is performed, the
+ // rest see that it has been validated during this boot.
+ {
+ unittest::TempDir Temp("ondisk-unified", /*Unique=*/true);
+ createCAS(Temp.path());
+ auto Counts =
+ runConcurrently([&] { return validate(Temp.path(), /*Force=*/false); });
+ EXPECT_EQ(Counts[ValidationResult::Valid], 1u);
+ EXPECT_EQ(Counts[ValidationResult::Skipped], NumTasks - 1);
+
+ Counts =
+ runConcurrently([&] { return validate(Temp.path(), /*Force=*/true); });
+ EXPECT_EQ(Counts[ValidationResult::Valid], NumTasks);
+ }
+
+ // Concurrently validate invalid data and recover if validation fails, as
+ // done by `llvm-cas -validate-if-needed -allow-recovery`. Exactly one of the
+ // recoveries is performed and none of them fail, regardless of how the
+ // validations and recoveries interleave.
+ for (bool Force : {false, true}) {
+ SCOPED_TRACE(Force ? "Force" : "NoForce");
+ unittest::TempDir Temp("ondisk-unified", /*Unique=*/true);
+ createCAS(Temp.path());
+ ASSERT_FALSE(sys::fs::remove(Temp.path("v1.1/data.v1")));
+
+ auto Counts = runConcurrently([&]() -> Expected<ValidationResult> {
+ Expected<ValidationResult> Result = validate(Temp.path(), Force);
+ if (Result)
+ return Result;
+ consumeError(Result.takeError());
+ return UnifiedOnDiskCache::recover(Temp.path());
+ });
+ EXPECT_EQ(Counts[ValidationResult::Recovered], 1u);
+ EXPECT_EQ(countCorruptDirs(Temp.path()), 1u);
+
+ // Recovery counts as validation for this boot.
+ std::optional<ValidationResult> Result;
+ ASSERT_THAT_ERROR(validate(Temp.path(), /*Force=*/false).moveInto(Result),
+ Succeeded());
+ EXPECT_EQ(Result, ValidationResult::Skipped);
+ ASSERT_THAT_ERROR(UnifiedOnDiskCache::recover(Temp.path()).moveInto(Result),
+ Succeeded());
+ EXPECT_EQ(Result, ValidationResult::Skipped);
+ }
+}
>From 1724e3667a56e0834ef208ef317a565c2ec78bec Mon Sep 17 00:00:00 2001
From: Steven Wu <stevenwu at apple.com>
Date: Thu, 24 Sep 2026 14:13:20 -0700
Subject: [PATCH 2/2] fix tests on windows (that are pre-existing issues)
Created using spr 1.3.7
---
llvm/include/llvm/CAS/UnifiedOnDiskCache.h | 3 +-
llvm/lib/CAS/OnDiskCommon.h | 2 +-
llvm/lib/CAS/UnifiedOnDiskCache.cpp | 46 +++++++++++------
llvm/test/tools/llvm-cas/lit.local.cfg | 8 +++
.../llvm-cas/plugin-validation-crash.test | 9 +++-
.../tools/llvm-cas/plugin-validation.test | 3 ++
llvm/unittests/CAS/OnDiskCommonUtils.h | 13 +++++
llvm/unittests/CAS/PluginCASTest.cpp | 7 ++-
llvm/unittests/CAS/UnifiedOnDiskCacheTest.cpp | 51 +++++++++++++++++--
9 files changed, 117 insertions(+), 25 deletions(-)
diff --git a/llvm/include/llvm/CAS/UnifiedOnDiskCache.h b/llvm/include/llvm/CAS/UnifiedOnDiskCache.h
index 554ca5e0cceba..5aa03130c8844 100644
--- a/llvm/include/llvm/CAS/UnifiedOnDiskCache.h
+++ b/llvm/include/llvm/CAS/UnifiedOnDiskCache.h
@@ -73,7 +73,8 @@ class UnifiedOnDiskCache {
/// Validate the data in \p Path in-process, if it has not been validated
/// since the last system boot. A successful validation is recorded so that
/// subsequent calls can skip it; a failed or crashed one is recorded as
- /// pending for \c recover, and is not skipped by subsequent calls.
+ /// pending for \c recover, and is not skipped by subsequent calls. Where the
+ /// boot time is not known validation is never skipped.
///
/// Validation can crash on invalid data. Clients that want to be resilient
/// to that should call this from a separate process (e.g. via
diff --git a/llvm/lib/CAS/OnDiskCommon.h b/llvm/lib/CAS/OnDiskCommon.h
index 4d2661c7b842e..c1d8adeac4240 100644
--- a/llvm/lib/CAS/OnDiskCommon.h
+++ b/llvm/lib/CAS/OnDiskCommon.h
@@ -68,7 +68,7 @@ Expected<size_t> preallocateFileTail(int FD, size_t CurrentSize,
///
/// \returns the boot time in seconds (0 if operation not supported), or an \c
/// Error.
-Expected<uint64_t> getBootTime();
+LLVM_ABI_FOR_TEST Expected<uint64_t> getBootTime();
/// Helper RAII class for copying a file to a unique file path. At destruction
/// time it will delete any new temporary files created.
diff --git a/llvm/lib/CAS/UnifiedOnDiskCache.cpp b/llvm/lib/CAS/UnifiedOnDiskCache.cpp
index ce921a1ef7f43..ebc545e9da6ee 100644
--- a/llvm/lib/CAS/UnifiedOnDiskCache.cpp
+++ b/llvm/lib/CAS/UnifiedOnDiskCache.cpp
@@ -53,7 +53,8 @@
/// the underlying on-disk storage. The low-level storage is designed to remain
/// coherent across regular process crashes, but may be invalid after power loss
/// or similar system failures. \c UnifiedOnDiskCache::validateIfNeeded allows
-/// validating the contents once per boot, and if validation fails (or crashes,
+/// validating the contents once per boot (or every time, where the boot time is
+/// not known), and if validation fails (or crashes,
/// when performed in a separate process) \c UnifiedOnDiskCache::recover can
/// recover by marking invalid data for garbage collection.
///
@@ -93,6 +94,7 @@
#include "llvm/Support/IOSandbox.h"
#include "llvm/Support/Path.h"
#include "llvm/Support/raw_ostream.h"
+#include <limits>
#include <optional>
using namespace llvm;
@@ -233,11 +235,11 @@ static Error validateInProcess(StringRef RootPath, StringRef HashName,
return Error::success();
}
-static Expected<uint64_t> getCachedBootTime() {
- static uint64_t BootTime = 0;
- if (BootTime == 0)
- if (Error E = getBootTime().moveInto(BootTime))
- return std::move(E);
+/// \returns the boot time, or 0 if it is not known, including if getting it
+/// failed. Validation is never skipped where the boot time is not known.
+static uint64_t getCachedBootTime() {
+ static const uint64_t BootTime =
+ expectedToOptional(getBootTime()).value_or(0);
return BootTime;
}
@@ -259,7 +261,8 @@ class LockedValidationFile {
/// Written before validating. It is an integer so that older versions, which
/// only know about boot times, still parse the file and treat it as not
/// validated. It never matches a boot time.
- static constexpr uint64_t ValidationPending = 0;
+ static constexpr uint64_t ValidationPending =
+ std::numeric_limits<uint64_t>::max();
static Expected<std::unique_ptr<LockedValidationFile>>
open(StringRef RootPath) {
@@ -302,7 +305,16 @@ class LockedValidationFile {
/// \returns the boot time of the last successful validation or recovery, or
/// 0 if there is none.
- uint64_t getLastValidBootTime() const { return State.value_or(0); }
+ uint64_t getLastValidBootTime() const {
+ return isValidationPending() ? 0 : State.value_or(0);
+ }
+
+ /// Whether the data was validated or recovered during the boot with
+ /// \p BootTime. Always false where the boot time is not known, i.e. 0,
+ /// since it cannot be told whether that was during the current boot.
+ bool isValidAtBoot(uint64_t BootTime) const {
+ return BootTime != 0 && State == BootTime;
+ }
bool isValidationPending() const { return State == ValidationPending; }
@@ -371,6 +383,14 @@ static Error markAllDBDirsCorrupt(StringRef RootPath) {
GCPath.assign(RootPath);
sys::path::append(GCPath,
CorruptPrefix + std::to_string(Attempt) + "." + DBDir);
+ // Skip names that are taken by earlier recoveries. Checking the error of
+ // the rename is not enough since Windows reports permission denied when
+ // the destination directory exists. Only garbage collection removes
+ // these directories concurrently, and it never creates them.
+ if (sys::fs::exists(GCPath)) {
+ EC = errc::file_exists;
+ continue;
+ }
EC = sys::fs::rename(PathBuf, GCPath);
// Darwin uses ENOTEMPTY. Linux may return either ENOTEMPTY or EEXIST.
if (EC != errc::directory_not_empty && EC != errc::file_exists)
@@ -401,9 +421,7 @@ Expected<ValidationResult> UnifiedOnDiskCache::validateIfNeeded(
return std::move(E);
#endif
- uint64_t BootTime = 0;
- if (Error E = getCachedBootTime().moveInto(BootTime))
- return std::move(E);
+ uint64_t BootTime = getCachedBootTime();
uint64_t ValidationBootTime = VF->getLastValidBootTime();
bool Skipped = false;
@@ -417,7 +435,7 @@ Expected<ValidationResult> UnifiedOnDiskCache::validateIfNeeded(
LogValidationError, Skipped);
});
- if (ValidationBootTime == BootTime && !ForceValidation) {
+ if (VF->isValidAtBoot(BootTime) && !ForceValidation) {
Skipped = true;
return ValidationResult::Skipped;
}
@@ -451,9 +469,7 @@ Expected<ValidationResult> UnifiedOnDiskCache::recover(StringRef RootPath) {
return std::move(E);
#endif
- uint64_t BootTime = 0;
- if (Error E = getCachedBootTime().moveInto(BootTime))
- return std::move(E);
+ uint64_t BootTime = getCachedBootTime();
bool Skipped = false;
std::string LogRecoveryError;
diff --git a/llvm/test/tools/llvm-cas/lit.local.cfg b/llvm/test/tools/llvm-cas/lit.local.cfg
index 0f6e925d6b4c7..2ee6eb53c7d54 100644
--- a/llvm/test/tools/llvm-cas/lit.local.cfg
+++ b/llvm/test/tools/llvm-cas/lit.local.cfg
@@ -1,6 +1,14 @@
+import platform
+
if not config.have_ondisk_cas:
config.unsupported = True
if config.llvm_cas_plugin_test_path:
config.available_features.add("cas-plugin")
config.substitutions.append(("%cas_plugin", config.llvm_cas_plugin_test_path))
+
+# Validation is only skipped where the boot time is known, see getBootTime().
+# This may not list all such platforms, but tests that require it only need to
+# run on some.
+if platform.system() in ("Darwin", "Linux"):
+ config.available_features.add("cas-boot-time")
diff --git a/llvm/test/tools/llvm-cas/plugin-validation-crash.test b/llvm/test/tools/llvm-cas/plugin-validation-crash.test
index 7f55f98a14e70..611432397f1bd 100644
--- a/llvm/test/tools/llvm-cas/plugin-validation-crash.test
+++ b/llvm/test/tools/llvm-cas/plugin-validation-crash.test
@@ -1,4 +1,7 @@
REQUIRES: cas-plugin
+# Checks that validation is skipped once the data has been validated since boot,
+# which requires the boot time to be known.
+REQUIRES: cas-boot-time
# HWASan does not tag the globals of a dlopen'ed library with glibc, see
# https://github.com/llvm/llvm-project/issues/57206.
UNSUPPORTED: hwasan
@@ -52,8 +55,10 @@ RUN: --get-cache-result @%t/abc.casid | FileCheck %s --check-prefix=NOT-FOUND
VALID: validated successfully
SKIPPED: validation skipped
INVALID: llvm-cas: validate-if-needed: cas contents invalid
-PENDING: {{^0$}}
-VALIDATED: {{^[1-9][0-9]*$}}
+# Validation pending is recorded as UINT64_MAX, a boot time is shorter. The boot
+# time is 0 on platforms where it is not known.
+PENDING: {{^18446744073709551615$}}
+VALIDATED: {{^[0-9]{1,19}$}}
RECOVERED: recovered from invalid data
RECOVERED-DIRS: corrupt.0.v1.1
NOT-FOUND: result not found
diff --git a/llvm/test/tools/llvm-cas/plugin-validation.test b/llvm/test/tools/llvm-cas/plugin-validation.test
index 6c7ef67a0024e..71510cf2afa3b 100644
--- a/llvm/test/tools/llvm-cas/plugin-validation.test
+++ b/llvm/test/tools/llvm-cas/plugin-validation.test
@@ -1,4 +1,7 @@
REQUIRES: cas-plugin
+# Checks that validation is skipped once the data has been validated since boot,
+# which requires the boot time to be known.
+REQUIRES: cas-boot-time
# HWASan does not tag the globals of a dlopen'ed library with glibc, see
# https://github.com/llvm/llvm-project/issues/57206.
UNSUPPORTED: hwasan
diff --git a/llvm/unittests/CAS/OnDiskCommonUtils.h b/llvm/unittests/CAS/OnDiskCommonUtils.h
index 303415630c939..9365c6c211ff3 100644
--- a/llvm/unittests/CAS/OnDiskCommonUtils.h
+++ b/llvm/unittests/CAS/OnDiskCommonUtils.h
@@ -17,8 +17,21 @@
#include "llvm/Support/BLAKE3.h"
#include "llvm/Testing/Support/Error.h"
+namespace llvm::cas::ondisk {
+/// Declared in the private "OnDiskCommon.h".
+Expected<uint64_t> getBootTime();
+} // namespace llvm::cas::ondisk
+
namespace llvm::unittest::cas {
+/// \returns whether the boot time is known, i.e. it is supported on this
+/// platform and getting it succeeds. Validation of on-disk data is only ever
+/// skipped if it is, since otherwise it cannot be told whether the data has
+/// been validated since boot.
+inline bool isBootTimeKnown() {
+ return expectedToOptional(llvm::cas::ondisk::getBootTime()).value_or(0) != 0;
+}
+
using namespace llvm::cas;
using namespace llvm::cas::ondisk;
diff --git a/llvm/unittests/CAS/PluginCASTest.cpp b/llvm/unittests/CAS/PluginCASTest.cpp
index d417d6c6b7bf5..5dc4400b2a62b 100644
--- a/llvm/unittests/CAS/PluginCASTest.cpp
+++ b/llvm/unittests/CAS/PluginCASTest.cpp
@@ -13,6 +13,7 @@
//===----------------------------------------------------------------------===//
#include "CASTestConfig.h"
+#include "OnDiskCommonUtils.h"
#include "llvm/CAS/ActionCache.h"
#include "llvm/CAS/ObjectStore.h"
#include "llvm/Config/config.h"
@@ -174,9 +175,11 @@ TEST(PluginCASTest, validate) {
}
// Already validated since boot.
+ const ValidationResult ValidatedSinceBoot =
+ isBootTimeKnown() ? ValidationResult::Skipped : ValidationResult::Valid;
ASSERT_THAT_ERROR(validateIfNeeded(/*Force=*/false).moveInto(Result),
Succeeded());
- EXPECT_EQ(Result, ValidationResult::Skipped);
+ EXPECT_EQ(Result, ValidatedSinceBoot);
ASSERT_THAT_ERROR(validateIfNeeded(/*Force=*/true).moveInto(Result),
Succeeded());
@@ -209,7 +212,7 @@ TEST(PluginCASTest, validate) {
// Recovery counts as validation for this boot.
ASSERT_THAT_ERROR(validateIfNeeded(/*Force=*/false).moveInto(Result),
Succeeded());
- EXPECT_EQ(Result, ValidationResult::Skipped);
+ EXPECT_EQ(Result, ValidatedSinceBoot);
std::pair<std::string, std::string> BadOpts[] = {{"bogus", ""}};
EXPECT_THAT_EXPECTED(
diff --git a/llvm/unittests/CAS/UnifiedOnDiskCacheTest.cpp b/llvm/unittests/CAS/UnifiedOnDiskCacheTest.cpp
index 800760ac569ca..f89cfb8556725 100644
--- a/llvm/unittests/CAS/UnifiedOnDiskCacheTest.cpp
+++ b/llvm/unittests/CAS/UnifiedOnDiskCacheTest.cpp
@@ -265,14 +265,17 @@ TEST_P(CustomHasherOnDiskCASTest, UnifiedOnDiskCacheConcurrentValidation) {
};
// Only one of the concurrent validations of valid data is performed, the
- // rest see that it has been validated during this boot.
+ // rest see that it has been validated during this boot. Where the boot time
+ // is not known they are all performed.
+ const bool BootTimeKnown = isBootTimeKnown();
{
unittest::TempDir Temp("ondisk-unified", /*Unique=*/true);
createCAS(Temp.path());
auto Counts =
runConcurrently([&] { return validate(Temp.path(), /*Force=*/false); });
- EXPECT_EQ(Counts[ValidationResult::Valid], 1u);
- EXPECT_EQ(Counts[ValidationResult::Skipped], NumTasks - 1);
+ EXPECT_EQ(Counts[ValidationResult::Valid], BootTimeKnown ? 1u : NumTasks);
+ EXPECT_EQ(Counts[ValidationResult::Skipped],
+ BootTimeKnown ? NumTasks - 1 : 0u);
Counts =
runConcurrently([&] { return validate(Temp.path(), /*Force=*/true); });
@@ -303,9 +306,49 @@ TEST_P(CustomHasherOnDiskCASTest, UnifiedOnDiskCacheConcurrentValidation) {
std::optional<ValidationResult> Result;
ASSERT_THAT_ERROR(validate(Temp.path(), /*Force=*/false).moveInto(Result),
Succeeded());
- EXPECT_EQ(Result, ValidationResult::Skipped);
+ EXPECT_EQ(Result, BootTimeKnown ? ValidationResult::Skipped
+ : ValidationResult::Valid);
ASSERT_THAT_ERROR(UnifiedOnDiskCache::recover(Temp.path()).moveInto(Result),
Succeeded());
EXPECT_EQ(Result, ValidationResult::Skipped);
}
}
+
+TEST_P(CustomHasherOnDiskCASTest, UnifiedOnDiskCacheRepeatedRecovery) {
+ auto HashFn = GetParam().HashFn;
+ StringRef HashName = GetParam().HashName;
+ size_t HashSize = GetParam().HashSize;
+
+ unittest::TempDir Temp("ondisk-unified", /*Unique=*/true);
+
+ // Each recovery moves the data aside under a new name, even though the
+ // directory names of earlier recoveries are still taken.
+ for (unsigned I = 0; I != 3; ++I) {
+ SCOPED_TRACE(I);
+ {
+ std::unique_ptr<UnifiedOnDiskCache> UniDB;
+ ASSERT_THAT_ERROR(UnifiedOnDiskCache::open(Temp.path(),
+ /*SizeLimit=*/std::nullopt,
+ HashName, HashSize)
+ .moveInto(UniDB),
+ Succeeded());
+ std::optional<ObjectID> ID;
+ ASSERT_THAT_ERROR(store(UniDB->getGraphDB(), "1", {}).moveInto(ID),
+ Succeeded());
+ }
+ ASSERT_FALSE(sys::fs::remove(Temp.path("v1.1/data.v1")));
+ EXPECT_THAT_EXPECTED(
+ UnifiedOnDiskCache::validateIfNeeded(Temp.path(), HashName, HashSize,
+ /*CheckHash=*/true, HashFn,
+ /*ForceValidation=*/true),
+ Failed());
+
+ std::optional<ValidationResult> Result;
+ ASSERT_THAT_ERROR(UnifiedOnDiskCache::recover(Temp.path()).moveInto(Result),
+ Succeeded());
+ EXPECT_EQ(Result, ValidationResult::Recovered);
+ EXPECT_TRUE(
+ sys::fs::exists(Temp.path("corrupt." + std::to_string(I) + ".v1.1")));
+ EXPECT_FALSE(sys::fs::exists(Temp.path("v1.1")));
+ }
+}
More information about the llvm-commits
mailing list