ProvSQL C/C++ API
Adding support for provenance and uncertainty management to PostgreSQL databases
Loading...
Searching...
No Matches
MMappedUUIDHashTable.cpp
Go to the documentation of this file.
1/**
2 * @file MMappedUUIDHashTable.cpp
3 * @brief Open-addressing hash table over a memory-mapped file: implementation.
4 *
5 * Implements all methods of @c MMappedUUIDHashTable declared in
6 * @c MMappedUUIDHashTable.h:
7 * - @c MMappedUUIDHashTable(): open/create the backing file and map it.
8 * - @c ~MMappedUUIDHashTable(): sync and unmap.
9 * - @c add(): insert a UUID and assign the next sequential integer.
10 * - @c operator[](): look up an integer by UUID.
11 * - @c sync(): flush the backing region (@c MappedRegion::sync()).
12 *
13 * Internal helpers:
14 * - @c grow(): double the table size and rehash.
15 * - @c find(): locate the slot index for a UUID (or @c NOTHING if absent).
16 * - @c set(): write a key-value pair into the table.
17 */
19
20#include <cassert>
21#include <cerrno>
22#include <cstdint>
23#include <cstring>
24#include <new>
25#include <stdexcept>
26#include <string>
27#include <vector>
28
29#include <fcntl.h>
30#include <unistd.h>
31
32#include <sys/mman.h>
33
34MMappedUUIDHashTable::MMappedUUIDHashTable(const char *filename, bool read_only, uint64_t magic_value)
35{
36 path_ = filename;
37 read_only_ = read_only;
38 auto size = region.openFile(filename, read_only);
39 bool empty = (size == 0);
40
41 if(empty) {
43 region.resizeFile(size);
44 }
45
46 region.map(size);
47 table = reinterpret_cast<table_t *>(region.base());
48
49 if(empty) {
50 table->magic = magic_value;
51 table->version = 1;
52 table->elem_size = static_cast<uint16_t>(sizeof(value_t));
53 table->flags = 0;
54 table->log_size = table_t::logSizeForSize(size);
55 table->nb_elements = 0;
56 table->next_value = 0;
57 for(unsigned long i=0; i<table->capacity(); ++i) {
58 table->t[i].value = NOTHING;
59 }
60 } else {
61 if(table->magic != magic_value)
62 throw std::runtime_error("ProvSQL mmap: wrong file type (magic mismatch)");
63 if(table->version != 1)
64 throw std::runtime_error("ProvSQL mmap: unsupported format version "
65 + std::to_string(table->version));
66 if(table->elem_size != sizeof(value_t))
67 throw std::runtime_error("ProvSQL mmap: element size mismatch (recompile required)");
68 unclean_ = (table->flags & FLAG_DIRTY) != 0;
69 }
70
71 /* Mark the file open for writing; the destructor clears it. Found still
72 set on open, it says the previous writer died mid-write, which
73 provsql.check_store() reports. */
74 if(!read_only)
75 table->flags |= FLAG_DIRTY;
76}
77
78/* Rehashing rebuilds the whole table, so it cannot be done in place: a
79 process killed part-way through an in-place rehash leaves the mapping
80 of every not-yet-reinserted token gone, and each of those tokens then
81 reads back as a fresh input -- silently, since an unknown token is a
82 valid input gate. Instead the new table is built in memory and handed
83 to MappedRegion::replaceContents, which writes it to a sibling file,
84 forces it to disk, and renames it into place: a crash at any point
85 leaves either the complete old table or the complete new one. */
87{
88 const unsigned new_log_size = table->log_size + 1;
89 const std::size_t new_size = table_t::sizeForLogSize(new_log_size);
90 const unsigned long new_capacity = 1ul << new_log_size;
91
92 std::vector<char> buf(new_size);
93 table_t *nt = reinterpret_cast<table_t *>(buf.data());
94 nt->magic = table->magic;
95 nt->version = table->version;
96 nt->elem_size = table->elem_size;
97 nt->flags = table->flags;
98 nt->log_size = new_log_size;
99 nt->nb_elements = table->nb_elements;
100 nt->next_value = table->next_value;
101 for(unsigned long i=0; i<new_capacity; ++i)
102 nt->t[i].value = NOTHING;
103
104 for(unsigned long i=0; i<table->capacity(); ++i) {
105 if(table->t[i].value == NOTHING)
106 continue;
107 const value_t &e = table->t[i];
108 unsigned long k =
109 (*reinterpret_cast<const unsigned long *>(&e.uuid)) % new_capacity;
110 while(nt->t[k].value != NOTHING)
111 k = (k + 1) % new_capacity;
112 nt->t[k] = e;
113 }
114
115 region.replaceContents(path_.c_str(), buf.data(), new_size);
116 table = reinterpret_cast<table_t *>(region.base());
117}
118
120{
121 if(table && !read_only_)
122 table->flags &= ~FLAG_DIRTY;
123 region.close();
124}
125
127{
128 auto k = hash(u);
129 while(table->t[k].value != NOTHING &&
130 std::memcmp(&table->t[k].uuid, &u, sizeof(pg_uuid_t))) {
131 k = (k+1) % table->capacity();
132 }
133
134 return k;
135}
136
138{
139 auto k = find(u);
140
141 return table->t[k].value;
142}
143
144std::pair<unsigned long,bool> MMappedUUIDHashTable::add(pg_uuid_t u)
145{
146 auto k = find(u);
147 if(table->t[k].value != NOTHING)
148 return std::make_pair(table->t[k].value, false);
149 return publish(u, table->next_value);
150}
151
152std::pair<unsigned long,bool> MMappedUUIDHashTable::publish(pg_uuid_t u,
153 unsigned long value)
154{
155 auto k = find(u);
156
157 if(table->t[k].value != NOTHING)
158 return std::make_pair(table->t[k].value, false);
159
160 if(table->nb_elements >= MAXIMUM_LOAD_FACTOR * table->capacity())
161 grow();
162 k = find(u);
163
164 ++table->nb_elements;
165 table->t[k].uuid = u;
166 if(value + 1 > table->next_value)
167 table->next_value = value + 1;
168 /* The value store publishes the entry: it is the last write, and an
169 aligned 8-byte store, so a reader sees NOTHING or the whole thing. */
170 table->t[k].value = value;
171 return std::make_pair(value, true);
172}
173
175{
176 region.sync();
177}
178
180{
181 region.flush();
182}
Open-addressing hash table mapping UUIDs to integers, backed by an mmap file.
bool read_only_
Mapped read-only: the header must not be written.
unsigned long hash(pg_uuid_t u) const
Compute the starting slot index for UUID u.
std::pair< unsigned long, bool > publish(pg_uuid_t u, unsigned long value)
Insert UUID u with a caller-chosen value.
static constexpr uint32_t FLAG_DIRTY
Bit of table_t::flags set while the file is open for writing.
void grow()
Double the table capacity and rehash all existing entries.
std::pair< unsigned long, bool > add(pg_uuid_t u)
Insert UUID u, assigning it the next available integer.
MMappedUUIDHashTable(const char *filename, bool read_only, uint64_t magic)
Open (or create) the mmap-backed hash table.
bool unclean_
The previous run left the dirty bit set.
static constexpr unsigned STARTING_LOG_SIZE
Initial log2 capacity (65 536 slots).
unsigned long find(pg_uuid_t u) const
Find the slot index of u, or NOTHING if absent.
unsigned long operator[](pg_uuid_t u) const
Look up the integer index for UUID u.
std::string path_
Backing file, for the atomic rehash in grow().
~MMappedUUIDHashTable()
Sync and unmap the file.
MappedRegion region
Backing storage (shared mmap, or heap buffer).
void sync()
Flush the backing region to its file (MappedRegion::sync()).
static constexpr unsigned long NOTHING
Sentinel returned by operator[]() when the UUID is not present.
void flush()
Force the backing file to stable storage (MappedRegion::flush()).
static constexpr double MAXIMUM_LOAD_FACTOR
Rehash when this fraction of slots is occupied.
table_t * table
Typed view of region.base().
On-disk layout of the hash table stored in the mmap file.
static constexpr unsigned logSizeForSize(std::size_t size)
Compute the log2 of the slot count from the file size.
value_t t[]
Flexible array of hash-table slots.
uint32_t flags
Bit 0: opened for writing and not closed since.
static constexpr std::size_t sizeForLogSize(unsigned ls)
Compute the file size required for a table with 2^ls slots.
uint64_t magic
File-type identifier.
unsigned log_size
log2 of the number of slots
uint16_t elem_size
sizeof(value_t) at write time
unsigned long nb_elements
Current number of stored key-value pairs.
unsigned long next_value
Next integer value to assign to a new UUID.
uint16_t version
Format version (currently 1).
One slot in the hash table: a UUID key and its associated integer value.
unsigned long value
Associated integer (gate index), or 0 if slot is empty.