Skip to content

Commit fa08de2

Browse files
[CAS] Improve MappedFileRegionBumpPtr
Improve MappedFileRegionBumpPtr so it can handle being opened with different capacities.
1 parent f116faf commit fa08de2

File tree

5 files changed

+349
-74
lines changed

5 files changed

+349
-74
lines changed

llvm/include/llvm/CAS/MappedFileRegionBumpPtr.h

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,16 @@
55
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
66
//
77
//===----------------------------------------------------------------------===//
8+
//
9+
/// \file
10+
/// This file declares interface for MappedFileRegionBumpPtr, a bump pointer
11+
/// allocator, backed by a memory-mapped file.
12+
///
13+
//===----------------------------------------------------------------------===//
814

915
#ifndef LLVM_CAS_MAPPEDFILEREGIONBUMPPTR_H
1016
#define LLVM_CAS_MAPPEDFILEREGIONBUMPPTR_H
1117

12-
#include "llvm/Config/llvm-config.h"
1318
#include "llvm/Support/Alignment.h"
1419
#include "llvm/Support/FileSystem.h"
1520
#include <atomic>
@@ -18,7 +23,7 @@ namespace llvm::cas {
1823

1924
namespace ondisk {
2025
class OnDiskCASLogger;
21-
}
26+
} // namespace ondisk
2227

