1
- // ===- MappedFileRegionBumpPtr.cpp ------------------------------------===//
1
+ // ===---------------------------------- ------------------------------------===//
2
2
//
3
3
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4
4
// See https://llvm.org/LICENSE.txt for license information.
5
5
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6
6
//
7
7
// ===----------------------------------------------------------------------===//
8
- // / \file
8
+ // / \file Implements MappedFileRegionBumpPtr.
9
9
// /
10
10
// / A bump pointer allocator, backed by a memory-mapped file.
11
11
// /
20
20
// / and across multiple processes without locking for every read. Our current
21
21
// / implementation strategy is:
22
22
// /
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.
27
27
// / 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.
29
31
// /
30
32
// / Additionally, we attempt to resize the file to its actual data size when
31
33
// / closing the mapping, if this is the only concurrent instance. This is done
35
37
// / which typically loses sparseness. These mitigations only work while the file
36
38
// / is not in use.
37
39
// /
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 .
42
44
// /
43
45
// / To support resizing, we use two separate file locks:
44
46
// / 1. We use a shared reader lock on a ".shared" file until destruction.
54
56
#include " llvm/CAS/MappedFileRegionBumpPtr.h"
55
57
#include " OnDiskCommon.h"
56
58
#include " llvm/CAS/OnDiskCASLogger.h"
57
- #include " llvm/Support/Compiler.h"
58
59
59
60
#if LLVM_ON_UNIX
60
61
#include < sys/stat.h>
@@ -82,6 +83,10 @@ struct FileLockRAII {
82
83
~FileLockRAII () { consumeError (unlock ()); }
83
84
84
85
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
+
85
90
if (std::error_code EC = lockFileThreadSafe (FD, LK))
86
91
return createFileError (Path, EC);
87
92
Locked = LK;
@@ -107,9 +112,15 @@ struct FileSizeInfo {
107
112
} // end anonymous namespace
108
113
109
114
Expected<MappedFileRegionBumpPtr> MappedFileRegionBumpPtr::create (
110
- const Twine &Path, uint64_t Capacity, int64_t BumpPtrOffset ,
115
+ const Twine &Path, uint64_t Capacity, uint64_t HeaderOffset ,
111
116
std::shared_ptr<ondisk::OnDiskCASLogger> Logger,
112
117
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
+
113
124
MappedFileRegionBumpPtr Result;
114
125
Result.Path = Path.str ();
115
126
Result.Logger = std::move (Logger);
@@ -146,66 +157,82 @@ Expected<MappedFileRegionBumpPtr> MappedFileRegionBumpPtr::create(
146
157
if (!FileSize)
147
158
return createFileError (Result.Path , FileSize.getError ());
148
159
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.
149
162
if (FileSize->Size < Capacity) {
150
163
// Lock the file exclusively so only one process will do the initialization.
151
- if (Error E = InitLock.unlock ())
152
- return std::move (E);
153
164
if (Error E = InitLock.lock (sys::fs::LockKind::Exclusive))
154
165
return std::move (E);
155
166
// Retrieve the current size now that we have exclusive access.
156
167
FileSize = FileSizeInfo::get (File);
157
168
if (!FileSize)
158
- return createFileError (Result.Path , FileSize.getError ());
169
+ return createFileError (Result.Path , FileSize.getError ());
159
170
}
160
171
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) {
168
176
assert (InitLock.Locked == sys::fs::LockKind::Exclusive);
169
177
if (std::error_code EC = sys::fs::resize_file_sparse (FD, Capacity))
170
178
return createFileError (Result.Path , EC);
171
-
172
179
if (Result.Logger )
173
180
Result.Logger ->log_MappedFileRegionBumpPtr_resizeFile (
174
181
Result.Path , FileSize->Size , Capacity);
175
- } else {
176
- // Someone else initialized it.
177
- Capacity = FileSize->Size ;
182
+ MappingSize = Capacity;
178
183
}
179
184
180
185
// Create the mapped region.
181
186
{
182
187
std::error_code EC;
183
188
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);
185
190
if (EC)
186
191
return createFileError (Result.Path , EC);
187
192
Result.Region = std::move (Map);
188
193
}
189
194
190
- if (FileSize->Size == 0 ) {
195
+ if (FileSize->Size < MinCapacity ) {
191
196
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 .
193
198
if (Error E = NewFileConstructor (Result))
194
199
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);
197
223
}
198
224
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.
202
230
FileSize = FileSizeInfo::get (File);
203
231
if (!FileSize)
204
232
return createFileError (Result.Path , FileSize.getError ());
205
- assert (InitLock.Locked == sys::fs::LockKind::Exclusive);
206
- Result.H ->AllocatedSize .exchange (FileSize->AllocatedSize );
207
233
}
208
234
235
+ Result.H ->AllocatedSize .exchange (FileSize->AllocatedSize );
209
236
return Result;
210
237
}
211
238
@@ -226,10 +253,11 @@ void MappedFileRegionBumpPtr::destroyImpl() {
226
253
size_t Capacity = capacity ();
227
254
// sync to file system to make sure all contents are up-to-date.
228
255
(void )Region.sync ();
256
+ // unmap the file before resizing since that is the requirement for
257
+ // some platforms.
229
258
Region.unmap ();
230
259
(void )sys::fs::resize_file (*FD, Size);
231
260
(void )unlockFileThreadSafe (*SharedLockFD);
232
-
233
261
if (Logger)
234
262
Logger->log_MappedFileRegionBumpPtr_resizeFile (Path, Capacity, Size);
235
263
}
@@ -251,20 +279,19 @@ void MappedFileRegionBumpPtr::destroyImpl() {
251
279
Logger->log_MappedFileRegionBumpPtr_close (Path);
252
280
}
253
281
254
- void MappedFileRegionBumpPtr::initializeBumpPtr ( int64_t BumpPtrOffset ) {
282
+ void MappedFileRegionBumpPtr::initializeHeader ( uint64_t HeaderOffset ) {
255
283
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 () &&
258
286
" Expected end offset to be pre-allocated" );
259
- assert (isAligned (Align::Of<decltype (*H)>(), BumpPtrOffset ) &&
287
+ assert (isAligned (Align::Of<decltype (*H)>(), HeaderOffset ) &&
260
288
" 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);
267
290
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" );
268
295
if (Logger)
269
296
Logger->log_MappedFileRegionBumpPtr_create (Path, *FD, data (), capacity (),
270
297
size ());
@@ -277,16 +304,16 @@ static Error createAllocatorOutOfSpaceError() {
277
304
278
305
Expected<int64_t > MappedFileRegionBumpPtr::allocateOffset (uint64_t AllocSize) {
279
306
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 ())) {
283
310
// Return the allocation. If the start already passed the end, that means
284
311
// some other concurrent allocations already consumed all the capacity.
285
312
// There is no need to return the original value. If the start was not
286
313
// passed the end, current allocation certainly bumped it passed the end.
287
314
// All other allocation afterwards must have failed and current allocation
288
315
// is in charge of return the allocation back to a valid value.
289
- if (OldEnd <= ( int64_t ) capacity ())
316
+ if (OldEnd <= capacity ())
290
317
(void )H->BumpPtr .exchange (OldEnd);
291
318
292
319
if (Logger)
@@ -296,12 +323,13 @@ Expected<int64_t> MappedFileRegionBumpPtr::allocateOffset(uint64_t AllocSize) {
296
323
return createAllocatorOutOfSpaceError ();
297
324
}
298
325
299
- int64_t DiskSize = H->AllocatedSize ;
326
+ uint64_t DiskSize = H->AllocatedSize ;
300
327
if (LLVM_UNLIKELY (NewEnd > DiskSize)) {
301
- int64_t NewSize;
328
+ uint64_t NewSize;
302
329
// 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))
305
333
return std::move (E);
306
334
assert (NewSize >= DiskSize + Increment);
307
335
// FIXME: on Darwin this can under-count the size if there is a race to
0 commit comments