ProvSQL C/C++ API
Adding support for provenance and uncertainty management to PostgreSQL databases
Loading...
Searching...
No Matches
MappedRegion.h
Go to the documentation of this file.
1/**
2 * @file MappedRegion.h
3 * @brief File-backed memory region with two interchangeable backends.
4 *
5 * A @c MappedRegion owns a backing file and a base pointer to @c length()
6 * bytes of it, with @c map() / @c remap() / @c sync() / @c close(). It is
7 * the single storage primitive under @c MMappedVector and
8 * @c MMappedUUIDHashTable.
9 *
10 * Multi-process build: the region is a shared (@c MAP_SHARED) @c mmap of
11 * the file. The kernel keeps the mapping coherent across the backends and
12 * the worker and flushes dirty pages, so a backend's writes are visible to
13 * the others through the same file.
14 *
15 * Single-process build (@c PROVSQL_INPROCESS_STORE): the region is a heap
16 * buffer loaded from the file on @c map() and written back explicitly on
17 * @c sync() / @c close(). Emscripten does not support @c MAP_SHARED
18 * write-back, and with a single process a shared mapping has no purpose;
19 * the file still lives under @c $PGDATA, so PGlite persists it. Write-back
20 * timing is the caller's responsibility (the store registers an
21 * @c on_proc_exit hook so a backend flushes before it exits).
22 */
23#ifndef MAPPED_REGION_H
24#define MAPPED_REGION_H
25
26#include <cerrno>
27#include <cstddef>
28#include <cstdlib>
29#include <cstring>
30#include <stdexcept>
31#include <string>
32
33#include <fcntl.h>
34#include <unistd.h>
35
36#include "provsql_config.h"
37
38#ifndef PROVSQL_INPROCESS_STORE
39#include <sys/mman.h>
40#endif
41
42/**
43 * @brief Push a file's dirty data pages to stable storage.
44 *
45 * @c fdatasync where the platform has it; macOS does not declare it,
46 * and its @c fsync only reaches the drive's cache, so there the
47 * @c F_FULLFSYNC fcntl (what PostgreSQL itself issues) is the durable
48 * barrier, with @c fsync as the fallback on file systems that reject it.
49 * @return 0 on success, -1 with @c errno set otherwise.
50 */
51static inline int provsql_fdatasync(int fd) {
52#if defined(__APPLE__)
53 if(fcntl(fd, F_FULLFSYNC) == 0)
54 return 0;
55 return fsync(fd);
56#else
57 return fdatasync(fd);
58#endif
59}
60
62int fd_ = -1; ///< Backing file descriptor
63void *base_ = nullptr; ///< Base of the mapped region / heap buffer
64std::size_t length_ = 0; ///< Current region length in bytes
65bool read_only_ = false; ///< Opened read-only (no write-back)
66
67public:
68MappedRegion() = default;
69MappedRegion(const MappedRegion &) = delete;
71
72/**
73 * @brief Open (creating if absent) the backing file.
74 * @return The file's current size in bytes (0 if newly created).
75 */
76std::size_t openFile(const char *filename, bool read_only) {
77 read_only_ = read_only;
78 fd_ = open(filename, O_CREAT | (read_only ? O_RDONLY : O_RDWR), 0600); // flawfinder: ignore
79 if(fd_ == -1)
80 throw std::runtime_error(strerror(errno));
81 auto size = lseek(fd_, 0, SEEK_END);
82 lseek(fd_, 0, SEEK_SET);
83 return static_cast<std::size_t>(size);
84}
85
86/** @brief Set the backing file's size.
87 *
88 * The shared-mmap backend must pre-size the file (mmap maps file-backed
89 * pages). The heap-buffer backend does not: it allocates the buffer and
90 * @c sync() extends the file with @c pwrite. Crucially, leaving the file
91 * unsized until the first @c sync() means a fresh file that is never
92 * synced (e.g. a backend that aborts before write-back) stays empty on
93 * disk and is re-initialised cleanly on reopen, rather than persisting as
94 * a full-size, never-written file whose zero header fails magic
95 * validation. */
96void resizeFile(std::size_t length) {
97#ifdef PROVSQL_INPROCESS_STORE
98 (void) length;
99#else
100 if(ftruncate(fd_, length))
101 throw std::runtime_error(strerror(errno));
102#endif
103}
104
105/** @brief Establish the initial region of @p length bytes over the file. */
106void map(std::size_t length) {
107#ifdef PROVSQL_INPROCESS_STORE
108 base_ = malloc(length);
109 if(!base_)
110 throw std::runtime_error("ProvSQL: out of memory mapping region");
111 ssize_t r = pread(fd_, base_, length, 0); // flawfinder: ignore
112 if(r < 0)
113 throw std::runtime_error(strerror(errno));
114 if(static_cast<std::size_t>(r) < length)
115 memset(static_cast<char *>(base_) + r, 0, length - static_cast<std::size_t>(r));
116#else
117 base_ = ::mmap(nullptr, length, PROT_READ | (read_only_ ? 0 : PROT_WRITE),
118 MAP_SHARED, fd_, 0);
119 if(base_ == MAP_FAILED)
120 throw std::runtime_error(strerror(errno));
121#endif
122 length_ = length;
123}
124
125/** @brief Grow the region to @p new_length, preserving existing content. */
126void remap(std::size_t new_length) {
127#ifdef PROVSQL_INPROCESS_STORE
128 resizeFile(new_length);
129 void *p = realloc(base_, new_length);
130 if(!p)
131 throw std::runtime_error("ProvSQL: out of memory growing region");
132 base_ = p;
133 if(new_length > length_)
134 memset(static_cast<char *>(base_) + length_, 0, new_length - length_);
135#else
136 if(::munmap(base_, length_))
137 throw std::runtime_error(strerror(errno));
138 resizeFile(new_length);
139 base_ = ::mmap(nullptr, new_length, PROT_READ | (read_only_ ? 0 : PROT_WRITE),
140 MAP_SHARED, fd_, 0);
141 if(base_ == MAP_FAILED)
142 throw std::runtime_error(strerror(errno));
143#endif
144 length_ = new_length;
145}
146
147/**
148 * @brief Force the backing file's contents to stable storage.
149 *
150 * @c sync() pushes the region's bytes into the file; this pushes the
151 * file's dirty pages out of the kernel's cache, which is what a crash of
152 * the machine (as opposed to a crash of PostgreSQL) can otherwise lose.
153 * @c fdatasync on the descriptor rather than @c msync on the mapping:
154 * it flushes the file's dirty pages whoever dirtied them, and does not
155 * walk the mapping.
156 */
157void flush() {
158 if(read_only_ || fd_ == -1)
159 return;
160#ifdef PROVSQL_INPROCESS_STORE
161 sync();
162#endif
163 if(provsql_fdatasync(fd_) && errno != EINVAL)
164 throw std::runtime_error(strerror(errno));
165}
166
167/**
168 * @brief Replace the backing file, atomically, with @p length bytes of
169 * @p data, and remap onto the result.
170 *
171 * Used to rewrite a region whose new contents cannot be derived from the
172 * old ones in place -- rehashing the UUID table. The bytes go to a
173 * sibling file, are forced to disk, and then @c rename(2) puts them in
174 * place in one step, so a crash at any point leaves either the complete
175 * old file or the complete new one.
176 *
177 * @param path Path of the backing file (the same one @c openFile opened).
178 * @param data Bytes the region is to hold from now on.
179 * @param length Number of bytes at @p data, and the new size of the region.
180 */
181void replaceContents(const char *path, const void *data, std::size_t length) {
182 if(read_only_)
183 throw std::runtime_error("ProvSQL mmap: cannot replace a read-only region");
184#ifdef PROVSQL_INPROCESS_STORE
185 /* Single process, no shared mapping: swap the heap buffer and let the
186 next sync() write it back. Emscripten has no directory fsync and no
187 crash window to protect against here. */
188 void *p = realloc(base_, length);
189 if(!p)
190 throw std::runtime_error("ProvSQL: out of memory replacing region");
191 base_ = p;
192 memcpy(base_, data, length);
193 length_ = length;
194 sync();
195 (void) path;
196#else
197 std::string tmp = std::string(path) + ".new";
198 int tfd = open(tmp.c_str(), O_CREAT | O_TRUNC | O_RDWR, 0600); // flawfinder: ignore
199 if(tfd == -1)
200 throw std::runtime_error(strerror(errno));
201 const char *p = static_cast<const char *>(data);
202 std::size_t left = length;
203 while(left > 0) {
204 ssize_t w = write(tfd, p, left);
205 if(w <= 0) {
206 int e = errno;
207 ::close(tfd);
208 unlink(tmp.c_str());
209 throw std::runtime_error(strerror(e));
210 }
211 left -= static_cast<std::size_t>(w);
212 p += w;
213 }
214 if(provsql_fdatasync(tfd) || rename(tmp.c_str(), path)) {
215 int e = errno;
216 ::close(tfd);
217 unlink(tmp.c_str());
218 throw std::runtime_error(strerror(e));
219 }
220 ::close(tfd);
221 syncDirectoryOf(path);
222
223 if(base_ && ::munmap(base_, length_))
224 throw std::runtime_error(strerror(errno));
225 base_ = nullptr;
226 if(fd_ != -1)
227 ::close(fd_);
228 fd_ = open(path, O_RDWR); // flawfinder: ignore
229 if(fd_ == -1)
230 throw std::runtime_error(strerror(errno));
231 base_ = ::mmap(nullptr, length, PROT_READ | PROT_WRITE, MAP_SHARED, fd_, 0);
232 if(base_ == MAP_FAILED)
233 throw std::runtime_error(strerror(errno));
234 length_ = length;
235#endif
236}
237
238/** @brief Flush the region to the backing file (no-op when read-only). */
239void sync() {
240 if(read_only_ || !base_)
241 return;
242#ifdef PROVSQL_INPROCESS_STORE
243 if(pwrite(fd_, base_, length_, 0) < 0) // flawfinder: ignore
244 throw std::runtime_error(strerror(errno));
245#else
246 msync(base_, length_, MS_SYNC);
247#endif
248}
249
250/** @brief Write back (if writable) and release the region and file. */
251void close() {
252 if(base_) {
253#ifdef PROVSQL_INPROCESS_STORE
254 sync();
255 free(base_);
256#else
257 ::munmap(base_, length_);
258#endif
259 base_ = nullptr;
260 }
261 if(fd_ != -1) {
262 ::close(fd_);
263 fd_ = -1;
264 }
265}
266
267void *base() const { return base_; }
268std::size_t length() const { return length_; }
269
270private:
271#ifndef PROVSQL_INPROCESS_STORE
272/** @brief Force the directory entry of @p path to disk, so the rename
273 * that created it survives a crash of the machine. */
274static void syncDirectoryOf(const char *path) {
275 std::string dir(path);
276 auto slash = dir.rfind('/');
277 dir = (slash == std::string::npos) ? "." : dir.substr(0, slash);
278 int dfd = open(dir.c_str(), O_RDONLY); // flawfinder: ignore
279 if(dfd == -1)
280 return;
281 fsync(dfd);
282 ::close(dfd);
283}
284#endif
285};
286
287#endif /* MAPPED_REGION_H */
static int provsql_fdatasync(int fd)
Push a file's dirty data pages to stable storage.
void close()
Write back (if writable) and release the region and file.
std::size_t openFile(const char *filename, bool read_only)
Open (creating if absent) the backing file.
void map(std::size_t length)
Establish the initial region of length bytes over the file.
void remap(std::size_t new_length)
Grow the region to new_length, preserving existing content.
bool read_only_
Opened read-only (no write-back).
MappedRegion(const MappedRegion &)=delete
std::size_t length() const
std::size_t length_
Current region length in bytes.
int fd_
Backing file descriptor.
void * base_
Base of the mapped region / heap buffer.
MappedRegion()=default
void sync()
Flush the region to the backing file (no-op when read-only).
static void syncDirectoryOf(const char *path)
Force the directory entry of path to disk, so the rename that created it survives a crash of the mach...
MappedRegion & operator=(const MappedRegion &)=delete
void replaceContents(const char *path, const void *data, std::size_t length)
Replace the backing file, atomically, with length bytes of data, and remap onto the result.
void resizeFile(std::size_t length)
Set the backing file's size.
void flush()
Force the backing file's contents to stable storage.
void * base() const
Build-configuration switches shared across the C and C++ sources.