1//===- OnDiskCommon.cpp ---------------------------------------------------===//
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#include "OnDiskCommon.h"
10#include "llvm/Support/Errno.h"
11#include "llvm/Support/Error.h"
12#include "llvm/Support/FileSystem.h"
13#include "llvm/Support/Path.h"
14#include "llvm/Support/Process.h"
15#include <chrono>
16#include <mutex>
17#include <thread>
18
19#if __has_include(<sys/file.h>)
20#include <sys/file.h>
21#ifdef LOCK_SH
22#define HAVE_FLOCK 1
23#else
24#define HAVE_FLOCK 0
25#endif
26#endif
27
28#if __has_include(<fcntl.h>)
29#include <fcntl.h>
30#endif
31
32#if __has_include(<sys/mount.h>)
33#include <sys/mount.h> // statfs
34#endif
35
36#ifdef __APPLE__
37#if __has_include(<sys/sysctl.h>)
38#include <sys/sysctl.h>
39#endif
40#endif
41
42#ifdef _WIN32
43#include "llvm/Support/Windows/WindowsSupport.h"
44#endif
45
46using namespace llvm;
47
48static uint64_t OnDiskCASMaxMappingSize = 0;
49
50Expected<std::optional<uint64_t>> cas::ondisk::getOverriddenMaxMappingSize() {
51 static std::once_flag Flag;
52 Error Err = Error::success();
53 std::call_once(once&: Flag, f: [&Err] {
54 ErrorAsOutParameter EAO(&Err);
55 constexpr const char *EnvVar = "LLVM_CAS_MAX_MAPPING_SIZE";
56 auto Value = sys::Process::GetEnv(name: EnvVar);
57 if (!Value)
58 return;
59
60 uint64_t Size;
61 if (StringRef(*Value).getAsInteger(/*auto*/ Radix: 0, Result&: Size))
62 Err = createStringError(EC: inconvertibleErrorCode(),
63 Fmt: "invalid value for %s: expected integer", Vals: EnvVar);
64 OnDiskCASMaxMappingSize = Size;
65 });
66
67 if (Err)
68 return std::move(Err);
69
70 if (OnDiskCASMaxMappingSize == 0)
71 return std::nullopt;
72
73 return OnDiskCASMaxMappingSize;
74}
75
76void cas::ondisk::setMaxMappingSize(uint64_t Size) {
77 OnDiskCASMaxMappingSize = Size;
78}
79
80std::error_code cas::ondisk::lockFileThreadSafe(int FD,
81 sys::fs::LockKind Kind) {
82#if HAVE_FLOCK
83 if (sys::RetryAfterSignal(
84 Fail: -1, F&: flock, As: FD,
85 As: Kind == sys::fs::LockKind::Exclusive ? LOCK_EX : LOCK_SH) == 0)
86 return std::error_code();
87 return std::error_code(errno, std::generic_category());
88#elif defined(_WIN32)
89 // On Windows this implementation is thread-safe.
90 return sys::fs::lockFile(FD, Kind);
91#else
92 return make_error_code(std::errc::no_lock_available);
93#endif
94}
95
96std::error_code cas::ondisk::unlockFileThreadSafe(int FD) {
97#if HAVE_FLOCK
98 if (sys::RetryAfterSignal(Fail: -1, F&: flock, As: FD, LOCK_UN) == 0)
99 return std::error_code();
100 return std::error_code(errno, std::generic_category());
101#elif defined(_WIN32)
102 // On Windows this implementation is thread-safe.
103 return sys::fs::unlockFile(FD);
104#else
105 return make_error_code(std::errc::no_lock_available);
106#endif
107}
108
109std::error_code
110cas::ondisk::tryLockFileThreadSafe(int FD, std::chrono::milliseconds Timeout,
111 sys::fs::LockKind Kind) {
112#if HAVE_FLOCK
113 auto Start = std::chrono::steady_clock::now();
114 auto End = Start + Timeout;
115 do {
116 if (sys::RetryAfterSignal(
117 Fail: -1, F&: flock, As: FD,
118 As: (Kind == sys::fs::LockKind::Exclusive ? LOCK_EX : LOCK_SH) |
119 LOCK_NB) == 0)
120 return std::error_code();
121 int Error = errno;
122 if (Error == EWOULDBLOCK) {
123 if (Timeout.count() == 0)
124 break;
125 // Match sys::fs::tryLockFile, which sleeps for 1 ms per attempt.
126 std::this_thread::sleep_for(rtime: std::chrono::milliseconds(1));
127 continue;
128 }
129 return std::error_code(Error, std::generic_category());
130 } while (std::chrono::steady_clock::now() < End);
131 return make_error_code(e: std::errc::no_lock_available);
132#elif defined(_WIN32)
133 // On Windows this implementation is thread-safe.
134 return sys::fs::tryLockFile(FD, Timeout, Kind);
135#else
136 return make_error_code(std::errc::no_lock_available);
137#endif
138}
139
140Expected<size_t> cas::ondisk::preallocateFileTail(int FD, size_t CurrentSize,
141 size_t NewSize) {
142 auto CreateError = [&](std::error_code EC) -> Expected<size_t> {
143 if (EC == std::errc::not_supported)
144 // Ignore ENOTSUP in case the filesystem cannot preallocate.
145 return NewSize;
146#if defined(HAVE_POSIX_FALLOCATE)
147 if (EC == std::errc::invalid_argument && CurrentSize < NewSize && // len > 0
148 NewSize < std::numeric_limits<off_t>::max()) // 0 <= offset, len < max
149 // Prior to 2024, POSIX required EINVAL for cases that should be ENOTSUP,
150 // so handle it the same as above if it is not one of the other ways to
151 // get EINVAL.
152 return NewSize;
153#endif
154 return createStringError(EC,
155 S: "failed to allocate to CAS file: " + EC.message());
156 };
157#if defined(HAVE_POSIX_FALLOCATE)
158 // Note: posix_fallocate returns its error directly, not via errno.
159 int Err;
160 do {
161 Err = posix_fallocate(FD, CurrentSize, NewSize - CurrentSize);
162 } while (Err == EINTR);
163 if (Err)
164 return CreateError(std::error_code(Err, std::generic_category()));
165 return NewSize;
166#elif defined(__APPLE__)
167 fstore_t FAlloc;
168 FAlloc.fst_flags = F_ALLOCATEALL;
169#if defined(F_ALLOCATEPERSIST) && \
170 defined(__ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__) && \
171 __ENVIRONMENT_MAC_OS_X_VERSION_MIN_REQUIRED__ >= 130000
172 // F_ALLOCATEPERSIST is introduced in macOS 13.
173 FAlloc.fst_flags |= F_ALLOCATEPERSIST;
174#endif
175 FAlloc.fst_posmode = F_PEOFPOSMODE;
176 FAlloc.fst_offset = 0;
177 FAlloc.fst_length = NewSize - CurrentSize;
178 FAlloc.fst_bytesalloc = 0;
179 if (sys::RetryAfterSignal(-1, ::fcntl, FD, F_PREALLOCATE, &FAlloc) == -1)
180 return CreateError(errnoAsErrorCode());
181 assert(CurrentSize + FAlloc.fst_bytesalloc >= NewSize);
182 return CurrentSize + FAlloc.fst_bytesalloc;
183#else
184 (void)CreateError; // Silence unused variable.
185 return NewSize; // Pretend it worked.
186#endif
187}
188
189bool cas::ondisk::useSmallMappingSize(const Twine &P) {
190 // Add exceptions to use small database file here.
191#if defined(__APPLE__) && __has_include(<sys/mount.h>)
192 // macOS tmpfs does not support sparse tails.
193 SmallString<128> PathStorage;
194 StringRef Path = P.toNullTerminatedStringRef(PathStorage);
195 struct statfs StatFS;
196 if (statfs(Path.data(), &StatFS) != 0)
197 return false;
198
199 if (strcmp(StatFS.f_fstypename, "tmpfs") == 0)
200 return true;
201#endif
202 // Default to use regular database file.
203 return false;
204}
205
206Expected<uint64_t> cas::ondisk::getBootTime() {
207#ifdef __APPLE__
208#if __has_include(<sys/sysctl.h>) && defined(KERN_BOOTTIME)
209 struct timeval TV;
210 size_t TVLen = sizeof(TV);
211 int KernBoot[2] = {CTL_KERN, KERN_BOOTTIME};
212 if (sysctl(KernBoot, 2, &TV, &TVLen, nullptr, 0) < 0)
213 return createStringError(llvm::errnoAsErrorCode(),
214 "failed to get boottime");
215 if (TVLen != sizeof(TV))
216 return createStringError("sysctl kern.boottime unexpected format");
217 return TV.tv_sec;
218#else
219 return 0;
220#endif
221#elif defined(__linux__)
222 // Use the mtime for /proc, which is recreated during system boot.
223 // We could also read /proc/stat and search for 'btime'.
224 sys::fs::file_status Status;
225 if (std::error_code EC = sys::fs::status(path: "/proc", result&: Status))
226 return createFileError(F: "/proc", EC);
227 return Status.getLastModificationTime().time_since_epoch().count();
228#elif defined(_WIN32)
229 // Compute it from the current time and the time since boot, which includes
230 // time spent asleep.
231 auto Uptime = std::chrono::milliseconds(GetTickCount64());
232 auto Boot = std::chrono::system_clock::now() - Uptime;
233 return std::chrono::duration_cast<std::chrono::seconds>(
234 Boot.time_since_epoch())
235 .count();
236#else
237 return 0;
238#endif
239}
240
241Expected<StringRef>
242cas::ondisk::UniqueTempFile::createAndCopyFrom(StringRef ParentPath,
243 StringRef CopyFromPath) {
244 // \c clonefile requires that the destination path doesn't exist. We create
245 // a "placeholder" temporary file, then modify its path a bit and use that
246 // for \c clonefile to write to.
247 // FIXME: Instead of creating a dummy file, add a new file system API for
248 // copying to a unique path that can loop while checking EEXIST.
249 SmallString<256> UniqueTmpPath;
250 SmallString<256> Model;
251 Model += ParentPath;
252 sys::path::append(path&: Model, a: "%%%%%%%.tmp");
253 if (std::error_code EC = sys::fs::createUniqueFile(Model, ResultPath&: UniqueTmpPath))
254 return createFileError(F: Model, EC);
255 TmpPath = std::move(UniqueTmpPath);
256 TmpPath += ".tmp"; // modify so that there's no file at that path.
257 // \c copy_file will use \c clonefile when applicable.
258 if (std::error_code EC = sys::fs::copy_file(From: CopyFromPath, To: TmpPath))
259 return createFileError(F: TmpPath, EC);
260
261 return TmpPath;
262}
263
264Error cas::ondisk::UniqueTempFile::renameTo(StringRef RenameToPath) {
265 if (std::error_code EC = sys::fs::rename(from: TmpPath, to: RenameToPath))
266 return createFileError(F: RenameToPath, EC);
267 TmpPath.clear();
268 return Error::success();
269}
270
271cas::ondisk::UniqueTempFile::~UniqueTempFile() {
272 if (!TmpPath.empty())
273 sys::fs::remove(path: TmpPath);
274 if (!UniqueTmpPath.empty())
275 sys::fs::remove(path: UniqueTmpPath);
276}
277