2328
/// Allocator for an owned mapped file region that supports thread-safe and
2429
/// process-safe bump pointer allocation.
@@ -36,28 +41,34 @@ class OnDiskCASLogger;
3641
/// in the same process since file locks will misbehave. Clients should
3742
/// coordinate (somehow).
3843
///
39-
/// \note Currently we allocate the whole file without sparseness on Windows.
40-
///
4144
/// Provides 8-byte alignment for all allocations.
4245
class MappedFileRegionBumpPtr {
4346
public:
4447
using RegionT = sys::fs::mapped_file_region;
4548

49+
/// Header for MappedFileRegionBumpPtr. It can be configured to be located
50+
/// at any location within the file and the allocation will be appended after
51+
/// the header.
52+
struct Header {
53+
std::atomic<uint64_t> BumpPtr;
54+
std::atomic<uint64_t> AllocatedSize;
55+
};
56+
4657
/// Create a \c MappedFileRegionBumpPtr.
4758
///
4859
/// \param Path the path to open the mapped region.
4960
/// \param Capacity the maximum size for the mapped file region.
50-
/// \param BumpPtrOffset the offset at which to store the bump pointer.
61+
/// \param HeaderOffset the offset at which to store the header. This is so
62+
/// that information can be stored before the header, like a file magic.
5163
/// \param NewFileConstructor is for constructing new files. It has exclusive
5264
/// access to the file. Must call \c initializeBumpPtr.
5365
static Expected<MappedFileRegionBumpPtr>
54-
create(const Twine &Path, uint64_t Capacity, int64_t BumpPtrOffset,
66+
create(const Twine &Path, uint64_t Capacity, uint64_t HeaderOffset,
5567
std::shared_ptr<ondisk::OnDiskCASLogger> Logger,
5668
function_ref<Error(MappedFileRegionBumpPtr &)> NewFileConstructor);
5769

58-
/// Finish initializing the bump pointer. Must be called by
59-
/// \c NewFileConstructor.
60-
void initializeBumpPtr(int64_t BumpPtrOffset);
70+
/// Finish initializing the header. Must be called by \c NewFileConstructor.
71+
void initializeHeader(uint64_t HeaderOffset);
6172

6273
/// Minimum alignment for allocations, currently hardcoded to 8B.
6374
static constexpr Align getAlign() {
@@ -108,14 +119,12 @@ class MappedFileRegionBumpPtr {
108119
}
109120

110121
private:
111-
struct Header {
112-
std::atomic<int64_t> BumpPtr;
113-
std::atomic<int64_t> AllocatedSize;
114-
};
115122
RegionT Region;
116123
Header *H = nullptr;
117124
std::string Path;
125+
// File descriptor for the main storage file.
118126
std::optional<int> FD;
127+
// File descriptor for the file used as reader/writer lock.
119128
std::optional<int> SharedLockFD;
120129
std::shared_ptr<ondisk::OnDiskCASLogger> Logger = nullptr;
121130
};

llvm/lib/CAS/MappedFileRegionBumpPtr.cpp

Lines changed: 84 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
//===- MappedFileRegionBumpPtr.cpp ------------------------------------===//
1+
//===----------------------------------------------------------------------===//
22
//
33
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
44
// See https://llvm.org/LICENSE.txt for license information.
55
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
66
//
77
//===----------------------------------------------------------------------===//
8-
/// \file
8+
/// \file Implements MappedFileRegionBumpPtr.
99
///
1010
/// A bump pointer allocator, backed by a memory-mapped file.
1111
///
@@ -20,12 +20,14 @@
2020
/// and across multiple processes without locking for every read. Our current
2121
/// implementation strategy is:
2222
///
23-
/// 1. Use \c ftruncate (\c sys::fs::resize_file) to grow the file to its max
24-
/// size (typically several GB). Many modern filesystems will create a sparse
25-
/// file, so that the trailing unused pages do not take space on disk.
26-
/// 2. Call \c mmap (\c sys::fs::mapped_file_region)
23+
/// 1. Use \c sys::fs::resize_file_sparse to grow the file to its max size
24+
/// (typically several GB). If the file system doesn't support sparse file,
25+
/// this may return a fully allocated file.
26+
/// 2. Call \c sys::fs::mapped_file_region to map the entire file.
2727
/// 3. [Automatic as part of 2.]
28-
/// 4. [Automatic as part of 2.]
28+
/// 4. If supported, use \c fallocate or similiar APIs to ensure the file system
29+
/// storage for the sparse file so we won't end up with partial file if the
30+
/// disk is out of space.
2931
///
3032
/// Additionally, we attempt to resize the file to its actual data size when
3133
/// closing the mapping, if this is the only concurrent instance. This is done
@@ -35,10 +37,10 @@
3537
/// which typically loses sparseness. These mitigations only work while the file
3638
/// is not in use.
3739
///
38-
/// FIXME: we assume that all concurrent users of the file will use the same
39-
/// value for Capacity. Otherwise a process with a larger capacity can write
40-
/// data that is "out of bounds" for processes with smaller capacity. Currently
41-
/// this is true in the CAS.
40+
/// If different values of the capacity is used for concurrent users of the same
41+
/// mapping, the capacity is determined by the first value used to open the
42+
/// file. It is a requirement for the users to always open the file with the
43+
/// same \c HeaderOffset, otherwise the behavior is undefined.
4244
///
4345
/// To support resizing, we use two separate file locks:
4446
/// 1. We use a shared reader lock on a ".shared" file until destruction.
@@ -54,7 +56,6 @@
5456
#include "llvm/CAS/MappedFileRegionBumpPtr.h"
5557
#include "OnDiskCommon.h"
5658
#include "llvm/CAS/OnDiskCASLogger.h"
57-
#include "llvm/Support/Compiler.h"
5859

5960
#if LLVM_ON_UNIX
6061
#include <sys/stat.h>
@@ -82,6 +83,10 @@ struct FileLockRAII {
8283
~FileLockRAII() { consumeError(unlock()); }
8384

8485
Error lock(sys::fs::LockKind LK) {
86+
// Try unlock first. If not locked, this is no-op.
87+
if (auto E = unlock())
88+
return E;
89+
8590
if (std::error_code EC = lockFileThreadSafe(FD, LK))
8691
return createFileError(Path, EC);
8792
Locked = LK;
@@ -107,9 +112,15 @@ struct FileSizeInfo {
107112
} // end anonymous namespace
108113

109114
Expected<MappedFileRegionBumpPtr> MappedFileRegionBumpPtr::create(
110-
const Twine &Path, uint64_t Capacity, int64_t BumpPtrOffset,
115+
const Twine &Path, uint64_t Capacity, uint64_t HeaderOffset,
111116
std::shared_ptr<ondisk::OnDiskCASLogger> Logger,
112117
function_ref<Error(MappedFileRegionBumpPtr &)> NewFileConstructor) {
118+
uint64_t MinCapacity = HeaderOffset + sizeof(Header);
119+
if (Capacity < MinCapacity)
120+
return createStringError(
121+
std::make_error_code(std::errc::invalid_argument),
122+
"capacity is too small to hold MappedFileRegionBumpPtr");
123+
113124
MappedFileRegionBumpPtr Result;
114125
Result.Path = Path.str();
115126
Result.Logger = std::move(Logger);
@@ -146,66 +157,82 @@ Expected<MappedFileRegionBumpPtr> MappedFileRegionBumpPtr::create(
146157
if (!FileSize)
147158
return createFileError(Result.Path, FileSize.getError());
148159

160+
// If the size is smaller than the capacity, we need to initialize the file.
161+
// It maybe empty, or may have been shrunk during a previous close.
149162
if (FileSize->Size < Capacity) {
150163
// Lock the file exclusively so only one process will do the initialization.
151-
if (Error E = InitLock.unlock())
152-
return std::move(E);
153164
if (Error E = InitLock.lock(sys::fs::LockKind::Exclusive))
154165
return std::move(E);
155166
// Retrieve the current size now that we have exclusive access.
156167
FileSize = FileSizeInfo::get(File);
157168
if (!FileSize)
158-
return createFileError(Result.Path, FileSize.getError());
169+
return createFileError(Result.Path, FileSize.getError());
159170
}
160171

161-
// At this point either the file is still under-sized, or we have the size for
162-
// the completely initialized file.
163-
164-
if (FileSize->Size < Capacity) {
165-
// We are initializing the file; it may be empty, or may have been shrunk
166-
// during a previous close.
167-
// FIXME: Detect a case where someone opened it with a smaller capacity.
172+
uint64_t MappingSize = FileSize->Size;
173+
// If the size is still smaller than the minimal required size, we need to
174+
// resize the file to the capacity.
175+
if (FileSize->Size < MinCapacity) {
168176
assert(InitLock.Locked == sys::fs::LockKind::Exclusive);
169177
if (std::error_code EC = sys::fs::resize_file_sparse(FD, Capacity))
170178
return createFileError(Result.Path, EC);
171-
172179
if (Result.Logger)
173180
Result.Logger->log_MappedFileRegionBumpPtr_resizeFile(
174181
Result.Path, FileSize->Size, Capacity);
175-
} else {
176-
// Someone else initialized it.
177-
Capacity = FileSize->Size;
182+
MappingSize = Capacity;
178183
}
179184

180185
// Create the mapped region.
181186
{
182187
std::error_code EC;
183188
sys::fs::mapped_file_region Map(
184-
File, sys::fs::mapped_file_region::readwrite, Capacity, 0, EC);
189+
File, sys::fs::mapped_file_region::readwrite, MappingSize, 0, EC);
185190
if (EC)
186191
return createFileError(Result.Path, EC);
187192
Result.Region = std::move(Map);
188193
}
189194

190-
if (FileSize->Size == 0) {
195+
if (FileSize->Size < MinCapacity) {
191196
assert(InitLock.Locked == sys::fs::LockKind::Exclusive);
192-
// We are creating a new file; run the constructor.
197+
// If we need to fully initialize the file, call NewFileConstructor.
193198
if (Error E = NewFileConstructor(Result))
194199
return std::move(E);
195-
} else {
196-
Result.initializeBumpPtr(BumpPtrOffset);
200+
} else
201+
Result.initializeHeader(HeaderOffset);
202+
203+
if (Result.H->BumpPtr >= FileSize->Size && FileSize->Size < Capacity) {
204+
assert(InitLock.Locked == sys::fs::LockKind::Exclusive);
205+
// If the BumpPtr larger than or equal to the size of the file (it can be
206+
// larger if process is terminated when the out of memory allocation
207+
// happens) and smaller than capacity, this was shrunken by a previous
208+
// close, resize back to capacity and re-initialize the mapped_file_region.
209+
Result.Region.unmap();
210+
if (std::error_code EC = sys::fs::resize_file_sparse(FD, Capacity))
211+
return createFileError(Result.Path, EC);
212+
if (Result.Logger)
213+
Result.Logger->log_MappedFileRegionBumpPtr_resizeFile(
214+
Result.Path, FileSize->Size, Capacity);
215+
216+
std::error_code EC;
217+
sys::fs::mapped_file_region Map(
218+
File, sys::fs::mapped_file_region::readwrite, Capacity, 0, EC);
219+
if (EC)
220+
return createFileError(Result.Path, EC);
221+
Result.Region = std::move(Map);
222+
Result.initializeHeader(HeaderOffset);
197223
}
198224

199-
if (FileSize->Size < Capacity && FileSize->AllocatedSize < Capacity) {
200-
// We are initializing the file; sync the allocated size in case it
201-
// changed when truncating or during construction.
225+
if (InitLock.Locked == sys::fs::LockKind::Exclusive) {
226+
// If holding an exclusive lock, we might have resized the file and
227+
// performed some read/write to the file. Query the file size again to make
228+
// sure everything is up-to-date. Otherwise, FileSize info is already
229+
// up-to-date.
202230
FileSize = FileSizeInfo::get(File);
203231
if (!FileSize)
204232
return createFileError(Result.Path, FileSize.getError());
205-
assert(InitLock.Locked == sys::fs::LockKind::Exclusive);
206-
Result.H->AllocatedSize.exchange(FileSize->AllocatedSize);
207233
}
208234

235+
Result.H->AllocatedSize.exchange(FileSize->AllocatedSize);
209236
return Result;
210237
}
211238

@@ -226,10 +253,11 @@ void MappedFileRegionBumpPtr::destroyImpl() {
226253
size_t Capacity = capacity();
227254
// sync to file system to make sure all contents are up-to-date.
228255
(void)Region.sync();
256+
// unmap the file before resizing since that is the requirement for
257+
// some platforms.
229258
Region.unmap();
230259
(void)sys::fs::resize_file(*FD, Size);
231260
(void)unlockFileThreadSafe(*SharedLockFD);
232-
233261
if (Logger)
234262
Logger->log_MappedFileRegionBumpPtr_resizeFile(Path, Capacity, Size);
235263
}
@@ -251,20 +279,19 @@ void MappedFileRegionBumpPtr::destroyImpl() {
251279
Logger->log_MappedFileRegionBumpPtr_close(Path);
252280
}
253281

254-
void MappedFileRegionBumpPtr::initializeBumpPtr(int64_t BumpPtrOffset) {
282+
void MappedFileRegionBumpPtr::initializeHeader(uint64_t HeaderOffset) {
255283
assert(capacity() < (uint64_t)INT64_MAX && "capacity must fit in int64_t");
256-
int64_t BumpPtrEndOffset = BumpPtrOffset + sizeof(decltype(*H));
257-
assert(BumpPtrEndOffset <= (int64_t)capacity() &&
284+
uint64_t HeaderEndOffset = HeaderOffset + sizeof(decltype(*H));
285+
assert(HeaderEndOffset <= capacity() &&
258286
"Expected end offset to be pre-allocated");
259-
assert(isAligned(Align::Of<decltype(*H)>(), BumpPtrOffset) &&
287+
assert(isAligned(Align::Of<decltype(*H)>(), HeaderOffset) &&
260288
"Expected end offset to be aligned");
261-
H = reinterpret_cast<decltype(H)>(data() + BumpPtrOffset);
262-
263-
int64_t ExistingValue = 0;
264-
if (!H->BumpPtr.compare_exchange_strong(ExistingValue, BumpPtrEndOffset))
265-
assert(ExistingValue >= BumpPtrEndOffset &&
266-
"Expected 0, or past the end of the BumpPtr itself");
289+
H = reinterpret_cast<decltype(H)>(data() + HeaderOffset);
267290

291+
uint64_t ExistingValue = 0;
292+
if (!H->BumpPtr.compare_exchange_strong(ExistingValue, HeaderEndOffset))
293+
assert(ExistingValue >= HeaderEndOffset &&
294+
"Expected 0, or past the end of the header itself");
268295
if (Logger)
269296
Logger->log_MappedFileRegionBumpPtr_create(Path, *FD, data(), capacity(),
270297
size());
@@ -277,16 +304,16 @@ static Error createAllocatorOutOfSpaceError() {
277304

278305
Expected<int64_t> MappedFileRegionBumpPtr::allocateOffset(uint64_t AllocSize) {
279306
AllocSize = alignTo(AllocSize, getAlign());
280-
int64_t OldEnd = H->BumpPtr.fetch_add(AllocSize);
281-
int64_t NewEnd = OldEnd + AllocSize;
282-
if (LLVM_UNLIKELY(NewEnd > (int64_t)capacity())) {
307+
uint64_t OldEnd = H->BumpPtr.fetch_add(AllocSize);
308+
uint64_t NewEnd = OldEnd + AllocSize;
309+
if (LLVM_UNLIKELY(NewEnd > capacity())) {
283310
// Return the allocation. If the start already passed the end, that means
284311
// some other concurrent allocations already consumed all the capacity.
285312
// There is no need to return the original value. If the start was not
286313
// passed the end, current allocation certainly bumped it passed the end.
287314
// All other allocation afterwards must have failed and current allocation
288315
// is in charge of return the allocation back to a valid value.
289-
if (OldEnd <= (int64_t)capacity())
316+
if (OldEnd <= capacity())
290317
(void)H->BumpPtr.exchange(OldEnd);
291318

292319
if (Logger)
@@ -296,12 +323,13 @@ Expected<int64_t> MappedFileRegionBumpPtr::allocateOffset(uint64_t AllocSize) {
296323
return createAllocatorOutOfSpaceError();
297324
}
298325

299-
int64_t DiskSize = H->AllocatedSize;
326+
uint64_t DiskSize = H->AllocatedSize;
300327
if (LLVM_UNLIKELY(NewEnd > DiskSize)) {
301-
int64_t NewSize;
328+
uint64_t NewSize;
302329
// The minimum increment is a page, but allocate more to amortize the cost.
303-
constexpr int64_t Increment = 1 * 1024 * 1024; // 1 MB
304-
if (Error E = preallocateFileTail(*FD, DiskSize, DiskSize + Increment).moveInto(NewSize))
330+
constexpr uint64_t Increment = 1 * 1024 * 1024; // 1 MB
331+
if (Error E = preallocateFileTail(*FD, DiskSize, DiskSize + Increment)
332+
.moveInto(NewSize))
305333
return std::move(E);
306334
assert(NewSize >= DiskSize + Increment);
307335
// FIXME: on Darwin this can under-count the size if there is a race to

llvm/lib/CAS/OnDiskHashMappedTrie.cpp

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ class DatabaseFile {
123123
uint64_t Magic;
124124
uint64_t Version;
125125
std::atomic<int64_t> RootTableOffset;
126-
std::atomic<int64_t> BumpPtr;
126+
MappedFileRegionBumpPtr::Header MappedFileHeader;
127127
};
128128

129129
const Header &getHeader() { return *H; }
@@ -185,16 +185,16 @@ DatabaseFile::create(const Twine &Path, uint64_t Capacity,
185185
return createTableConfigError(std::errc::argument_out_of_domain,
186186
Path.str(), "datafile",
187187
"Allocator too small for header");
188-
(void)new (Alloc.data()) Header{getMagic(), getVersion(), {0}, {0}};
189-
Alloc.initializeBumpPtr(offsetof(Header, BumpPtr));
188+
(void)new (Alloc.data()) Header{getMagic(), getVersion(), {0}, {}};
189+
Alloc.initializeHeader(offsetof(Header, MappedFileHeader));
190190
DatabaseFile DB(Alloc);
191191
return NewDBConstructor(DB);
192192
};
193193

194194
// Get or create the file.
195195
MappedFileRegionBumpPtr Alloc;
196196
if (Error E = MappedFileRegionBumpPtr::create(
197-
Path, Capacity, offsetof(Header, BumpPtr),
197+
Path, Capacity, offsetof(Header, MappedFileHeader),
198198
std::move(Logger), NewFileConstructor)
199199
.moveInto(Alloc))
200200
return std::move(E);
@@ -264,7 +264,7 @@ Error DatabaseFile::validate(MappedFileRegion &Region) {
264264
"database: wrong version");
265265

266266
// Check the bump-ptr, which should point past the header.
267-
if (H->BumpPtr.load() < (int64_t)sizeof(Header))
267+
if (H->MappedFileHeader.BumpPtr.load() < (int64_t)sizeof(Header))
268268
return createStringError(std::errc::invalid_argument,
269269
"database: corrupt bump-ptr");
270270

0 commit comments

Comments
 (0)