88#include <shared_mutex>
94#include <unordered_map>
98#define WIN32_LEAN_AND_MEAN
107namespace fs = std::filesystem;
120using Bytes = std::span<const std::byte>;
135 using std::runtime_error::runtime_error;
139 using std::runtime_error::runtime_error;
143 using io_error::io_error;
147 using std::runtime_error::runtime_error;
155inline constexpr uint8_t MAGIC[8] = {
'F',
'L',
'U',
'X',
'E',
'N',
'0',
'1'};
156inline constexpr uint8_t FLAG_LIVE = 0x00;
157inline constexpr uint8_t FLAG_TOMB = 0x01;
158inline constexpr uint8_t MAX_KEY = 255;
159inline constexpr size_t HEADER_SIZE = 6;
169inline void encode_header(uint8_t out[HEADER_SIZE],
170 const EntryHeader &h)
noexcept {
173 out[2] =
static_cast<uint8_t
>(h.val_len & 0xFFu);
174 out[3] =
static_cast<uint8_t
>((h.val_len >> 8) & 0xFFu);
175 out[4] =
static_cast<uint8_t
>((h.val_len >> 16) & 0xFFu);
176 out[5] =
static_cast<uint8_t
>((h.val_len >> 24) & 0xFFu);
180inline auto decode_header(
const uint8_t in[HEADER_SIZE])
noexcept
185 .val_len =
static_cast<uint32_t
>(in[2]) |
186 static_cast<uint32_t
>(in[3]) << 8 |
187 static_cast<uint32_t
>(in[4]) << 16 |
188 static_cast<uint32_t
>(in[5]) << 24,
199 using is_transparent = void;
201 auto operator()(std::string_view sv)
const noexcept ->
size_t {
202 return std::hash<std::string_view>{}(sv);
204 auto operator()(
const std::string &s)
const noexcept ->
size_t {
205 return std::hash<std::string_view>{}(s);
210auto utf8_to_wstring(
const std::string &utf8) -> std::wstring {
211 int len = MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1,
nullptr, 0);
215 std::wstring wide(len - 1, L
'\0');
216 MultiByteToWideChar(CP_UTF8, 0, utf8.c_str(), -1, wide.data(), len);
222std::unordered_map<std::string, IndexEntry, StringHash, std::equal_to<>>;
228 HANDLE file_ = INVALID_HANDLE_VALUE;
229 HANDLE map_ =
nullptr;
233 uint8_t *ptr_ =
nullptr;
235 size_t file_size_ = 0;
236 std::atomic<bool> dirty_{
false};
240 MappedFile() =
default;
241 ~MappedFile() { close(); }
243 MappedFile(
const MappedFile &) =
delete;
244 auto operator=(
const MappedFile &) -> MappedFile & =
delete;
245 MappedFile(
const MappedFile &&) =
delete;
246 auto operator=(MappedFile &&) -> MappedFile & =
delete;
248 auto open(
const std::string &path) ->
bool {
250 path_ = utf8_to_wstring(path);
251 file_ = CreateFileW(path_.c_str(), GENERIC_READ | GENERIC_WRITE, 0,
nullptr,
252 OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL,
nullptr);
253 if (file_ == INVALID_HANDLE_VALUE) {
256 SetFilePointer(file_, 0,
nullptr, FILE_END);
259 fd_ = ::open(path_.c_str(), O_RDWR | O_CREAT | O_APPEND, 0644);
270 if (file_ != INVALID_HANDLE_VALUE) {
272 file_ = INVALID_HANDLE_VALUE;
282 auto remap() ->
bool {
287 dirty_.store(
false, std::memory_order_release);
292 map_ = CreateFileMappingW(file_,
nullptr, PAGE_READWRITE, 0, 0,
nullptr);
295 dirty_.store(
false, std::memory_order_release);
299 ptr_ =
static_cast<uint8_t *
>(
300 MapViewOfFile(map_, FILE_MAP_ALL_ACCESS, 0, 0, size_));
305 dirty_.store(
false, std::memory_order_release);
309 ptr_ =
static_cast<uint8_t *
>(
310 ::mmap(
nullptr, size_, PROT_READ | PROT_WRITE, MAP_SHARED, fd_, 0));
312 if (ptr_ == MAP_FAILED) {
314 dirty_.store(
false, std::memory_order_release);
318 dirty_.store(
false, std::memory_order_release);
322 auto append(
const void *data,
size_t len) ->
bool {
328 if (!WriteFile(file_, data,
static_cast<DWORD
>(len), &written,
nullptr) ||
329 written !=
static_cast<DWORD
>(len)) {
333 if (::write(fd_, data, len) !=
static_cast<ssize_t
>(len)) {
337 dirty_.store(
true, std::memory_order_relaxed);
347 [[nodiscard]]
auto sync() ->
bool {
349 return FlushFileBuffers(file_) != 0;
351 return ::fsync(fd_) == 0;
365 [[nodiscard]]
auto truncate(
size_t new_size) ->
bool {
369 li.QuadPart =
static_cast<LONGLONG
>(new_size);
370 if (!SetFilePointerEx(file_, li,
nullptr, FILE_BEGIN)) {
373 if (!SetEndOfFile(file_)) {
374 SetFilePointer(file_, 0,
nullptr, FILE_END);
377 SetFilePointer(file_, 0,
nullptr, FILE_END);
379 if (::ftruncate(fd_,
static_cast<off_t
>(new_size)) != 0) {
383 file_size_ = new_size;
384 dirty_.store(
true, std::memory_order_release);
413 auto rewrite(
const std::vector<uint8_t> &data) ->
bool {
414 fs::path tmp_path = path_;
418 HANDLE tmp = CreateFileW(tmp_path.c_str(), GENERIC_WRITE, 0,
nullptr,
419 CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL,
nullptr);
420 if (tmp == INVALID_HANDLE_VALUE) {
424 const bool write_ok =
425 WriteFile(tmp, data.data(),
static_cast<DWORD
>(data.size()), &written,
427 written ==
static_cast<DWORD
>(data.size()) &&
428 FlushFileBuffers(tmp) != 0;
433 DeleteFileW(tmp_path.c_str());
438 ::open(tmp_path.c_str(), O_RDWR | O_CREAT | O_TRUNC, 0644);
442 const bool write_ok = ::write(tmp_fd, data.data(), data.size()) ==
443 static_cast<ssize_t
>(data.size()) &&
444 ::fsync(tmp_fd) == 0;
449 ::unlink(tmp_path.c_str());
455 if (file_ != INVALID_HANDLE_VALUE) {
457 file_ = INVALID_HANDLE_VALUE;
467 if (!ReplaceFileW(path_.c_str(), tmp_path.c_str(),
nullptr,
468 REPLACEFILE_IGNORE_MERGE_ERRORS,
nullptr,
nullptr)) {
470 DeleteFileW(tmp_path.c_str());
473 CreateFileW(path_.c_str(), GENERIC_READ | GENERIC_WRITE, 0,
nullptr,
474 OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,
nullptr);
476 if (file_ == INVALID_HANDLE_VALUE) {
477 throw io_error(
"fluxen: failed to reopen database file after replace");
480 SetFilePointer(file_, 0,
nullptr, FILE_END);
482 throw io_error(
"fluxen: remap failed after replace failure");
487 if (::rename(tmp_path.c_str(), path_.c_str()) != 0) {
488 ::unlink(tmp_path.c_str());
489 fd_ = ::open(path_.c_str(), O_RDWR | O_APPEND, 0644);
491 throw io_error(
"fluxen: failed to reopen database file after rename");
494 throw io_error(
"fluxen: remap failed after rename failure");
501 file_ = CreateFileW(path_.c_str(), GENERIC_READ | GENERIC_WRITE, 0,
nullptr,
502 OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,
nullptr);
503 if (file_ == INVALID_HANDLE_VALUE) {
505 "fluxen: failed to reopen database file after successful replace");
507 SetFilePointer(file_, 0,
nullptr, FILE_END);
509 fd_ = ::open(path_.c_str(), O_RDWR | O_APPEND, 0644);
512 "fluxen: failed to reopen database file after successful rename");
516 throw io_error(
"fluxen: remap failed after successful rename");
526 [[nodiscard]]
auto ptr() const noexcept -> const uint8_t * {
return ptr_; }
533 [[nodiscard]]
auto is_dirty() const noexcept ->
bool {
534 return dirty_.load(std::memory_order_acquire);
537 [[nodiscard]]
auto size() const noexcept ->
size_t {
return file_size_; }
544 UnmapViewOfFile(ptr_);
550 ::munmap(ptr_, size_);
556 [[nodiscard]]
auto file_size() const noexcept ->
size_t {
559 GetFileSizeEx(file_, &sz);
560 return static_cast<size_t>(sz.QuadPart);
564 return static_cast<size_t>(st.st_size);
593 std::vector<uint8_t> val;
596 std::vector<Op> ops_;
606 void put(std::string_view key, std::string_view value) {
607 if (key.empty() || key.size() > detail::MAX_KEY) {
608 throw key_error(
"fluxen: key must be between 1 and 255 bytes");
610 ops_.push_back({.key = std::string(key),
611 .val = std::vector<uint8_t>(value.begin(), value.end()),
612 .is_delete =
false});
635 template <
typename T>
636 requires(std::is_trivially_copyable_v<T> &&
637 !std::is_convertible_v<T, std::string_view>)
638 void put(std::string_view key,
const T &value) {
639 if (key.empty() || key.size() > detail::MAX_KEY) {
640 throw key_error(
"fluxen: key must be between 1 and 255 bytes");
642 const auto *p =
reinterpret_cast<const uint8_t *
>(&value);
643 ops_.push_back({.key = std::string(key),
644 .val = std::vector<uint8_t>(p, p +
sizeof(T)),
645 .is_delete =
false});
658 if (key.empty() || key.size() > detail::MAX_KEY) {
659 throw key_error(
"fluxen: key must be between 1 and 255 bytes");
661 ops_.push_back({.key = std::string(key), .val = {}, .is_delete =
true});
703 detail::IndexMap index_;
704 mutable detail::MappedFile file_;
705 mutable std::shared_mutex mu_;
706 mutable std::mutex sync_mutex_;
707 bool poisoned_ =
false;
709 void check_poisoned()
const {
712 "fluxen: database is poisoned due to an unrecoverable I/O error");
737 explicit DB(
const std::string &path) {
738 if (!file_.open(path)) {
739 throw io_error(
"fluxen: failed to open '" + path +
"'");
742 if (file_.size() == 0) {
758 DB(
const DB &) =
delete;
759 auto operator=(
const DB &) ->
DB & =
delete;
784 void put(std::string_view key, std::string_view value) {
786 std::unique_lock lock(mu_);
787 append_entry(key,
reinterpret_cast<const uint8_t *
>(value.data()),
788 static_cast<uint32_t
>(value.size()),
false);
820 template <
typename T>
821 requires(std::is_trivially_copyable_v<T> &&
822 !std::is_convertible_v<T, std::string_view>)
823 void put(std::string_view key,
const T &value) {
825 std::unique_lock lock(mu_);
826 append_entry(key,
reinterpret_cast<const uint8_t *
>(&value),
827 static_cast<uint32_t
>(
sizeof(T)),
false);
866 template <
typename T = std::
string>
867 auto get(std::string_view key)
const -> std::optional<T> {
869 std::shared_lock lock(mu_);
872 auto it = index_.find(key);
873 if (it == index_.end()) {
877 const auto &entry = it->second;
879 if constexpr (std::is_same_v<T, std::string>) {
880 const auto *ptr = file_.ptr() + entry.val_offset;
881 return std::string(
reinterpret_cast<const char *
>(ptr), entry.val_len);
883 static_assert(std::is_trivially_copyable_v<T>,
884 "fluxen: T must be trivially copyable");
885 if (entry.val_len !=
static_cast<uint32_t
>(
sizeof(T))) {
889 std::memcpy(&result, file_.ptr() + entry.val_offset,
sizeof(T));
917 std::unique_lock lock(mu_);
918 append_entry(key,
nullptr, 0,
true);
934 [[nodiscard]]
auto has(std::string_view key)
const ->
bool {
936 std::shared_lock lock(mu_);
938 return index_.contains(key);
970 void each(
const std::function<
void(std::string_view,
Bytes)> &fn)
const {
972 std::shared_lock lock(mu_);
974 for (
const auto &[key, entry] : index_) {
976 reinterpret_cast<const std::byte *
>(file_.ptr() + entry.val_offset);
977 fn(key,
Bytes{ptr, entry.val_len});
1012 const std::function<
void(std::string_view,
Bytes)> &fn)
const {
1014 std::shared_lock lock(mu_);
1016 for (
const auto &[key, entry] : index_) {
1017 if (key.starts_with(pfx)) {
1019 reinterpret_cast<const std::byte *
>(file_.ptr() + entry.val_offset);
1020 fn(key,
Bytes{ptr, entry.val_len});
1080 if (result == rollback) {
1084 std::vector<uint8_t> batch;
1085 batch.reserve(tx.ops_.size() * 64);
1086 for (
const auto &op : tx.ops_) {
1087 detail::EntryHeader hdr{
1088 .flags = op.is_delete ? detail::FLAG_TOMB : detail::FLAG_LIVE,
1089 .key_len =
static_cast<uint8_t
>(op.key.size()),
1090 .val_len = op.is_delete ? 0u :
static_cast<uint32_t
>(op.val.size()),
1092 uint8_t raw[detail::HEADER_SIZE];
1093 detail::encode_header(raw, hdr);
1094 batch.insert(batch.end(), raw, raw + detail::HEADER_SIZE);
1095 batch.insert(batch.end(), op.key.begin(), op.key.end());
1096 if (!op.is_delete) {
1097 batch.insert(batch.end(), op.val.begin(), op.val.end());
1101 std::unique_lock lock(mu_);
1103 const size_t size_before = file_.size();
1105 if (!file_.append(batch.data(), batch.size())) {
1106 throw io_error(
"fluxen: transaction append failed");
1109 if (!file_.sync()) {
1110 if (!file_.truncate(size_before)) {
1113 "fluxen: transaction fsync failed and truncation failed. Database "
1114 "file may contain a partial tail entry");
1116 throw io_error(
"fluxen: transaction fsync failed");
1120 for (
const auto &op : tx.ops_) {
1121 pos += detail::HEADER_SIZE + op.key.size();
1123 index_.erase(op.key);
1125 index_[op.key] = {.val_offset = size_before + pos,
1126 .val_len =
static_cast<uint32_t
>(op.val.size())};
1127 pos += op.val.size();
1171 std::unique_lock lock(mu_);
1173 if (file_.is_dirty() && !file_.remap()) {
1174 throw io_error(
"fluxen: remap failed before compaction");
1177 std::vector<uint8_t> buf;
1178 buf.reserve(file_.size());
1180 buf.insert(buf.end(), detail::MAGIC, detail::MAGIC +
sizeof(detail::MAGIC));
1182 detail::IndexMap new_index;
1183 for (
const auto &[key, entry] : index_) {
1184 detail::EntryHeader hdr{
1185 .flags = detail::FLAG_LIVE,
1186 .key_len =
static_cast<uint8_t
>(key.size()),
1187 .val_len = entry.val_len,
1190 uint8_t raw[detail::HEADER_SIZE];
1191 detail::encode_header(raw, hdr);
1193 size_t val_off = buf.size() + detail::HEADER_SIZE + key.size();
1195 buf.insert(buf.end(), raw, raw + detail::HEADER_SIZE);
1196 buf.insert(buf.end(), key.begin(), key.end());
1198 const auto *val_ptr = file_.ptr() + entry.val_offset;
1199 buf.insert(buf.end(), val_ptr, val_ptr + entry.val_len);
1201 new_index[key] = {.val_offset = val_off, .val_len = entry.val_len};
1204 if (!file_.rewrite(buf)) {
1208 index_ = std::move(new_index);
1224 std::shared_lock lock(mu_);
1225 return index_.size();
1242 std::shared_lock lock(mu_);
1243 return file_.size();
1249 if (!file_.append(detail::MAGIC,
sizeof(detail::MAGIC))) {
1250 throw corrupt_error(
"fluxen: failed to write magic header");
1263 if (file_.size() <
sizeof(detail::MAGIC)) {
1264 throw corrupt_error(
"fluxen: file too small to be valid");
1267 if (std::memcmp(file_.ptr(), detail::MAGIC,
sizeof(detail::MAGIC)) != 0) {
1268 throw corrupt_error(
"fluxen: bad magic. File was not created by fluxen");
1271 size_t pos =
sizeof(detail::MAGIC);
1272 size_t last_good_pos = pos;
1274 while (pos + detail::HEADER_SIZE <= file_.size()) {
1275 uint8_t raw[detail::HEADER_SIZE];
1276 std::memcpy(raw, file_.ptr() + pos, detail::HEADER_SIZE);
1277 detail::EntryHeader hdr = detail::decode_header(raw);
1278 pos += detail::HEADER_SIZE;
1280 if (pos + hdr.key_len + hdr.val_len > file_.size()) {
1284 std::string key(
reinterpret_cast<const char *
>(file_.ptr() + pos),
1288 if (hdr.flags == detail::FLAG_TOMB) {
1291 index_[key] = {.val_offset = pos, .val_len = hdr.val_len};
1295 last_good_pos = pos;
1298 if (last_good_pos < file_.size()) {
1299 if (!file_.truncate(last_good_pos)) {
1300 throw corrupt_error(
1301 "fluxen: failed to truncate partial tail entry on open");
1315 void append_entry(std::string_view key,
const uint8_t *val, uint32_t val_len,
1317 if (key.empty() || key.size() > detail::MAX_KEY) {
1318 throw key_error(
"fluxen: key must be between 1 and 255 bytes");
1321 detail::EntryHeader hdr{
1322 .flags = tombstone ? detail::FLAG_TOMB : detail::FLAG_LIVE,
1323 .key_len =
static_cast<uint8_t
>(key.size()),
1327 std::vector<uint8_t> buf;
1328 buf.resize(detail::HEADER_SIZE + key.size() + val_len);
1329 detail::encode_header(buf.data(), hdr);
1330 std::memcpy(buf.data() + detail::HEADER_SIZE, key.data(), key.size());
1331 if (val && val_len) {
1332 std::memcpy(buf.data() + detail::HEADER_SIZE + key.size(), val, val_len);
1335 const size_t size_before = file_.size();
1337 if (!file_.append(buf.data(), buf.size())) {
1338 if (!file_.truncate(size_before)) {
1340 throw poisoned_error(
1341 "fluxen: append failed and truncation failed. Database file may "
1342 "contain a partial tail entry");
1344 throw io_error(
"fluxen: append failed");
1348 if (
auto it = index_.find(key); it != index_.end()) {
1352 index_[std::string(key)] = {.val_offset = file_.size() - val_len,
1353 .val_len = val_len};
1375 void ensure_mapped()
const {
1376 if (!file_.is_dirty()) {
1380 std::unique_lock sync_lock(sync_mutex_);
1381 if (file_.is_dirty() && !file_.remap()) {
1382 throw io_error(
"fluxen: remap failed");
A persistent key-value database backed by a single file.
Definition fluxen.hpp:701
auto has(std::string_view key) const -> bool
Returns true if the given key exists in the database.
Definition fluxen.hpp:934
auto file_size() const -> size_t
Returns the current size of the database file in bytes.
Definition fluxen.hpp:1240
auto key_count() const -> size_t
Returns the number of live keys currently stored.
Definition fluxen.hpp:1222
void prefix(std::string_view pfx, const std::function< void(std::string_view, Bytes)> &fn) const
Iterates over all keys that begin with the given prefix.
Definition fluxen.hpp:1011
void transaction(const std::function< TxResult(Tx &)> &fn)
Executes a batch of operations atomically.
Definition fluxen.hpp:1076
void remove(std::string_view key)
Deletes the value stored under the given key.
Definition fluxen.hpp:915
~DB()=default
Closes the database and releases all file locks.
void put(std::string_view key, const T &value)
Stores a trivially copyable value under the given key.
Definition fluxen.hpp:823
auto compact() -> bool
Rewrites the database file retaining only live entries.
Definition fluxen.hpp:1169
DB(const std::string &path)
Opens or creates a database at the given path.
Definition fluxen.hpp:737
auto get(std::string_view key) const -> std::optional< T >
Retrieves the value stored under the given key.
Definition fluxen.hpp:867
void each(const std::function< void(std::string_view, Bytes)> &fn) const
Iterates over all live key-value pairs.
Definition fluxen.hpp:970
void put(std::string_view key, std::string_view value)
Stores a string value under the given key.
Definition fluxen.hpp:784
A staged batch of write operations, used inside DB::transaction().
Definition fluxen.hpp:587
void put(std::string_view key, std::string_view value)
Stage a string value to be written.
Definition fluxen.hpp:606
void remove(std::string_view key)
Stage a key deletion.
Definition fluxen.hpp:657
void put(std::string_view key, const T &value)
Stage a trivially copyable value to be written.
Definition fluxen.hpp:638
TxResult
Controls whether a transaction's operations are applied or discarded.
Definition fluxen.hpp:129
std::span< const std::byte > Bytes
A non-owning view of raw bytes in the memory-mapped file.
Definition fluxen.hpp:120
Definition fluxen.hpp:138
fluxen's error hierarchy
Definition fluxen.hpp:134
Definition fluxen.hpp:146
Definition fluxen.hpp:142