diff --git a/builds/gnu/test_runner.sh b/builds/gnu/test_runner.sh
index 29e67663e..a36f92fad 100755
--- a/builds/gnu/test_runner.sh
+++ b/builds/gnu/test_runner.sh
@@ -8,13 +8,7 @@
# Define tests and options.
#==============================================================================
-BOOST_UNIT_TEST_OPTIONS=\
- "--log_level=warning "\
- "--show_progress=no "\
- "--detect_memory_leak=0 "\
- "--report_level=no "\
- "--build-info=yes "\
- "--run_tests=*"
+BOOST_UNIT_TEST_OPTIONS="--log_level=warning --show_progress=no --detect_memory_leak=0 --report_level=no --build_info=yes --run_test=*"
# Run tests.
#==============================================================================
diff --git a/builds/msvc/vs2022/libbitcoin-database-test/libbitcoin-database-test.props b/builds/msvc/vs2022/libbitcoin-database-test/libbitcoin-database-test.props
index 99e97d83b..235f7e94b 100644
--- a/builds/msvc/vs2022/libbitcoin-database-test/libbitcoin-database-test.props
+++ b/builds/msvc/vs2022/libbitcoin-database-test/libbitcoin-database-test.props
@@ -48,9 +48,6 @@
static
static
-
- cuda
-
diff --git a/builds/msvc/vs2022/libbitcoin-database-tools/libbitcoin-database-tools.props b/builds/msvc/vs2022/libbitcoin-database-tools/libbitcoin-database-tools.props
index 80ec6f538..61b127550 100644
--- a/builds/msvc/vs2022/libbitcoin-database-tools/libbitcoin-database-tools.props
+++ b/builds/msvc/vs2022/libbitcoin-database-tools/libbitcoin-database-tools.props
@@ -45,9 +45,6 @@
static
static
-
- cuda
-
diff --git a/builds/msvc/vs2022/libbitcoin-database/libbitcoin-database.props b/builds/msvc/vs2022/libbitcoin-database/libbitcoin-database.props
index a546a5581..18e49f789 100644
--- a/builds/msvc/vs2022/libbitcoin-database/libbitcoin-database.props
+++ b/builds/msvc/vs2022/libbitcoin-database/libbitcoin-database.props
@@ -44,9 +44,6 @@
static
static
-
- cuda
-
diff --git a/builds/msvc/vs2026/libbitcoin-database-test/libbitcoin-database-test.props b/builds/msvc/vs2026/libbitcoin-database-test/libbitcoin-database-test.props
index 99e97d83b..235f7e94b 100644
--- a/builds/msvc/vs2026/libbitcoin-database-test/libbitcoin-database-test.props
+++ b/builds/msvc/vs2026/libbitcoin-database-test/libbitcoin-database-test.props
@@ -48,9 +48,6 @@
static
static
-
- cuda
-
diff --git a/builds/msvc/vs2026/libbitcoin-database-test/libbitcoin-database-test.vcxproj b/builds/msvc/vs2026/libbitcoin-database-test/libbitcoin-database-test.vcxproj
index e85df954e..df7e5c2f1 100644
--- a/builds/msvc/vs2026/libbitcoin-database-test/libbitcoin-database-test.vcxproj
+++ b/builds/msvc/vs2026/libbitcoin-database-test/libbitcoin-database-test.vcxproj
@@ -270,7 +270,7 @@
-
+
@@ -286,7 +286,7 @@
-
+
diff --git a/builds/msvc/vs2026/libbitcoin-database-test/packages.config b/builds/msvc/vs2026/libbitcoin-database-test/packages.config
index dbcb8453c..13c710721 100644
--- a/builds/msvc/vs2026/libbitcoin-database-test/packages.config
+++ b/builds/msvc/vs2026/libbitcoin-database-test/packages.config
@@ -6,7 +6,7 @@
|
-->
-
+
diff --git a/builds/msvc/vs2026/libbitcoin-database-tools/libbitcoin-database-tools.props b/builds/msvc/vs2026/libbitcoin-database-tools/libbitcoin-database-tools.props
index 80ec6f538..61b127550 100644
--- a/builds/msvc/vs2026/libbitcoin-database-tools/libbitcoin-database-tools.props
+++ b/builds/msvc/vs2026/libbitcoin-database-tools/libbitcoin-database-tools.props
@@ -45,9 +45,6 @@
static
static
-
- cuda
-
diff --git a/builds/msvc/vs2026/libbitcoin-database-tools/libbitcoin-database-tools.vcxproj b/builds/msvc/vs2026/libbitcoin-database-tools/libbitcoin-database-tools.vcxproj
index 741e2daef..94de93344 100644
--- a/builds/msvc/vs2026/libbitcoin-database-tools/libbitcoin-database-tools.vcxproj
+++ b/builds/msvc/vs2026/libbitcoin-database-tools/libbitcoin-database-tools.vcxproj
@@ -139,7 +139,7 @@
-
+
@@ -154,7 +154,7 @@
-
+
diff --git a/builds/msvc/vs2026/libbitcoin-database-tools/packages.config b/builds/msvc/vs2026/libbitcoin-database-tools/packages.config
index 4efbc2028..4a7740eb3 100644
--- a/builds/msvc/vs2026/libbitcoin-database-tools/packages.config
+++ b/builds/msvc/vs2026/libbitcoin-database-tools/packages.config
@@ -6,7 +6,7 @@
|
-->
-
+
diff --git a/builds/msvc/vs2026/libbitcoin-database/libbitcoin-database.props b/builds/msvc/vs2026/libbitcoin-database/libbitcoin-database.props
index a546a5581..18e49f789 100644
--- a/builds/msvc/vs2026/libbitcoin-database/libbitcoin-database.props
+++ b/builds/msvc/vs2026/libbitcoin-database/libbitcoin-database.props
@@ -44,9 +44,6 @@
static
static
-
- cuda
-
diff --git a/builds/msvc/vs2026/libbitcoin-database/libbitcoin-database.vcxproj b/builds/msvc/vs2026/libbitcoin-database/libbitcoin-database.vcxproj
index 402da38c1..a44a1e8e6 100644
--- a/builds/msvc/vs2026/libbitcoin-database/libbitcoin-database.vcxproj
+++ b/builds/msvc/vs2026/libbitcoin-database/libbitcoin-database.vcxproj
@@ -309,7 +309,7 @@
-
+
@@ -324,7 +324,7 @@
-
+
diff --git a/builds/msvc/vs2026/libbitcoin-database/packages.config b/builds/msvc/vs2026/libbitcoin-database/packages.config
index 4efbc2028..4a7740eb3 100644
--- a/builds/msvc/vs2026/libbitcoin-database/packages.config
+++ b/builds/msvc/vs2026/libbitcoin-database/packages.config
@@ -6,7 +6,7 @@
|
-->
-
+
diff --git a/include/bitcoin/database/impl/memory/mmap.ipp b/include/bitcoin/database/impl/memory/mmap.ipp
index 3103a841d..0867f13fd 100644
--- a/include/bitcoin/database/impl/memory/mmap.ipp
+++ b/include/bitcoin/database/impl/memory/mmap.ipp
@@ -59,9 +59,9 @@ CLASS::mmap(const paths& filenames, size_t minimum, size_t expansion,
TEMPLATE
CLASS::~mmap() NOEXCEPT
{
- BC_ASSERT(!loaded_);
- BC_ASSERT(is_zero(logical_));
- BC_ASSERT(is_zero(capacity_));
+ BC_ASSERT(!loaded_.load());
+ BC_ASSERT(is_zero(logical_.load()));
+ BC_ASSERT(is_zero(capacity_.load()));
BC_ASSERT(std::ranges::all_of(memory_map_,
[](auto map) NOEXCEPT { return is_null(map); }));
BC_ASSERT(std::ranges::all_of(opened_,
@@ -78,8 +78,7 @@ bool CLASS::is_open() const NOEXCEPT
TEMPLATE
bool CLASS::is_loaded() const NOEXCEPT
{
- std::shared_lock field_lock(field_mutex_);
- return loaded_;
+ return loaded_.load();
}
// protected
@@ -94,14 +93,13 @@ size_t CLASS::to_capacity(size_t required) const NOEXCEPT
return std::max(minimum_, ceilinged_add(required, growth));
}
-// Read-write protected by atomic, write-write protected by remap_mutex.
+// Write-write protected by remap_mutex.
TEMPLATE
void CLASS::set_first_code(const error::error_t& ec) NOEXCEPT
{
- if (!fault_)
+ if (!fault_.load())
{
- // fault is not exposed so requires no atomic (fast read).
- fault_ = true;
+ fault_.store(true);
// error is atomic for public read exposure.
error_.store(ec);
diff --git a/include/bitcoin/database/impl/memory/mmap_dispatch.ipp b/include/bitcoin/database/impl/memory/mmap_dispatch.ipp
index e93f603bb..13a836065 100644
--- a/include/bitcoin/database/impl/memory/mmap_dispatch.ipp
+++ b/include/bitcoin/database/impl/memory/mmap_dispatch.ipp
@@ -41,11 +41,12 @@ memory CLASS::get_filled(size_t offset, size_t size,
{
std::unique_lock field_lock(field_mutex_);
- if (fault_ || !loaded_ || system::is_add_overflow(offset, size))
+ if (fault_.load() || !loaded_.load() ||
+ system::is_add_overflow(offset, size))
return {};
- const auto end = std::max(logical_, offset + size);
- if (end > capacity_)
+ const auto end = std::max(logical_.load(), offset + size);
+ if (end > capacity_.load())
{
const auto extended = to_capacity(end);
@@ -58,13 +59,16 @@ memory CLASS::get_filled(size_t offset, size_t size,
// Fill new capacity as offset may not be at end due to expansion.
auto data = memory_map_.front();
- const auto logical = to_width(logical_);
- const auto capacity = to_width(capacity_);
+ const auto logical = to_width(logical_.load());
+ const auto capacity = to_width(capacity_.load());
const auto start = std::next(data, logical);
std::fill_n(start, capacity - logical, backfill);
}
- logical_ = end;
+ // Raise to at least end (concurrent claims may already exceed it).
+ for (auto current = logical_.load(); current < end;)
+ if (logical_.compare_exchange_weak(current, end))
+ break;
}
return get(offset);
@@ -77,7 +81,7 @@ memory CLASS::get_capacity(size_t offset) const NOEXCEPT
memory out{ remap_mutex_ };
- if (!loaded_)
+ if (!loaded_.load())
return {};
auto data = memory_map_.front();
@@ -114,14 +118,14 @@ memory CLASS::get_at(size_t column, size_t offset) const NOEXCEPT
if (column >= columns)
return {};
- // Obtaining size before access prevents mutual mutex wait (deadlock).
+ // size() is a lock-free atomic read.
const auto allocated = size() * widths.at(column);
// Takes a shared lock on remap_mutex_ until destruct, blocking remap.
memory out{ remap_mutex_ };
- // loaded_ update is precluded by above lock, making this read atomic.
- if (!loaded_)
+ // loaded_ update is precluded by above lock.
+ if (!loaded_.load())
return {};
// With offset > size the assignment is negative (stream is exhausted).
diff --git a/include/bitcoin/database/impl/memory/mmap_private.ipp b/include/bitcoin/database/impl/memory/mmap_private.ipp
index 7d22c2bdf..8cf252c94 100644
--- a/include/bitcoin/database/impl/memory/mmap_private.ipp
+++ b/include/bitcoin/database/impl/memory/mmap_private.ipp
@@ -45,11 +45,11 @@ bool CLASS::map_all_(std::index_sequence) NOEXCEPT
{
if (!(map_() && ...))
{
- capacity_ = zero;
+ capacity_.store(zero);
return false;
}
- capacity_ = std::max(logical_, minimum_);
+ capacity_.store(std::max(logical_.load(), minimum_));
return true;
}
@@ -57,8 +57,9 @@ TEMPLATE
template
bool CLASS::unmap_all_(std::index_sequence) NOEXCEPT
{
- const auto success = (unmap_(capacity_) && ...);
- capacity_ = zero;
+ const auto capacity = capacity_.load();
+ const auto success = (unmap_(capacity) && ...);
+ capacity_.store(zero);
return success;
}
@@ -68,11 +69,11 @@ bool CLASS::remap_all_(size_t capacity, std::index_sequence) NOEXCEPT
{
if (!(remap_(capacity) && ...))
{
- capacity_ = zero;
+ capacity_.store(zero);
return false;
}
- capacity_ = capacity;
+ capacity_.store(capacity);
return true;
}
@@ -89,7 +90,7 @@ bool CLASS::flush_() NOEXCEPT
// unmap (and therefore msync) must be called before ftruncate.
// "To flush all the dirty pages plus the metadata for the file and ensure
// that they are physically written to disk..."
- const auto size = to_width(logical_);
+ const auto size = to_width(logical_.load());
const auto success =
(::msync(memory_map_[Column], size, MS_SYNC) != fail)
&& (::fsync(opened_[Column]) != fail);
@@ -126,7 +127,9 @@ bool CLASS::release_(size_t size) NOEXCEPT
if (!success)
set_first_code(error::munmap_failure);
- loaded_ = false;
+ // loaded_ is caller-owned: unmap_ publishes unloaded, remap_ remains
+ // loaded across replacement (lock-free allocate guards must not observe
+ // a transient unload).
memory_map_[Column] = {};
return success;
}
@@ -136,7 +139,7 @@ TEMPLATE
template
bool CLASS::unmap_(size_t size) NOEXCEPT
{
- const auto logical = to_width(logical_);
+ const auto logical = to_width(logical_.load());
#if defined(HAVE_MSC)
// Windows cannot resize a mapped file.
@@ -162,6 +165,8 @@ bool CLASS::unmap_(size_t size) NOEXCEPT
const auto success = release_(size) && truncated;
#endif
+ loaded_.store(false);
+
if (!success)
set_first_code(error::munmap_failure);
@@ -174,7 +179,7 @@ TEMPLATE
template
bool CLASS::map_() NOEXCEPT
{
- auto size = logical_;
+ auto size = logical_.load();
// Cannot map empty file, and want minimum capacity, so expand as required.
// disk_full: space is set but no code is set with false return.
@@ -194,7 +199,7 @@ TEMPLATE
template
bool CLASS::remap_(size_t size) NOEXCEPT
{
- BC_ASSERT(size >= logical_);
+ BC_ASSERT(size >= logical_.load());
// Cannot remap empty file, so expand to minimum capacity if zero.
if (is_zero(size))
@@ -204,7 +209,7 @@ bool CLASS::remap_(size_t size) NOEXCEPT
// macOS cannot remap in place, so release the mapping without trimming and
// the file remains at capacity_ bytes and resize_'s fallocate delta (and
// the fallocate shim's preallocation window) are exact by construction.
- if (!release_(capacity_))
+ if (!release_(capacity_.load()))
return false;
if (!resize_(size))
@@ -221,19 +226,20 @@ bool CLASS::remap_(size_t size) NOEXCEPT
// mman-win32 mremap hack (umap/map) requires flags and file descriptor.
memory_map_[Column] = system::pointer_cast(
- ::mremap_(memory_map_[Column], to_width(capacity_),
+ ::mremap_(memory_map_[Column], to_width(capacity_.load()),
to_width(size), PROT_READ | PROT_WRITE, MAP_SHARED,
opened_[Column]));
#elif defined(MREMAP_MAYMOVE)
memory_map_[Column] = system::pointer_cast(
- ::mremap(memory_map_[Column], to_width(capacity_),
+ ::mremap(memory_map_[Column], to_width(capacity_.load()),
to_width(size), MREMAP_MAYMOVE));
#else
- // macOS does not define mremap or MREMAP_MAYMOVE.
+ // macOS does not define mremap or MREMAP_MAYMOVE. The prior mapping was
+ // released above and the resized file is mapped fresh.
// TODO: see "MREMAP_MAYMOVE" in sqlite for map extension technique.
memory_map_[Column] = system::pointer_cast(
::mmap(nullptr, to_width(size), PROT_READ | PROT_WRITE,
@@ -250,7 +256,7 @@ template
bool CLASS::resize_(size_t size) NOEXCEPT
{
const auto target = to_width(size);
- const auto capacity = to_width(capacity_);
+ const auto capacity = to_width(capacity_.load());
// Disk full detection, any other failure is an abort.
#if !defined(WITHOUT_FALLOCATE)
@@ -263,12 +269,12 @@ bool CLASS::resize_(size_t size) NOEXCEPT
if (errno == ENOSPC)
{
set_disk_space(system::ceilinged_multiply(system::floored_subtract(
- size, capacity_), stride));
+ size, capacity_.load()), stride));
return false;
}
set_first_code(error::ftruncate_failure);
- unmap_(capacity_);
+ unmap_(capacity_.load());
return false;
}
@@ -286,7 +292,7 @@ bool CLASS::finalize_(size_t
{
if (memory_map_[Column] == MAP_FAILED)
{
- loaded_ = false;
+ loaded_.store(false);
memory_map_[Column] = {};
// mmap or mremap failure (not mapped).
@@ -332,7 +338,7 @@ bool CLASS::finalize_(size_t
}
#endif // !HAVE_MSC && !WITHOUT_MADVISE
- loaded_ = true;
+ loaded_.store(true);
return true;
}
diff --git a/include/bitcoin/database/impl/memory/mmap_storage.ipp b/include/bitcoin/database/impl/memory/mmap_storage.ipp
index 8ab5ee395..65c165fa6 100644
--- a/include/bitcoin/database/impl/memory/mmap_storage.ipp
+++ b/include/bitcoin/database/impl/memory/mmap_storage.ipp
@@ -84,7 +84,7 @@ code CLASS::open() NOEXCEPT
if (const auto ec = file::size_ex(bytes, opened_.front()))
return ec;
- logical_ = logical_rows(bytes);
+ logical_.store(logical_rows(bytes));
return error::success;
}
@@ -94,13 +94,13 @@ code CLASS::close() NOEXCEPT
std::unique_lock map_lock(remap_mutex_);
std::unique_lock field_lock(field_mutex_);
- if (loaded_)
+ if (loaded_.load())
return error::close_loaded;
if (opened_.front() == file::invalid)
return error::success;
- logical_ = zero;
+ logical_.store(zero);
for (auto& descriptor: opened_)
{
if (descriptor != file::invalid)
@@ -128,7 +128,7 @@ code CLASS::load() NOEXCEPT
if (remap_mutex_.try_lock())
{
- if (loaded_)
+ if (loaded_.load())
{
remap_mutex_.unlock();
return error::load_loaded;
@@ -156,7 +156,7 @@ code CLASS::reload() NOEXCEPT
if (remap_mutex_.try_lock())
{
- if (!loaded_)
+ if (!loaded_.load())
{
remap_mutex_.unlock();
return error::reload_unloaded;
@@ -178,9 +178,8 @@ code CLASS::flush() NOEXCEPT
{
// Prevent unload, resize, remap.
std::shared_lock map_lock(remap_mutex_);
- std::shared_lock field_lock(field_mutex_);
- if (!loaded_)
+ if (!loaded_.load())
return error::flush_unloaded;
// Reads fields and the memory map.
@@ -195,7 +194,7 @@ code CLASS::unload() NOEXCEPT
if (remap_mutex_.try_lock())
{
- if (!loaded_)
+ if (!loaded_.load())
{
remap_mutex_.unlock();
return error::success;
@@ -223,7 +222,7 @@ code CLASS::shrink() NOEXCEPT
if (remap_mutex_.try_lock())
{
- if (!loaded_)
+ if (!loaded_.load())
{
remap_mutex_.unlock();
return error::shrink_unloaded;
@@ -267,15 +266,13 @@ code CLASS::dump(const std::filesystem::path& path) const NOEXCEPT
TEMPLATE
size_t CLASS::size() const NOEXCEPT
{
- std::shared_lock field_lock(field_mutex_);
- return logical_;
+ return logical_.load();
}
TEMPLATE
size_t CLASS::capacity() const NOEXCEPT
{
- std::shared_lock field_lock(field_mutex_);
- return capacity_;
+ return capacity_.load();
}
TEMPLATE
@@ -283,10 +280,10 @@ bool CLASS::truncate(size_t count) NOEXCEPT
{
std::unique_lock field_lock(field_mutex_);
- if (count > logical_)
+ if (count > logical_.load())
return false;
- logical_ = count;
+ logical_.store(count);
return true;
}
@@ -295,13 +292,13 @@ bool CLASS::expand(size_t count) NOEXCEPT
{
std::unique_lock field_lock(field_mutex_);
- if (fault_ || !loaded_)
+ if (fault_.load() || !loaded_.load())
return false;
- if (count <= logical_)
+ if (count <= logical_.load())
return true;
- if (count > capacity_)
+ if (count > capacity_.load())
{
const auto extended = to_capacity(count);
std::unique_lock remap_lock(remap_mutex_);
@@ -309,7 +306,11 @@ bool CLASS::expand(size_t count) NOEXCEPT
return false;
}
- logical_ = count;
+ // Raise to at least count (concurrent claims may already exceed it).
+ for (auto current = logical_.load(); current < count;)
+ if (logical_.compare_exchange_weak(current, count))
+ break;
+
return true;
}
@@ -318,11 +319,12 @@ bool CLASS::reserve(size_t count) NOEXCEPT
{
std::unique_lock field_lock(field_mutex_);
- if (fault_ || !loaded_ || system::is_add_overflow(logical_, count))
+ if (fault_.load() || !loaded_.load() ||
+ system::is_add_overflow(logical_.load(), count))
return false;
- const auto end = logical_ + count;
- if (end > capacity_)
+ const auto end = logical_.load() + count;
+ if (end > capacity_.load())
{
const auto extended = to_capacity(end);
std::unique_lock remap_lock(remap_mutex_);
@@ -334,20 +336,47 @@ bool CLASS::reserve(size_t count) NOEXCEPT
return true;
}
-// Waits until all access pointers are destructed. Will deadlock if any access
-// pointer is waiting on allocation. Lock safety requires that access pointers
-// are short-lived and do not block on allocation.
+// The slow path waits until all access pointers are destructed. Will deadlock
+// if any access pointer is waiting on allocation. Lock safety requires that
+// access pointers are short-lived and do not block on allocation. Relies on
+// the suspend-writes contract of shrinking transitions (truncate/unload/
+// shrink/close), as the fast path does not serialize against them.
TEMPLATE
size_t CLASS::allocate(size_t count) NOEXCEPT
{
- std::unique_lock field_lock(field_mutex_);
+ // Fast path: claim rows within published capacity (no locks). A failed
+ // exchange implies another claim succeeded, so every retry is progress.
+ for (auto start = logical_.load();;)
+ {
+ if (fault_.load() || !loaded_.load() ||
+ system::is_add_overflow(start, count))
+ return storage::eof;
- if (fault_ || !loaded_ || system::is_add_overflow(logical_, count))
- return storage::eof;
+ if ((start + count) > capacity_.load())
+ break;
- auto end = logical_ + count;
- if (end > capacity_)
+ if (logical_.compare_exchange_weak(start, start + count))
+ return start;
+ }
+
+ // Slow path: serialize capacity growth (at most one grower). Fast paths
+ // continue claiming under the old capacity; the growth target covers them.
+ std::unique_lock field_lock(field_mutex_);
+ for (auto start = logical_.load();;)
{
+ if (fault_.load() || !loaded_.load() ||
+ system::is_add_overflow(start, count))
+ return storage::eof;
+
+ const auto end = start + count;
+ if (end <= capacity_.load())
+ {
+ if (logical_.compare_exchange_weak(start, end))
+ return start;
+
+ continue;
+ }
+
const auto extended = to_capacity(end);
// TODO: Could loop over a try lock here and log deadlock warning.
@@ -357,9 +386,6 @@ size_t CLASS::allocate(size_t count) NOEXCEPT
if (!remap_all_(extended, sequence{}))
return storage::eof;
}
-
- std::swap(logical_, end);
- return end;
}
} // namespace database
diff --git a/include/bitcoin/database/memory/mmap.hpp b/include/bitcoin/database/memory/mmap.hpp
index b4514c087..33e4910f0 100644
--- a/include/bitcoin/database/memory/mmap.hpp
+++ b/include/bitcoin/database/memory/mmap.hpp
@@ -212,14 +212,18 @@ class mmap
std::atomic space_{ zero };
std::atomic error_{ error::success };
- // These are protected by field_mutex_.
- // Fields require field_mutex_ exclusive for write, shared for flush/read.
- // logical_ and capacity_ are row counts (byte cound if width is one).
+ // Scalar fields are atomic: size/capacity reads and the allocate fast
+ // path (bounded claim within published capacity) are lock-free. Capacity
+ // growth and compound transitions (open/close/load/unload/shrink/
+ // truncate/expand/reserve/get_filled) are serialized by field_mutex_
+ // exclusive. Shrinking transitions additionally rely on the documented
+ // suspend-writes contract (the fast path does not serialize with them).
+ // logical_ and capacity_ are row counts (byte count if width is one).
std::array opened_;
- size_t capacity_{};
- size_t logical_{};
- bool fault_{};
- bool loaded_{};
+ std::atomic capacity_{};
+ std::atomic logical_{};
+ std::atomic fault_{};
+ std::atomic loaded_{};
mutable std::shared_mutex field_mutex_{};
// These are protected by remap_mutex_.
diff --git a/src/memory/mman.cpp b/src/memory/mman.cpp
index 88b7697c2..46f76c836 100644
--- a/src/memory/mman.cpp
+++ b/src/memory/mman.cpp
@@ -308,18 +308,20 @@ int fallocate(int fd, int, off_t offset, off_t len) NOEXCEPT
};
// Try contiguous allocation.
- auto result = ::fcntl(fd, F_PREALLOCATE, &store);
-
- // Fallback to non-contiguous.
- if (result == fail)
+ if (::fcntl(fd, F_PREALLOCATE, &store) == fail)
{
+ // Fallback to non-contiguous.
store.fst_flags = F_ALLOCATEALL;
- result = ::fcntl(fd, F_PREALLOCATE, &store);
+ store.fst_bytesalloc = 0;
+
+ // APFS spuriously fails F_PREALLOCATE (e.g. when logical EOF lies
+ // within blocks preallocated by a prior call), reporting ENOSPC
+ // despite ample space. Preallocation is therefore best-effort and
+ // failure is ignored: ftruncate is authoritative, and reports genuine
+ // exhaustion as its own ENOSPC (via sparse extension at worst).
+ ::fcntl(fd, F_PREALLOCATE, &store);
}
- if (result == fail)
- return fail;
-
// Extend file to new size (required for mmap). This is not required on
// Linux because fallocate(2) automatically extends file's logical size.
return ::ftruncate(fd, offset + len);
diff --git a/test/memory/mmap.cpp b/test/memory/mmap.cpp
index f5838dac6..40d398799 100644
--- a/test/memory/mmap.cpp
+++ b/test/memory/mmap.cpp
@@ -18,6 +18,8 @@
*/
#include "../test.hpp"
+#include
+
// TODO: need to fake map_(), unmap_() and flush_() in order to hit
// error::load_failure, error::flush_failure, error::unload_failure codes, but
// don't want to make this class virtual.
@@ -890,4 +892,87 @@ BOOST_AUTO_TEST_CASE(mmap__allocate__aggregate_remap__expected_geometry)
BOOST_REQUIRE(!instance.get_fault());
}
+// Concurrency stress: allocations must be unique, dense claims under
+// contention, with growth forced through the remap slow path.
+BOOST_AUTO_TEST_CASE(mmap__allocate__concurrent__unique_dense_claims)
+{
+ constexpr size_t threads = 8;
+ constexpr size_t rounds = 10'000;
+ using claim = std::pair;
+
+ const std::string file = TEST_PATH;
+ BOOST_REQUIRE(test::create(file));
+
+ // Tiny minimum and modest expansion force frequent slow-path remaps.
+ map instance(file, 1, 25);
+ BOOST_REQUIRE(!instance.open());
+ BOOST_REQUIRE(!instance.load());
+
+ std::atomic_bool failed{};
+ std::array, threads> claims{};
+ std::vector pool{};
+
+ for (size_t id = 0; id < threads; ++id)
+ {
+ pool.emplace_back([&, id]() NOEXCEPT
+ {
+ // Vary claim size by thread (1..3 bytes).
+ const auto count = add1(id % 3);
+ auto& mine = claims.at(id);
+ mine.reserve(rounds);
+
+ for (size_t round = 0; round < rounds; ++round)
+ {
+ const auto start = instance.allocate(count);
+ if (start == storage::eof)
+ {
+ failed.store(true);
+ return;
+ }
+
+ // Write the claim through an accessor (rides remap guard).
+ auto memory = instance.get(start);
+ if (!memory)
+ {
+ failed.store(true);
+ return;
+ }
+
+ std::fill_n(memory.begin(), count, system::narrow_cast(id));
+ mine.push_back({ start, count });
+ }
+ });
+ }
+
+ for (auto& thread: pool)
+ thread.join();
+
+ BOOST_REQUIRE(!failed.load());
+
+ // Aggregate and sort all claims by start.
+ std::vector all{};
+ all.reserve(threads * rounds);
+ for (const auto& mine: claims)
+ all.insert(all.end(), mine.begin(), mine.end());
+
+ std::sort(all.begin(), all.end());
+
+ // Claims are unique, non-overlapping and dense from zero to size().
+ size_t expected{};
+ auto contiguous = true;
+ for (const auto& [start, count]: all)
+ {
+ contiguous &= (start == expected);
+ expected = start + count;
+ }
+
+ BOOST_REQUIRE(contiguous);
+ BOOST_REQUIRE_EQUAL(expected, instance.size());
+ BOOST_REQUIRE_GE(instance.capacity(), instance.size());
+
+ BOOST_REQUIRE(!instance.unload());
+ BOOST_REQUIRE(!instance.close());
+ BOOST_REQUIRE(!instance.get_fault());
+}
+
BOOST_AUTO_TEST_SUITE_END()
diff --git a/test/query/batch/ecdsa.cpp b/test/query/batch/ecdsa.cpp
index bd176d43a..f9cb017ef 100644
--- a/test/query/batch/ecdsa.cpp
+++ b/test/query/batch/ecdsa.cpp
@@ -48,8 +48,7 @@ static bool set_one(test::query_accessor& query, const hash_digest& digest,
const header_link& link) NOEXCEPT
{
chain::ecdsa_signatures sigs{};
- return sigs.append(digest, { &key, one }, { &sig, one }) &&
- query.set_signatures(sigs, link);
+ return sigs.append(digest, key, sig) && query.set_signatures(sigs, link);
}
BOOST_AUTO_TEST_CASE(query_batch_ecdsa__verify_ecdsa_signatures__empty__empty)