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)