1//===----------------------------------------------------------------------===//
2//
3// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4// See https://llvm.org/LICENSE.txt for license information.
5// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6//
7//===----------------------------------------------------------------------===//
8//
9/// \file
10/// Encapsulates \p OnDiskGraphDB and \p OnDiskKeyValueDB instances within one
11/// directory while also restricting storage growth with a scheme of chaining
12/// the two most recent directories (primary & upstream), where the primary
13/// "faults-in" data from the upstream one. When the primary (most recent)
14/// directory exceeds its intended limit a new empty directory becomes the
15/// primary one.
16///
17/// Within the top-level directory (the path that \p UnifiedOnDiskCache::open
18/// receives) there are directories named like this:
19///
20/// 'v<version>.<x>'
21/// 'v<version>.<x+1>'
22/// 'v<version>.<x+2>'
23/// ...
24///
25/// 'version' is the version integer for this \p UnifiedOnDiskCache's scheme and
26/// the part after the dot is an increasing integer. The primary directory is
27/// the one with the highest integer and the upstream one is the directory
28/// before it. For example, if the sub-directories contained are:
29///
30/// 'v1.5', 'v1.6', 'v1.7', 'v1.8'
31///
32/// Then the primary one is 'v1.8', the upstream one is 'v1.7', and the rest are
33/// unused directories that can be safely deleted at any time and by any
34/// process.
35///
36/// Contained within the top-level directory is a file named "lock" which is
37/// used for processes to take shared or exclusive locks for the contents of the
38/// top directory. While a \p UnifiedOnDiskCache is open it keeps a shared lock
39/// for the top-level directory; when it closes, if the primary sub-directory
40/// exceeded its limit, it attempts to get an exclusive lock in order to create
41/// a new empty primary directory; if it can't get the exclusive lock it gives
42/// up and lets the next \p UnifiedOnDiskCache instance that closes to attempt
43/// again.
44///
45/// The downside of this scheme is that while \p UnifiedOnDiskCache is open on a
46/// directory, by any process, the storage size in that directory will keep
47/// growing unrestricted. But the major benefit is that garbage-collection can
48/// be triggered on a directory concurrently, at any time and by any process,
49/// without affecting any active readers/writers in the same process or other
50/// processes.
51///
52/// The \c UnifiedOnDiskCache also provides validation and recovery on top of
53/// the underlying on-disk storage. The low-level storage is designed to remain
54/// coherent across regular process crashes, but may be invalid after power loss
55/// or similar system failures. \c UnifiedOnDiskCache::validateIfNeeded allows
56/// validating the contents once per boot (or every time, where the boot time is
57/// not known), and if validation fails (or crashes,
58/// when performed in a separate process) \c UnifiedOnDiskCache::recover can
59/// recover by marking invalid data for garbage collection.
60///
61/// Validation and recovery are serialized by an exclusive lock on the
62/// "v1.validation" file, which records the boot time of the last successful
63/// validation or recovery. Before validating, the file is marked as validation
64/// pending, and the boot time is only written once validation succeeds; a
65/// validation that fails or crashes leaves it pending, so the next validation
66/// is not skipped. Recovery only happens while validation is pending, so when
67/// multiple processes attempt recovery after a failed validation only the first
68/// one recovers.
69///
70/// The data recovery described above requires exclusive access to the CAS, and
71/// it is an error to attempt recovery if the CAS is open in any process/thread.
72/// In order to maximize backwards compatibility with tools that do not perform
73/// validation before opening the CAS, we do not attempt to get exclusive access
74/// until recovery is actually performed, meaning as long as the data is valid
75/// it will not conflict with concurrent use.
76//
77//===----------------------------------------------------------------------===//
78
79#include "llvm/CAS/UnifiedOnDiskCache.h"
80#include "OnDiskCommon.h"
81#include "llvm/ADT/STLExtras.h"
82#include "llvm/ADT/ScopeExit.h"
83#include "llvm/ADT/SmallString.h"
84#include "llvm/ADT/SmallVector.h"
85#include "llvm/ADT/StringExtras.h"
86#include "llvm/ADT/StringRef.h"
87#include "llvm/CAS/OnDiskCASLogger.h"
88#include "llvm/CAS/OnDiskGraphDB.h"
89#include "llvm/CAS/OnDiskKeyValueDB.h"
90#include "llvm/Support/Compiler.h"
91#include "llvm/Support/Errc.h"
92#include "llvm/Support/Error.h"
93#include "llvm/Support/FileSystem.h"
94#include "llvm/Support/IOSandbox.h"
95#include "llvm/Support/Path.h"
96#include "llvm/Support/raw_ostream.h"
97#include <limits>
98#include <optional>
99
100using namespace llvm;
101using namespace llvm::cas;
102using namespace llvm::cas::ondisk;
103
104/// FIXME: When the version of \p DBDirPrefix is bumped up we need to figure out
105/// how to handle the leftover sub-directories of the previous version, within
106/// the \p UnifiedOnDiskCache::collectGarbage function.
107static constexpr StringLiteral DBDirPrefix = "v1.";
108
109static constexpr StringLiteral ValidationFilename = "v1.validation";
110static constexpr StringLiteral CorruptPrefix = "corrupt.";
111
112ObjectID UnifiedOnDiskCache::getObjectIDFromValue(ArrayRef<char> Value) {
113 // little endian encoded.
114 assert(Value.size() == sizeof(uint64_t));
115 return ObjectID::fromOpaqueData(Opaque: support::endian::read64le(P: Value.data()));
116}
117
118UnifiedOnDiskCache::ValueBytes
119UnifiedOnDiskCache::getValueFromObjectID(ObjectID ID) {
120 // little endian encoded.
121 UnifiedOnDiskCache::ValueBytes ValBytes;
122 static_assert(ValBytes.size() == sizeof(ID.getOpaqueData()));
123 support::endian::write64le(P: ValBytes.data(), V: ID.getOpaqueData());
124 return ValBytes;
125}
126
127Expected<std::optional<ArrayRef<char>>>
128UnifiedOnDiskCache::faultInFromUpstreamKV(ArrayRef<uint8_t> Key) {
129 assert(UpstreamGraphDB);
130 assert(UpstreamKVDB);
131
132 std::optional<ArrayRef<char>> UpstreamValue;
133 if (Error E = UpstreamKVDB->get(Key).moveInto(Value&: UpstreamValue))
134 return std::move(E);
135 if (!UpstreamValue)
136 return std::nullopt;
137
138 // The value is the \p ObjectID in the context of the upstream
139 // \p OnDiskGraphDB instance. Translate it to the context of the primary
140 // \p OnDiskGraphDB instance.
141 ObjectID UpstreamID = getObjectIDFromValue(Value: *UpstreamValue);
142 auto PrimaryID =
143 PrimaryGraphDB->getReference(Hash: UpstreamGraphDB->getDigest(Ref: UpstreamID));
144 if (LLVM_UNLIKELY(!PrimaryID))
145 return PrimaryID.takeError();
146 return PrimaryKVDB->put(Key, Value: getValueFromObjectID(ID: *PrimaryID));
147}
148
149/// \returns all the 'v<version>.<x>' names of sub-directories, sorted with
150/// ascending order of the integer after the dot. Corrupt directories, if
151/// included, will come first.
152static Expected<SmallVector<std::string, 4>>
153getAllDBDirs(StringRef Path, bool IncludeCorrupt = false) {
154 struct DBDir {
155 uint64_t Order;
156 std::string Name;
157 };
158 SmallVector<DBDir> FoundDBDirs;
159
160 std::error_code EC;
161 for (sys::fs::directory_iterator DirI(Path, EC), DirE; !EC && DirI != DirE;
162 DirI.increment(ec&: EC)) {
163 if (DirI->type() != sys::fs::file_type::directory_file)
164 continue;
165 StringRef SubDir = sys::path::filename(path: DirI->path());
166 if (IncludeCorrupt && SubDir.starts_with(Prefix: CorruptPrefix)) {
167 FoundDBDirs.push_back(Elt: {.Order: 0, .Name: std::string(SubDir)});
168 continue;
169 }
170 if (!SubDir.starts_with(Prefix: DBDirPrefix))
171 continue;
172 uint64_t Order;
173 if (SubDir.substr(Start: DBDirPrefix.size()).getAsInteger(Radix: 10, Result&: Order))
174 return createStringError(EC: inconvertibleErrorCode(),
175 S: "unexpected directory " + DirI->path());
176 FoundDBDirs.push_back(Elt: {.Order: Order, .Name: std::string(SubDir)});
177 }
178 if (EC)
179 return createFileError(F: Path, EC);
180
181 llvm::sort(C&: FoundDBDirs, Comp: [](const DBDir &LHS, const DBDir &RHS) -> bool {
182 return LHS.Order < RHS.Order;
183 });
184
185 SmallVector<std::string, 4> DBDirs;
186 for (DBDir &Dir : FoundDBDirs)
187 DBDirs.push_back(Elt: std::move(Dir.Name));
188 return DBDirs;
189}
190
191static Expected<SmallVector<std::string, 4>> getAllGarbageDirs(StringRef Path) {
192 auto DBDirs = getAllDBDirs(Path, /*IncludeCorrupt=*/true);
193 if (!DBDirs)
194 return DBDirs.takeError();
195
196 // FIXME: When the version of \p DBDirPrefix is bumped up we need to figure
197 // out how to handle the leftover sub-directories of the previous version.
198
199 for (unsigned Keep = 2; Keep > 0 && !DBDirs->empty(); --Keep) {
200 StringRef Back(DBDirs->back());
201 if (Back.starts_with(Prefix: CorruptPrefix))
202 break;
203 DBDirs->pop_back();
204 }
205 return *DBDirs;
206}
207
208/// \returns Given a sub-directory named 'v<version>.<x>', it outputs the
209/// 'v<version>.<x+1>' name.
210static void getNextDBDirName(StringRef DBDir, llvm::raw_ostream &OS) {
211 assert(DBDir.starts_with(DBDirPrefix));
212 uint64_t Count;
213 bool Failed = DBDir.substr(Start: DBDirPrefix.size()).getAsInteger(Radix: 10, Result&: Count);
214 assert(!Failed);
215 (void)Failed;
216 OS << DBDirPrefix << Count + 1;
217}
218
219Error UnifiedOnDiskCache::validateActionCache() const {
220 return getKeyValueDB().validate();
221}
222
223static Error validateInProcess(StringRef RootPath, StringRef HashName,
224 unsigned HashByteSize, bool CheckHash,
225 OnDiskGraphDB::HashingFuncT HashFn) {
226 std::shared_ptr<UnifiedOnDiskCache> UniDB;
227 if (Error E = UnifiedOnDiskCache::open(Path: RootPath, SizeLimit: std::nullopt, HashName,
228 HashByteSize)
229 .moveInto(Value&: UniDB))
230 return E;
231 if (Error E = UniDB->getGraphDB().validate(Deep: CheckHash, Hasher: HashFn))
232 return E;
233 if (Error E = UniDB->validateActionCache())
234 return E;
235 return Error::success();
236}
237
238/// \returns the boot time, or 0 if it is not known, including if getting it
239/// failed. Validation is never skipped where the boot time is not known.
240static uint64_t getCachedBootTime() {
241 static const uint64_t BootTime =
242 expectedToOptional(E: getBootTime()).value_or(u: 0);
243 return BootTime;
244}
245
246namespace {
247/// The validation file records the state of validation for the data:
248/// - empty: never validated.
249/// - \c ValidationPending: a validation started but did not yet succeed, i.e.
250/// it is in progress, failed, or crashed, and the data has not been
251/// recovered since.
252/// - a boot time: the data was validated or recovered during that boot.
253///
254/// While this object is alive it holds an exclusive lock on the file, which
255/// serializes validation and recovery across processes and threads.
256///
257/// Lock ordering: the validation file lock is always acquired before the
258/// top-level "lock" file, and the latter is only ever acquired exclusively via
259/// a non-blocking try-lock, so that validation and recovery cannot deadlock.
260class LockedValidationFile {
261public:
262 /// Written before validating. It is an integer so that older versions, which
263 /// only know about boot times, still parse the file and treat it as not
264 /// validated. It never matches a boot time.
265 static constexpr uint64_t ValidationPending =
266 std::numeric_limits<uint64_t>::max();
267
268 static Expected<std::unique_ptr<LockedValidationFile>>
269 open(StringRef RootPath) {
270 if (std::error_code EC = sys::fs::create_directories(path: RootPath))
271 return createFileError(F: RootPath, EC);
272
273 SmallString<256> PathBuf(RootPath);
274 sys::path::append(path&: PathBuf, a: ValidationFilename);
275 int FD = -1;
276 if (std::error_code EC = sys::fs::openFileForReadWrite(
277 Name: PathBuf, ResultFD&: FD, Disp: sys::fs::CD_OpenAlways, Flags: sys::fs::OF_None))
278 return createFileError(F: PathBuf, EC);
279 assert(FD != -1);
280 std::unique_ptr<LockedValidationFile> VF(
281 new LockedValidationFile(PathBuf, FD));
282
283 if (std::error_code EC =
284 lockFileThreadSafe(FD, Kind: sys::fs::LockKind::Exclusive))
285 return createFileError(F: PathBuf, EC);
286 VF->Locked = true;
287
288 SmallString<8> Bytes;
289 if (Error E = sys::fs::readNativeFileToEOF(FileHandle: VF->File, Buffer&: Bytes))
290 return createFileError(F: PathBuf, E: std::move(E));
291 if (!Bytes.empty()) {
292 uint64_t Value;
293 if (StringRef(Bytes).trim().getAsInteger(Radix: 10, Result&: Value))
294 return createFileError(F: PathBuf, EC: errc::illegal_byte_sequence,
295 Fmt: "expected integer");
296 VF->State = Value;
297 }
298 return std::move(VF);
299 }
300
301 ~LockedValidationFile() {
302 if (Locked)
303 unlockFileThreadSafe(FD);
304 sys::fs::closeFile(F&: File);
305 }
306
307 /// \returns the boot time of the last successful validation or recovery, or
308 /// 0 if there is none.
309 uint64_t getLastValidBootTime() const {
310 return isValidationPending() ? 0 : State.value_or(u: 0);
311 }
312
313 /// Whether the data was validated or recovered during the boot with
314 /// \p BootTime. Always false where the boot time is not known, i.e. 0,
315 /// since it cannot be told whether that was during the current boot.
316 bool isValidAtBoot(uint64_t BootTime) const {
317 // The boot time can be computed from the current time, so it moves when
318 // the clock is adjusted and the recorded one can be later than BootTime
319 // during the same boot. A reboot always makes it later. Pending is larger
320 // than any boot time, so it needs to be excluded.
321 return BootTime != 0 && !isValidationPending() && State &&
322 BootTime <= *State;
323 }
324
325 bool isValidationPending() const { return State == ValidationPending; }
326
327 Error setValidationPending() { return write(Value: ValidationPending); }
328
329 Error setLastValidBootTime(uint64_t BootTime) { return write(Value: BootTime); }
330
331private:
332 LockedValidationFile(StringRef Path, int FD)
333 : Path(Path), FD(FD), File(sys::fs::convertFDToNativeFile(FD)) {}
334
335 Error write(uint64_t Value) {
336 if (State == Value)
337 return Error::success();
338 if (std::error_code EC = sys::fs::resize_file(FD, Size: 0))
339 return createFileError(F: Path, EC);
340 raw_fd_ostream OS(FD, /*shouldClose=*/false);
341 OS.seek(off: 0); // resize does not reset position
342 OS << Value << '\n';
343 if (OS.has_error())
344 return createFileError(F: Path, EC: OS.error());
345 State = Value;
346 return Error::success();
347 }
348
349 SmallString<256> Path;
350 int FD;
351 sys::fs::file_t File;
352 bool Locked = false;
353 std::optional<uint64_t> State;
354};
355} // namespace
356
357/// Marks all the database directories in \p RootPath as corrupt, which makes
358/// them eligible for garbage collection. Requires exclusive access to the CAS.
359static Error markAllDBDirsCorrupt(StringRef RootPath) {
360 SmallString<256> PathBuf(RootPath);
361 sys::path::append(path&: PathBuf, a: "lock");
362
363 int LockFD = -1;
364 if (std::error_code EC = sys::fs::openFileForReadWrite(
365 Name: PathBuf, ResultFD&: LockFD, Disp: sys::fs::CD_OpenAlways, Flags: sys::fs::OF_None))
366 return createFileError(F: PathBuf, EC);
367 sys::fs::file_t LockFile = sys::fs::convertFDToNativeFile(FD: LockFD);
368 llvm::scope_exit CloseLock([&]() { sys::fs::closeFile(F&: LockFile); });
369 if (std::error_code EC = tryLockFileThreadSafe(FD: LockFD)) {
370 if (EC == std::errc::no_lock_available)
371 return createFileError(
372 F: PathBuf, EC,
373 Fmt: "CAS recovery requires exclusive access but CAS was in use");
374 return createFileError(F: PathBuf, EC);
375 }
376 llvm::scope_exit UnlockFD([&]() { unlockFileThreadSafe(FD: LockFD); });
377
378 auto DBDirs = getAllDBDirs(Path: RootPath);
379 if (!DBDirs)
380 return DBDirs.takeError();
381
382 for (StringRef DBDir : *DBDirs) {
383 sys::path::remove_filename(path&: PathBuf);
384 sys::path::append(path&: PathBuf, a: DBDir);
385 // Pick the first name not taken by earlier recoveries. Checking the error
386 // of the rename is not enough since Windows reports permission denied when
387 // the destination directory exists. The name cannot be taken concurrently
388 // since only garbage collection touches these directories, and it only
389 // removes them.
390 int Attempt = 0, MaxAttempts = 100;
391 SmallString<128> GCPath;
392 for (; Attempt < MaxAttempts; ++Attempt) {
393 GCPath.assign(RHS: RootPath);
394 sys::path::append(path&: GCPath,
395 a: CorruptPrefix + std::to_string(val: Attempt) + "." + DBDir);
396 if (!sys::fs::exists(Path: GCPath))
397 break;
398 }
399 if (Attempt == MaxAttempts)
400 return createStringError(
401 EC: errc::file_exists,
402 S: "rename " + PathBuf +
403 " failed: too many CAS directories awaiting pruning");
404 if (std::error_code EC = sys::fs::rename(from: PathBuf, to: GCPath))
405 return createStringError(EC, S: "rename " + PathBuf + " to " + GCPath +
406 " failed: " + EC.message());
407 }
408 return Error::success();
409}
410
411Expected<ValidationResult> UnifiedOnDiskCache::validateIfNeeded(
412 StringRef RootPath, StringRef HashName, unsigned HashByteSize,
413 bool CheckHash, OnDiskGraphDB::HashingFuncT HashFn, bool ForceValidation) {
414 std::unique_ptr<LockedValidationFile> VF;
415 if (Error E = LockedValidationFile::open(RootPath).moveInto(Value&: VF))
416 return std::move(E);
417
418 std::shared_ptr<ondisk::OnDiskCASLogger> Logger;
419#ifndef _WIN32
420 if (Error E =
421 ondisk::OnDiskCASLogger::openIfEnabled(Path: RootPath).moveInto(Value&: Logger))
422 return std::move(E);
423#endif
424
425 uint64_t BootTime = getCachedBootTime();
426 uint64_t ValidationBootTime = VF->getLastValidBootTime();
427
428 bool Skipped = false;
429 std::string LogValidationError;
430
431 llvm::scope_exit Log([&] {
432 if (!Logger)
433 return;
434 Logger->logUnifiedOnDiskCacheValidateIfNeeded(
435 Path: RootPath, BootTime, ValidationTime: ValidationBootTime, CheckHash, Force: ForceValidation,
436 ValidationError: LogValidationError, Skipped);
437 });
438
439 if (VF->isValidAtBoot(BootTime) && !ForceValidation) {
440 Skipped = true;
441 return ValidationResult::Skipped;
442 }
443
444 // Mark validation as pending until it succeeds, so that a failed or crashed
445 // validation is detected by recovery and by the next validation.
446 if (Error E = VF->setValidationPending())
447 return std::move(E);
448
449 if (Error E = validateInProcess(RootPath, HashName, HashByteSize, CheckHash,
450 HashFn)) {
451 if (Logger)
452 LogValidationError = toStringWithoutConsuming(E);
453 return std::move(E);
454 }
455
456 if (Error E = VF->setLastValidBootTime(BootTime))
457 return std::move(E);
458 return ValidationResult::Valid;
459}
460
461Expected<ValidationResult> UnifiedOnDiskCache::recover(StringRef RootPath) {
462 std::unique_ptr<LockedValidationFile> VF;
463 if (Error E = LockedValidationFile::open(RootPath).moveInto(Value&: VF))
464 return std::move(E);
465
466 std::shared_ptr<ondisk::OnDiskCASLogger> Logger;
467#ifndef _WIN32
468 if (Error E =
469 ondisk::OnDiskCASLogger::openIfEnabled(Path: RootPath).moveInto(Value&: Logger))
470 return std::move(E);
471#endif
472
473 uint64_t BootTime = getCachedBootTime();
474
475 bool Skipped = false;
476 std::string LogRecoveryError;
477
478 llvm::scope_exit Log([&] {
479 if (!Logger)
480 return;
481 Logger->logUnifiedOnDiskCacheRecover(Path: RootPath, BootTime, RecoveryError: LogRecoveryError,
482 Skipped);
483 });
484
485 // Unless validation is still pending, the data has been recovered or
486 // successfully validated since the failed validation, e.g. by a concurrent
487 // process.
488 if (!VF->isValidationPending()) {
489 Skipped = true;
490 return ValidationResult::Skipped;
491 }
492
493 if (Error E = markAllDBDirsCorrupt(RootPath)) {
494 if (Logger)
495 LogRecoveryError = toStringWithoutConsuming(E);
496 return std::move(E);
497 }
498
499 if (Error E = VF->setLastValidBootTime(BootTime))
500 return std::move(E);
501 return ValidationResult::Recovered;
502}
503
504Expected<std::unique_ptr<UnifiedOnDiskCache>>
505UnifiedOnDiskCache::open(StringRef RootPath, std::optional<uint64_t> SizeLimit,
506 StringRef HashName, unsigned HashByteSize,
507 OnDiskGraphDB::FaultInPolicy FaultInPolicy) {
508 auto BypassSandbox = sys::sandbox::scopedDisable();
509
510 if (std::error_code EC = sys::fs::create_directories(path: RootPath))
511 return createFileError(F: RootPath, EC);
512
513 SmallString<256> PathBuf(RootPath);
514 sys::path::append(path&: PathBuf, a: "lock");
515 int LockFD = -1;
516 if (std::error_code EC = sys::fs::openFileForReadWrite(
517 Name: PathBuf, ResultFD&: LockFD, Disp: sys::fs::CD_OpenAlways, Flags: sys::fs::OF_None))
518 return createFileError(F: PathBuf, EC);
519 assert(LockFD != -1);
520 // Locking the directory using shared lock, which will prevent other processes
521 // from creating a new chain (essentially while a \p UnifiedOnDiskCache
522 // instance holds a shared lock the storage for the primary directory will
523 // grow unrestricted).
524 if (std::error_code EC =
525 lockFileThreadSafe(FD: LockFD, Kind: sys::fs::LockKind::Shared))
526 return createFileError(F: PathBuf, EC);
527
528 auto DBDirs = getAllDBDirs(Path: RootPath);
529 if (!DBDirs)
530 return DBDirs.takeError();
531 if (DBDirs->empty())
532 DBDirs->push_back(Elt: (Twine(DBDirPrefix) + "1").str());
533
534 std::shared_ptr<ondisk::OnDiskCASLogger> Logger;
535#ifndef _WIN32
536 if (Error E =
537 ondisk::OnDiskCASLogger::openIfEnabled(Path: RootPath).moveInto(Value&: Logger))
538 return std::move(E);
539#endif
540
541 /// If there is only one directory open databases on it. If there are 2 or
542 /// more directories, get the most recent directories and chain them, with the
543 /// most recent being the primary one. The remaining directories are unused
544 /// data than can be garbage-collected.
545 auto UniDB = std::unique_ptr<UnifiedOnDiskCache>(new UnifiedOnDiskCache());
546 std::unique_ptr<OnDiskGraphDB> UpstreamGraphDB;
547 std::unique_ptr<OnDiskKeyValueDB> UpstreamKVDB;
548 if (DBDirs->size() > 1) {
549 StringRef UpstreamDir = *(DBDirs->end() - 2);
550 PathBuf = RootPath;
551 sys::path::append(path&: PathBuf, a: UpstreamDir);
552 if (Error E =
553 OnDiskGraphDB::open(Path: PathBuf, HashName, HashByteSize,
554 /*UpstreamDB=*/nullptr, Logger, Policy: FaultInPolicy)
555 .moveInto(Value&: UpstreamGraphDB))
556 return std::move(E);
557 if (Error E = OnDiskKeyValueDB::open(Path: PathBuf, HashName, KeySize: HashByteSize,
558 /*ValueName=*/"objectid",
559 /*ValueSize=*/sizeof(uint64_t),
560 /*UnifiedCache=*/nullptr, Logger)
561 .moveInto(Value&: UpstreamKVDB))
562 return std::move(E);
563 }
564
565 StringRef PrimaryDir = *(DBDirs->end() - 1);
566 PathBuf = RootPath;
567 sys::path::append(path&: PathBuf, a: PrimaryDir);
568 std::unique_ptr<OnDiskGraphDB> PrimaryGraphDB;
569 if (Error E =
570 OnDiskGraphDB::open(Path: PathBuf, HashName, HashByteSize,
571 UpstreamDB: UpstreamGraphDB.get(), Logger, Policy: FaultInPolicy)
572 .moveInto(Value&: PrimaryGraphDB))
573 return std::move(E);
574 std::unique_ptr<OnDiskKeyValueDB> PrimaryKVDB;
575 // \p UnifiedOnDiskCache does manual chaining for key-value requests,
576 // including an extra translation step of the value during fault-in.
577 if (Error E = OnDiskKeyValueDB::open(Path: PathBuf, HashName, KeySize: HashByteSize,
578 /*ValueName=*/"objectid",
579 /*ValueSize=*/sizeof(uint64_t),
580 UnifiedCache: UniDB.get(), Logger)
581 .moveInto(Value&: PrimaryKVDB))
582 return std::move(E);
583
584 UniDB->RootPath = RootPath;
585 UniDB->SizeLimit = SizeLimit.value_or(u: 0);
586 UniDB->LockFD = LockFD;
587 UniDB->NeedsGarbageCollection = DBDirs->size() > 2;
588 UniDB->PrimaryDBDir = PrimaryDir;
589 UniDB->UpstreamGraphDB = std::move(UpstreamGraphDB);
590 UniDB->PrimaryGraphDB = std::move(PrimaryGraphDB);
591 UniDB->UpstreamKVDB = std::move(UpstreamKVDB);
592 UniDB->PrimaryKVDB = std::move(PrimaryKVDB);
593 UniDB->Logger = std::move(Logger);
594
595 return std::move(UniDB);
596}
597
598void UnifiedOnDiskCache::setSizeLimit(std::optional<uint64_t> SizeLimit) {
599 this->SizeLimit = SizeLimit.value_or(u: 0);
600}
601
602uint64_t UnifiedOnDiskCache::getStorageSize() const {
603 uint64_t TotalSize = getPrimaryStorageSize();
604 if (UpstreamGraphDB)
605 TotalSize += UpstreamGraphDB->getStorageSize();
606 if (UpstreamKVDB)
607 TotalSize += UpstreamKVDB->getStorageSize();
608 return TotalSize;
609}
610
611uint64_t UnifiedOnDiskCache::getPrimaryStorageSize() const {
612 return PrimaryGraphDB->getStorageSize() + PrimaryKVDB->getStorageSize();
613}
614
615bool UnifiedOnDiskCache::hasExceededSizeLimit() const {
616 uint64_t CurSizeLimit = SizeLimit;
617 if (!CurSizeLimit)
618 return false;
619
620 // If the hard limit is beyond 85%, declare above limit and request clean up.
621 unsigned CurrentPercent =
622 std::max(a: PrimaryGraphDB->getHardStorageLimitUtilization(),
623 b: PrimaryKVDB->getHardStorageLimitUtilization());
624 if (CurrentPercent > 85)
625 return true;
626
627 // We allow each of the directories in the chain to reach up to half the
628 // intended size limit. Check whether the primary directory has exceeded half
629 // the limit or not, in order to decide whether we need to start a new chain.
630 //
631 // We could check the size limit against the sum of sizes of both the primary
632 // and upstream directories but then if the upstream is significantly larger
633 // than the intended limit, it would trigger a new chain to be created before
634 // the primary has reached its own limit. Essentially in such situation we
635 // prefer reclaiming the storage later in order to have more consistent cache
636 // hits behavior.
637 return (CurSizeLimit / 2) < getPrimaryStorageSize();
638}
639
640Error UnifiedOnDiskCache::close(bool CheckSizeLimit) {
641 auto BypassSandbox = sys::sandbox::scopedDisable();
642
643 if (LockFD == -1)
644 return Error::success(); // already closed.
645 llvm::scope_exit CloseLock([&]() {
646 assert(LockFD >= 0);
647 sys::fs::file_t LockFile = sys::fs::convertFDToNativeFile(FD: LockFD);
648 sys::fs::closeFile(F&: LockFile);
649 LockFD = -1;
650 });
651
652 bool ExceededSizeLimit = CheckSizeLimit ? hasExceededSizeLimit() : false;
653 UpstreamKVDB.reset();
654 PrimaryKVDB.reset();
655 UpstreamGraphDB.reset();
656 PrimaryGraphDB.reset();
657 if (std::error_code EC = unlockFileThreadSafe(FD: LockFD))
658 return createFileError(F: RootPath, EC);
659
660 if (!ExceededSizeLimit)
661 return Error::success();
662
663 // The primary directory exceeded its intended size limit. Try to get an
664 // exclusive lock in order to create a new primary directory for next time
665 // this \p UnifiedOnDiskCache path is opened.
666
667 if (std::error_code EC = tryLockFileThreadSafe(
668 FD: LockFD, Timeout: std::chrono::milliseconds(0), Kind: sys::fs::LockKind::Exclusive)) {
669 if (EC == errc::no_lock_available)
670 return Error::success(); // couldn't get exclusive lock, give up.
671 return createFileError(F: RootPath, EC);
672 }
673 llvm::scope_exit UnlockFile([&]() { unlockFileThreadSafe(FD: LockFD); });
674
675 // Managed to get an exclusive lock which means there are no other open
676 // \p UnifiedOnDiskCache instances for the same path, so we can safely start a
677 // new primary directory. To start a new primary directory we just have to
678 // create a new empty directory with the next consecutive index; since this is
679 // an atomic operation we will leave the top-level directory in a consistent
680 // state even if the process dies during this code-path.
681
682 SmallString<256> PathBuf(RootPath);
683 raw_svector_ostream OS(PathBuf);
684 OS << sys::path::get_separator();
685 getNextDBDirName(DBDir: PrimaryDBDir, OS);
686 if (std::error_code EC = sys::fs::create_directory(path: PathBuf))
687 return createFileError(F: PathBuf, EC);
688
689 NeedsGarbageCollection = true;
690 return Error::success();
691}
692
693UnifiedOnDiskCache::UnifiedOnDiskCache() = default;
694
695UnifiedOnDiskCache::~UnifiedOnDiskCache() { consumeError(Err: close()); }
696
697Error UnifiedOnDiskCache::collectGarbage(StringRef Path,
698 ondisk::OnDiskCASLogger *Logger) {
699 auto DBDirs = getAllGarbageDirs(Path);
700 if (!DBDirs)
701 return DBDirs.takeError();
702
703 SmallString<256> PathBuf(Path);
704 for (StringRef UnusedSubDir : *DBDirs) {
705 sys::path::append(path&: PathBuf, a: UnusedSubDir);
706 if (Logger)
707 Logger->logUnifiedOnDiskCacheCollectGarbage(Path: PathBuf);
708 if (std::error_code EC = sys::fs::remove_directories(path: PathBuf))
709 return createFileError(F: PathBuf, EC);
710 sys::path::remove_filename(path&: PathBuf);
711 }
712 return Error::success();
713}
714
715Error UnifiedOnDiskCache::collectGarbage() {
716 return collectGarbage(Path: RootPath, Logger: Logger.get());
717}
718