From def7a91200accc0e9734f10f75362ba52751ddcd Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Fri, 7 Aug 2026 12:19:05 -0400 Subject: [PATCH 01/11] Iterate refused growth toward necessity. --- .../database/impl/memory/mmap_private.ipp | 16 +++++++--- .../database/impl/memory/mmap_staging.ipp | 11 +++++-- .../database/impl/memory/mmap_storage.ipp | 32 +++++++++++++++---- include/bitcoin/database/memory/mmap.hpp | 9 +++--- 4 files changed, 50 insertions(+), 18 deletions(-) diff --git a/include/bitcoin/database/impl/memory/mmap_private.ipp b/include/bitcoin/database/impl/memory/mmap_private.ipp index 3fbe4fd24..9d49a7a0b 100644 --- a/include/bitcoin/database/impl/memory/mmap_private.ipp +++ b/include/bitcoin/database/impl/memory/mmap_private.ipp @@ -107,11 +107,17 @@ bool CLASS::unmap_all_(std::index_sequence) NOEXCEPT TEMPLATE template -bool CLASS::remap_all_(size_t capacity, std::index_sequence) NOEXCEPT +bool CLASS::remap_all_(size_t capacity, std::index_sequence, + bool final) NOEXCEPT { - if (!(remap_(capacity) && ...)) + if (!(remap_(capacity, final) && ...)) { - capacity_.store(zero); + // A non-final refusal leaves the maps and capacity intact for the + // caller's reduced retry (columns already committed larger by the + // refused attempt harmlessly retain their surplus commitment). + if (final) + capacity_.store(zero); + return false; } @@ -276,7 +282,7 @@ bool CLASS::map_() NOEXCEPT // Remapping has no effect on logical size, sets map_/capacity_. TEMPLATE template -bool CLASS::remap_(size_t size) NOEXCEPT +bool CLASS::remap_(size_t size, bool STAGING_ONLY(final)) NOEXCEPT { BC_ASSERT(size >= logical_.load()); @@ -291,7 +297,7 @@ bool CLASS::remap_(size_t size) NOEXCEPT if (!resize_(size)) return false; - return commit_(size); + return commit_(size, final); #else if (!resize_(size)) return false; diff --git a/include/bitcoin/database/impl/memory/mmap_staging.ipp b/include/bitcoin/database/impl/memory/mmap_staging.ipp index 3909ef6a9..752376543 100644 --- a/include/bitcoin/database/impl/memory/mmap_staging.ipp +++ b/include/bitcoin/database/impl/memory/mmap_staging.ipp @@ -440,13 +440,16 @@ bool CLASS::stage_() NOEXCEPT return true; } -// Commit failure results in unmapped. +// Commit failure results in unmapped when final (the default); a non-final +// in-reservation commit refusal returns false untouched, so the caller may +// iterate a reduced request (commit is a kernel admission decision evaluated +// per request, so a refused amortization step does not imply exhaustion). // Growth within the reservation commits pages in place (stable map base); an // exhausted reservation is replaced and its unsettled content copied, under // the exclusive remap lock held by the caller. TEMPLATE template -bool CLASS::commit_(size_t size) NOEXCEPT +bool CLASS::commit_(size_t size, bool final) NOEXCEPT { const auto target = to_width(size); @@ -478,7 +481,9 @@ bool CLASS::commit_(size_t size) NOEXCEPT if ((target > from) && (mmap_commit(std::next(memory_map_[Column], from), target - from) == fail)) { - teardown_(error::mmap_failure); + if (final) + teardown_(error::mmap_failure); + return false; } diff --git a/include/bitcoin/database/impl/memory/mmap_storage.ipp b/include/bitcoin/database/impl/memory/mmap_storage.ipp index dfbee63c8..0791f130e 100644 --- a/include/bitcoin/database/impl/memory/mmap_storage.ipp +++ b/include/bitcoin/database/impl/memory/mmap_storage.ipp @@ -475,11 +475,24 @@ bool CLASS::expand(size_t count) NOEXCEPT if (count > capacity_.load()) { - const auto extended = to_growth(count); std::unique_lock remap_lock(remap_mutex_); - if (!remap_all_(extended, sequence{})) - return false; + // Commitment is a kernel admission decision (a charge against + // ram+swap) evaluated per request, so a large amortization step can + // be refused while the necessity is easily backed (settle returns + // charge behind the writer continuously). Iterate: halve the refused + // surplus toward the necessity, and only a final refusal of the + // necessity itself is genuine exhaustion (which tears down). Disk + // full exits the iteration (resize_ refusals do not reduce away). + using namespace system; + for (auto extended = to_growth(count); + !remap_all_(extended, sequence{}, extended <= count); + extended = ceilinged_add(count, + to_half(floored_subtract(extended, count)))) + { + if ((extended <= count) || !is_zero(space_.load())) + return false; + } } // Raise to at least count (concurrent claims may already exceed it). @@ -503,11 +516,18 @@ bool CLASS::reserve(size_t count) NOEXCEPT const auto end = logical_.load() + count; if (end > capacity_.load()) { - const auto extended = to_growth(end); std::unique_lock remap_lock(remap_mutex_); - if (!remap_all_(extended, sequence{})) - return false; + // Iterated commitment (see expand): reduce a refused amortization + // step toward the necessity before treating refusal as exhaustion. + for (auto extended = to_growth(end); + !remap_all_(extended, sequence{}, extended <= end); + extended = ceilinged_add(end, + to_half(floored_subtract(extended, end)))) + { + if ((extended <= end) || !is_zero(space_.load())) + return false; + } } // Same as allocate except logical does not change. diff --git a/include/bitcoin/database/memory/mmap.hpp b/include/bitcoin/database/memory/mmap.hpp index 6dd05f156..ba4befd97 100644 --- a/include/bitcoin/database/memory/mmap.hpp +++ b/include/bitcoin/database/memory/mmap.hpp @@ -1,4 +1,4 @@ -/** +/** * Copyright (c) 2011-2026 libbitcoin developers * * This file is part of libbitcoin. @@ -280,7 +280,8 @@ class mmap template bool unmap_all_(std::index_sequence) NOEXCEPT; template - bool remap_all_(size_t capacity, std::index_sequence) NOEXCEPT; + bool remap_all_(size_t capacity, std::index_sequence, + bool final=true) NOEXCEPT; // mman wrappers, not thread safe. template @@ -292,7 +293,7 @@ class mmap template bool unmap_(size_t size) NOEXCEPT; template - bool remap_(size_t size) NOEXCEPT; + bool remap_(size_t size, bool final=true) NOEXCEPT; template bool resize_(size_t size) NOEXCEPT; template @@ -312,7 +313,7 @@ class mmap template bool stage_() NOEXCEPT; template - bool commit_(size_t size) NOEXCEPT; + bool commit_(size_t size, bool final=true) NOEXCEPT; template bool settle_(size_t from, size_t to) NOEXCEPT; template From c7b017451560f5716ebbf630e92923aebda6f310 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Fri, 7 Aug 2026 12:19:05 -0400 Subject: [PATCH 02/11] Treat refused necessity as disk full, not fault. --- .../database/impl/memory/mmap_storage.ipp | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/include/bitcoin/database/impl/memory/mmap_storage.ipp b/include/bitcoin/database/impl/memory/mmap_storage.ipp index 0791f130e..05308484b 100644 --- a/include/bitcoin/database/impl/memory/mmap_storage.ipp +++ b/include/bitcoin/database/impl/memory/mmap_storage.ipp @@ -481,17 +481,26 @@ bool CLASS::expand(size_t count) NOEXCEPT // ram+swap) evaluated per request, so a large amortization step can // be refused while the necessity is easily backed (settle returns // charge behind the writer continuously). Iterate: halve the refused - // surplus toward the necessity, and only a final refusal of the - // necessity itself is genuine exhaustion (which tears down). Disk - // full exits the iteration (resize_ refusals do not reduce away). + // surplus toward the necessity. Refusal of the necessity itself is a + // resource condition, not store damage: treated as the memory analog + // of disk full (space set, store intact, writes fail fast until + // cleared), it clears by settle drainage or operator relief, where + // teardown would convert a transient shortage into a restore. using namespace system; for (auto extended = to_growth(count); - !remap_all_(extended, sequence{}, extended <= count); + !remap_all_(extended, sequence{}, false); extended = ceilinged_add(count, to_half(floored_subtract(extended, count)))) { - if ((extended <= count) || !is_zero(space_.load())) + if (!is_zero(space_.load())) return false; + + if (extended <= count) + { + set_disk_space(ceilinged_multiply( + floored_subtract(count, capacity_.load()), stride)); + return false; + } } } @@ -519,14 +528,22 @@ bool CLASS::reserve(size_t count) NOEXCEPT std::unique_lock remap_lock(remap_mutex_); // Iterated commitment (see expand): reduce a refused amortization - // step toward the necessity before treating refusal as exhaustion. + // step toward the necessity; refusal of the necessity is the memory + // analog of disk full (recoverable), never teardown. for (auto extended = to_growth(end); - !remap_all_(extended, sequence{}, extended <= end); + !remap_all_(extended, sequence{}, false); extended = ceilinged_add(end, to_half(floored_subtract(extended, end)))) { - if ((extended <= end) || !is_zero(space_.load())) + if (!is_zero(space_.load())) return false; + + if (extended <= end) + { + set_disk_space(ceilinged_multiply( + floored_subtract(end, capacity_.load()), stride)); + return false; + } } } From 500ab3271cb1fafa61aadff61a75a6c14e1b6f95 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Fri, 7 Aug 2026 12:19:05 -0400 Subject: [PATCH 03/11] Extend growth descent to disk provisioning. --- .../database/impl/memory/mmap_private.ipp | 23 ++++++++------ .../database/impl/memory/mmap_storage.ipp | 30 ++++++++++--------- include/bitcoin/database/memory/mmap.hpp | 2 +- 3 files changed, 31 insertions(+), 24 deletions(-) diff --git a/include/bitcoin/database/impl/memory/mmap_private.ipp b/include/bitcoin/database/impl/memory/mmap_private.ipp index 9d49a7a0b..f6cf5f734 100644 --- a/include/bitcoin/database/impl/memory/mmap_private.ipp +++ b/include/bitcoin/database/impl/memory/mmap_private.ipp @@ -113,8 +113,8 @@ bool CLASS::remap_all_(size_t capacity, std::index_sequence, if (!(remap_(capacity, final) && ...)) { // A non-final refusal leaves the maps and capacity intact for the - // caller's reduced retry (columns already committed larger by the - // refused attempt harmlessly retain their surplus commitment). + // caller's reduced retry (columns already grown by the refused + // attempt harmlessly retain surplus commitment or provisioning). if (final) capacity_.store(zero); @@ -282,7 +282,7 @@ bool CLASS::map_() NOEXCEPT // Remapping has no effect on logical size, sets map_/capacity_. TEMPLATE template -bool CLASS::remap_(size_t size, bool STAGING_ONLY(final)) NOEXCEPT +bool CLASS::remap_(size_t size, bool final) NOEXCEPT { BC_ASSERT(size >= logical_.load()); @@ -294,12 +294,12 @@ bool CLASS::remap_(size_t size, bool STAGING_ONLY(final)) NOEXCEPT // The file is preallocated to capacity, preserving disk full detection at // allocation, and growth commits reserved anonymous pages in place, so no // mapping is released and the map base is stable within the reservation. - if (!resize_(size)) + if (!resize_(size, final)) return false; return commit_(size, final); #else - if (!resize_(size)) + if (!resize_(size, final)) return false; #if defined(HAVE_MSC) @@ -321,7 +321,7 @@ bool CLASS::remap_(size_t size, bool STAGING_ONLY(final)) NOEXCEPT // disk_full: space is set but no code is set with false return. TEMPLATE template -bool CLASS::resize_(size_t size) NOEXCEPT +bool CLASS::resize_(size_t size, bool final) NOEXCEPT { // The file is provisioned ahead of commitment, so growth within the // provisioned extent requires no disk operation (the space is reserved). @@ -340,11 +340,16 @@ bool CLASS::resize_(size_t size) NOEXCEPT #endif { // Disk full is the only restartable store failure (leave mapped). + // A non-final refusal is not published: the caller retries reduced. if (errno == ENOSPC) { - using namespace system; - set_disk_space(ceilinged_multiply(floored_subtract(size, extent), - stride)); + if (final) + { + using namespace system; + set_disk_space(ceilinged_multiply( + floored_subtract(size, extent), stride)); + } + return false; } diff --git a/include/bitcoin/database/impl/memory/mmap_storage.ipp b/include/bitcoin/database/impl/memory/mmap_storage.ipp index 05308484b..1f956603e 100644 --- a/include/bitcoin/database/impl/memory/mmap_storage.ipp +++ b/include/bitcoin/database/impl/memory/mmap_storage.ipp @@ -477,22 +477,23 @@ bool CLASS::expand(size_t count) NOEXCEPT { std::unique_lock remap_lock(remap_mutex_); - // Commitment is a kernel admission decision (a charge against - // ram+swap) evaluated per request, so a large amortization step can - // be refused while the necessity is easily backed (settle returns - // charge behind the writer continuously). Iterate: halve the refused - // surplus toward the necessity. Refusal of the necessity itself is a - // resource condition, not store damage: treated as the memory analog - // of disk full (space set, store intact, writes fail fast until - // cleared), it clears by settle drainage or operator relief, where - // teardown would convert a transient shortage into a restore. + // Growth asks are amortized (rate surplus over the necessity), and + // admission of an ask is a resource decision evaluated per request: + // the kernel charges a commitment against ram+swap, and the disk + // charges a provisioning extension against free space. Either can + // refuse a large amortization step while the necessity is easily + // backed, so iterate: halve the refused surplus toward the necessity. + // Refusal of the necessity itself is resource exhaustion, not store + // damage: published as disk full (space set, store intact, writes + // fail fast until cleared), it clears by settle drainage or operator + // relief, where teardown would convert a shortage into a restore. using namespace system; for (auto extended = to_growth(count); !remap_all_(extended, sequence{}, false); extended = ceilinged_add(count, to_half(floored_subtract(extended, count)))) { - if (!is_zero(space_.load())) + if (fault_.load()) return false; if (extended <= count) @@ -527,15 +528,16 @@ bool CLASS::reserve(size_t count) NOEXCEPT { std::unique_lock remap_lock(remap_mutex_); - // Iterated commitment (see expand): reduce a refused amortization - // step toward the necessity; refusal of the necessity is the memory - // analog of disk full (recoverable), never teardown. + // Iterated growth (see expand): reduce a refused amortization step + // toward the necessity; refusal of the necessity is exhaustion of + // the refusing resource, published as disk full (recoverable), + // never teardown. for (auto extended = to_growth(end); !remap_all_(extended, sequence{}, false); extended = ceilinged_add(end, to_half(floored_subtract(extended, end)))) { - if (!is_zero(space_.load())) + if (fault_.load()) return false; if (extended <= end) diff --git a/include/bitcoin/database/memory/mmap.hpp b/include/bitcoin/database/memory/mmap.hpp index ba4befd97..f23a44e67 100644 --- a/include/bitcoin/database/memory/mmap.hpp +++ b/include/bitcoin/database/memory/mmap.hpp @@ -295,7 +295,7 @@ class mmap template bool remap_(size_t size, bool final=true) NOEXCEPT; template - bool resize_(size_t size) NOEXCEPT; + bool resize_(size_t size, bool final=true) NOEXCEPT; template bool finalize_(size_t size) NOEXCEPT; From c6dd0980efaf3a652105fd15c78697ec8748b5f1 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Fri, 7 Aug 2026 12:19:05 -0400 Subject: [PATCH 04/11] Add growth admission headroom. --- include/bitcoin/database/impl/memory/mmap.ipp | 2 ++ .../database/impl/memory/mmap_private.ipp | 20 +++++++++++++------ .../database/impl/memory/mmap_staging.ipp | 7 ++++--- .../database/impl/memory/mmap_storage.ipp | 14 +++++++++---- include/bitcoin/database/memory/mmap.hpp | 1 + include/bitcoin/database/memory/mstage.hpp | 2 +- include/bitcoin/database/memory/settings.hpp | 4 ++++ include/bitcoin/database/store.hpp | 3 ++- src/memory/mstage.cpp | 18 +++++++++++++++-- 9 files changed, 54 insertions(+), 17 deletions(-) diff --git a/include/bitcoin/database/impl/memory/mmap.ipp b/include/bitcoin/database/impl/memory/mmap.ipp index 27cdf1b92..5822e1136 100644 --- a/include/bitcoin/database/impl/memory/mmap.ipp +++ b/include/bitcoin/database/impl/memory/mmap.ipp @@ -39,6 +39,7 @@ CLASS::mmap(const path& filename, const storage_settings& settings, : filenames_{ filename }, minimum_(to_rows(settings.size)), expansion_(settings.rate), + headroom_(system::possible_narrow_cast(settings.headroom)), access_(settings.access), random_(random), staged_(staged), @@ -53,6 +54,7 @@ CLASS::mmap(const paths& filenames, const storage_settings& settings, : filenames_(filenames), minimum_(to_rows(settings.size)), expansion_(settings.rate), + headroom_(system::possible_narrow_cast(settings.headroom)), access_(settings.access), random_(random), staged_(staged), diff --git a/include/bitcoin/database/impl/memory/mmap_private.ipp b/include/bitcoin/database/impl/memory/mmap_private.ipp index f6cf5f734..c7b2ea035 100644 --- a/include/bitcoin/database/impl/memory/mmap_private.ipp +++ b/include/bitcoin/database/impl/memory/mmap_private.ipp @@ -329,14 +329,17 @@ bool CLASS::resize_(size_t size, bool final) NOEXCEPT if (size <= extent) return true; + using namespace system; const auto target = to_width(size); const auto capacity = to_width(extent); + const auto probed = ceilinged_add(target, headroom_); - // Disk full detection, any other failure is an abort. + // Disk full detection, any other failure is an abort. The extension is + // probed with the headroom, released upon the grant (truncated back). #if !defined(WITHOUT_FALLOCATE) - if (::fallocate(opened_[Column], 0, capacity, target - capacity) == fail) + if (::fallocate(opened_[Column], 0, capacity, probed - capacity) == fail) #else - if (::ftruncate(opened_[Column], target) == fail) + if (::ftruncate(opened_[Column], probed) == fail) #endif { // Disk full is the only restartable store failure (leave mapped). @@ -344,11 +347,8 @@ bool CLASS::resize_(size_t size, bool final) NOEXCEPT if (errno == ENOSPC) { if (final) - { - using namespace system; set_disk_space(ceilinged_multiply( floored_subtract(size, extent), stride)); - } return false; } @@ -358,6 +358,14 @@ bool CLASS::resize_(size_t size, bool final) NOEXCEPT return false; } + if (!is_zero(headroom_) && + (::ftruncate(opened_[Column], target) == fail)) + { + set_first_code(error::ftruncate_failure); + unmap_(capacity_.load()); + return false; + } + return true; } diff --git a/include/bitcoin/database/impl/memory/mmap_staging.ipp b/include/bitcoin/database/impl/memory/mmap_staging.ipp index 752376543..d76a35f0f 100644 --- a/include/bitcoin/database/impl/memory/mmap_staging.ipp +++ b/include/bitcoin/database/impl/memory/mmap_staging.ipp @@ -397,7 +397,7 @@ bool CLASS::stage_() NOEXCEPT const auto settled = page_floor(to_width(settled_.load())); if ((target > settled) && (mmap_commit(std::next(memory_map_[Column], - settled), target - settled) == fail)) + settled), target - settled, headroom_) == fail)) { teardown_(error::mmap_failure); return false; @@ -479,7 +479,7 @@ bool CLASS::commit_(size_t size, bool final) NOEXCEPT const auto from = std::max(settled, current); if ((target > from) && (mmap_commit(std::next(memory_map_[Column], from), - target - from) == fail)) + target - from, headroom_) == fail)) { if (final) teardown_(error::mmap_failure); @@ -535,7 +535,8 @@ bool CLASS::commit_(size_t size, bool final) NOEXCEPT return true; } - if (mmap_commit(std::next(base, settled), target - settled) == fail) + if (mmap_commit(std::next(base, settled), target - settled, + headroom_) == fail) { ::munmap(replace, reserved); teardown_(error::mmap_failure); diff --git a/include/bitcoin/database/impl/memory/mmap_storage.ipp b/include/bitcoin/database/impl/memory/mmap_storage.ipp index 1f956603e..f98027540 100644 --- a/include/bitcoin/database/impl/memory/mmap_storage.ipp +++ b/include/bitcoin/database/impl/memory/mmap_storage.ipp @@ -483,10 +483,16 @@ bool CLASS::expand(size_t count) NOEXCEPT // charges a provisioning extension against free space. Either can // refuse a large amortization step while the necessity is easily // backed, so iterate: halve the refused surplus toward the necessity. - // Refusal of the necessity itself is resource exhaustion, not store - // damage: published as disk full (space set, store intact, writes - // fail fast until cleared), it clears by settle drainage or operator - // relief, where teardown would convert a shortage into a restore. + // Growth asks are amortized (rate surplus over the necessity), and + // each is admitted only while it leaves the configured headroom of + // the backing resource unclaimed (probed with the ask, released on + // grant), so exhaustion never consumes the system's final bytes. A + // large amortization step can be refused while the necessity fits, + // so iterate: halve the refused surplus toward the necessity. + // Refusal of the necessity is exhaustion, not store damage: + // published as disk full (space set, store intact, writes fail fast + // until cleared), it clears by settle drainage or operator relief, + // where teardown would convert a shortage into a restore. using namespace system; for (auto extended = to_growth(count); !remap_all_(extended, sequence{}, false); diff --git a/include/bitcoin/database/memory/mmap.hpp b/include/bitcoin/database/memory/mmap.hpp index f23a44e67..2001cf1f3 100644 --- a/include/bitcoin/database/memory/mmap.hpp +++ b/include/bitcoin/database/memory/mmap.hpp @@ -383,6 +383,7 @@ class mmap const paths filenames_; const size_t minimum_; const size_t expansion_; + const size_t headroom_; const advice access_; const bool random_; const bool staged_; diff --git a/include/bitcoin/database/memory/mstage.hpp b/include/bitcoin/database/memory/mstage.hpp index 43a8b6f2c..6944fd28a 100644 --- a/include/bitcoin/database/memory/mstage.hpp +++ b/include/bitcoin/database/memory/mstage.hpp @@ -42,7 +42,7 @@ void* mmap_reserve(size_t size) NOEXCEPT; /// Commit reserved pages as readable/writable anonymous memory. -int mmap_commit(void* address, size_t size) NOEXCEPT; +int mmap_commit(void* address, size_t size, size_t headroom) NOEXCEPT; /// Replace committed pages with a read-only shared mapping of the file. int mmap_settle(void* address, size_t size, int fd, size_t offset) NOEXCEPT; diff --git a/include/bitcoin/database/memory/settings.hpp b/include/bitcoin/database/memory/settings.hpp index 31880963d..5ba490ece 100644 --- a/include/bitcoin/database/memory/settings.hpp +++ b/include/bitcoin/database/memory/settings.hpp @@ -56,6 +56,10 @@ struct storage_settings /// Body expansion rate (percentage). uint16_t rate{ 5 }; + /// Headroom (bytes): growth is admitted only while it leaves this much + /// of the backing resource (memory commitment, disk space) unclaimed. + uint64_t headroom{ system::power2(28u) }; + /// Page advice for mapped reads (see advice). Bodies default to /// scattered: they are far too large to reside, and validation reads /// prevouts from arbitrary earlier blocks, so read-ahead manufactures diff --git a/include/bitcoin/database/store.hpp b/include/bitcoin/database/store.hpp index 3b08b020a..541cc47fa 100644 --- a/include/bitcoin/database/store.hpp +++ b/include/bitcoin/database/store.hpp @@ -237,7 +237,8 @@ class store // Heads are minimally allocated with no expansion (heads size to their // configured buckets at table creation) and randomly probed (the advice // preserves optimal classic mapped reads; staged heads are anonymous). - static constexpr storage_settings head_settings{ 1, 0, advice::random }; + static constexpr storage_settings head_settings{ .size = 1, .rate = 0, + .access = advice::random }; // Bodies are append-only, so stage writes in anonymous memory where the // staging backend is built (heads update in place and remain resident). diff --git a/src/memory/mstage.cpp b/src/memory/mstage.cpp index d11e950df..325382e38 100644 --- a/src/memory/mstage.cpp +++ b/src/memory/mstage.cpp @@ -46,9 +46,23 @@ void* mmap_reserve(size_t size) NOEXCEPT -1, 0); } -int mmap_commit(void* address, size_t size) NOEXCEPT +// The commitment is granted only if it leaves the headroom free: the probe +// charges the headroom alongside the request (atomic with its admission) +// and releases it upon the grant. +int mmap_commit(void* address, size_t size, size_t headroom) NOEXCEPT { - return ::mprotect(address, size, PROT_READ | PROT_WRITE); + if (is_zero(headroom)) + return ::mprotect(address, size, PROT_READ | PROT_WRITE); + + const auto probe = ::mmap(nullptr, headroom, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + + if (probe == MAP_FAILED) + return -1; + + const auto result = ::mprotect(address, size, PROT_READ | PROT_WRITE); + ::munmap(probe, headroom); + return result; } int mmap_settle(void* address, size_t size, int fd, size_t offset) NOEXCEPT From 72a1316234dd6440e1f81fc82f5defb5072e1c24 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Fri, 7 Aug 2026 14:01:27 -0400 Subject: [PATCH 05/11] Probe disk headroom via sibling reservation. --- .../database/impl/memory/mmap_private.ipp | 71 ++++++++++++++----- .../database/impl/memory/mmap_storage.ipp | 8 +-- 2 files changed, 56 insertions(+), 23 deletions(-) diff --git a/include/bitcoin/database/impl/memory/mmap_private.ipp b/include/bitcoin/database/impl/memory/mmap_private.ipp index c7b2ea035..bd2cde291 100644 --- a/include/bitcoin/database/impl/memory/mmap_private.ipp +++ b/include/bitcoin/database/impl/memory/mmap_private.ipp @@ -332,41 +332,74 @@ bool CLASS::resize_(size_t size, bool final) NOEXCEPT using namespace system; const auto target = to_width(size); const auto capacity = to_width(extent); - const auto probed = ceilinged_add(target, headroom_); - // Disk full detection, any other failure is an abort. The extension is - // probed with the headroom, released upon the grant (truncated back). + // The extension is admitted only if it leaves the headroom free: a + // sibling reservation charges the headroom on the same volume across + // the extension and releases it upon the grant (the mapped file itself + // is never oversized, which msc cannot shrink under a live section). + auto probe = file::invalid; + path name{}; + if (!is_zero(headroom_)) + { + name = filenames_[Column]; + name += ".probe"; + if (file::create_file(name)) + probe = file::open(name); + #if !defined(WITHOUT_FALLOCATE) - if (::fallocate(opened_[Column], 0, capacity, probed - capacity) == fail) + if ((probe == file::invalid) || + (::fallocate(probe, 0, zero, headroom_) == fail)) #else - if (::ftruncate(opened_[Column], probed) == fail) + if ((probe == file::invalid) || + (::ftruncate(probe, headroom_) == fail)) #endif - { - // Disk full is the only restartable store failure (leave mapped). - // A non-final refusal is not published: the caller retries reduced. - if (errno == ENOSPC) { + if (probe != file::invalid) + file::close(probe); + + file::remove(name); if (final) - set_disk_space(ceilinged_multiply( - floored_subtract(size, extent), stride)); + set_disk_space(ceilinged_add(headroom_, ceilinged_multiply( + floored_subtract(size, extent), stride))); return false; } + } - set_first_code(error::ftruncate_failure); - unmap_(capacity_.load()); - return false; + // Disk full detection, any other failure is an abort. +#if !defined(WITHOUT_FALLOCATE) + const auto extended = + ::fallocate(opened_[Column], 0, capacity, target - capacity) != fail; +#else + const auto extended = ::ftruncate(opened_[Column], target) != fail; +#endif + const auto full = !extended && (errno == ENOSPC); + + if (probe != file::invalid) + { + file::close(probe); + file::remove(name); } - if (!is_zero(headroom_) && - (::ftruncate(opened_[Column], target) == fail)) + if (extended) + return true; + + // Disk full is the only restartable store failure (leave mapped). + // A non-final refusal is not published: the caller retries reduced. + // The published requirement includes the headroom: a retry re-probes, + // so resumption below the sum would only bounce back into suspension. + if (full) { - set_first_code(error::ftruncate_failure); - unmap_(capacity_.load()); + if (final) + set_disk_space(ceilinged_add(headroom_, ceilinged_multiply( + floored_subtract(size, extent), stride))); + return false; } - return true; + set_first_code(error::ftruncate_failure); + unmap_(capacity_.load()); + return false; } // Finalize failure results in unmapped. diff --git a/include/bitcoin/database/impl/memory/mmap_storage.ipp b/include/bitcoin/database/impl/memory/mmap_storage.ipp index f98027540..cd7ad5ab8 100644 --- a/include/bitcoin/database/impl/memory/mmap_storage.ipp +++ b/include/bitcoin/database/impl/memory/mmap_storage.ipp @@ -504,8 +504,8 @@ bool CLASS::expand(size_t count) NOEXCEPT if (extended <= count) { - set_disk_space(ceilinged_multiply( - floored_subtract(count, capacity_.load()), stride)); + set_disk_space(ceilinged_add(headroom_, ceilinged_multiply( + floored_subtract(count, capacity_.load()), stride))); return false; } } @@ -548,8 +548,8 @@ bool CLASS::reserve(size_t count) NOEXCEPT if (extended <= end) { - set_disk_space(ceilinged_multiply( - floored_subtract(end, capacity_.load()), stride)); + set_disk_space(ceilinged_add(headroom_, ceilinged_multiply( + floored_subtract(end, capacity_.load()), stride))); return false; } } From c6eb4e541f7a5bebe0f486d6cad16cb41267787c Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Fri, 7 Aug 2026 16:13:05 -0400 Subject: [PATCH 06/11] Honor non-final refusal in reservation migration. --- .../database/impl/memory/mmap_staging.ipp | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/include/bitcoin/database/impl/memory/mmap_staging.ipp b/include/bitcoin/database/impl/memory/mmap_staging.ipp index d76a35f0f..b21f9acd0 100644 --- a/include/bitcoin/database/impl/memory/mmap_staging.ipp +++ b/include/bitcoin/database/impl/memory/mmap_staging.ipp @@ -441,9 +441,10 @@ bool CLASS::stage_() NOEXCEPT } // Commit failure results in unmapped when final (the default); a non-final -// in-reservation commit refusal returns false untouched, so the caller may -// iterate a reduced request (commit is a kernel admission decision evaluated -// per request, so a refused amortization step does not imply exhaustion). +// refusal (in-reservation commit, replacement reservation, or replacement +// commit) returns false with the standing mapping untouched, so the caller +// may iterate a reduced request (admission is evaluated per request, so a +// refused amortization step does not imply exhaustion). // Growth within the reservation commits pages in place (stable map base); an // exhausted reservation is replaced and its unsettled content copied, under // the exclusive remap lock held by the caller. @@ -501,7 +502,9 @@ bool CLASS::commit_(size_t size, bool final) NOEXCEPT if (replace == MAP_FAILED) { - teardown_(error::mmap_failure); + if (final) + teardown_(error::mmap_failure); + return false; } @@ -535,11 +538,19 @@ bool CLASS::commit_(size_t size, bool final) NOEXCEPT return true; } + // The replacement commit spans the unsettled prefix, transiently charged + // over the standing reservation, so its refusal is the largest single + // admission of the design: non-final refusal leaves the standing mapping + // untouched for the caller's reduced retry (a reduced ask fits within + // the standing reservation), and settle drainage shrinks the span, so a + // necessity refusal pauses recoverable rather than tearing down. if (mmap_commit(std::next(base, settled), target - settled, headroom_) == fail) { ::munmap(replace, reserved); - teardown_(error::mmap_failure); + if (final) + teardown_(error::mmap_failure); + return false; } From 34be6c721a28982d0c6e668c089a05408c4e4ce1 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Fri, 7 Aug 2026 18:04:32 -0400 Subject: [PATCH 07/11] Serialize staged claims and ladder all growth. --- .../database/impl/memory/mmap_staging.ipp | 31 ++-- .../database/impl/memory/mmap_storage.ipp | 143 +++++++++--------- include/bitcoin/database/memory/mmap.hpp | 3 +- 3 files changed, 98 insertions(+), 79 deletions(-) diff --git a/include/bitcoin/database/impl/memory/mmap_staging.ipp b/include/bitcoin/database/impl/memory/mmap_staging.ipp index b21f9acd0..d6b178a36 100644 --- a/include/bitcoin/database/impl/memory/mmap_staging.ipp +++ b/include/bitcoin/database/impl/memory/mmap_staging.ipp @@ -160,35 +160,42 @@ size_t CLASS::frontier() const NOEXCEPT #if defined(MANAGE_STAGING) +// Claim and record an extent under one lock: a claim never exists outside +// the ring and the ring is start-ordered, so the frontier can never pass an +// unwritten extent (claim-then-record raced the frontier past the claim). +// Returns eof (unclaimed) on insufficient capacity, fault, or disk full. TEMPLATE -void CLASS::record_(size_t start, size_t count) NOEXCEPT +size_t CLASS::record_(size_t count) NOEXCEPT { - if (!staged_ || is_zero(count)) - return; - std::unique_lock extent_lock(extent_mutex_); + if (is_zero(count)) + return logical_.load(); + maintain_(); using namespace system; auto [head, size] = unpack_word(window_.load(relaxed)); // A full ring waits on completions (extents are allocation-coarse, so - // saturation implies extreme concurrency). An unrecorded extent would be - // unsafe: an emptied ring advances the frontier to logical, so untracked - // incomplete writes could settle. Completions are lock-free, so waiting - // needs only this thread's own maintenance; fault or disk full releases - // the wait (recording is then moot, as recovery discards the ring). + // saturation implies extreme concurrency). Completions are lock-free, so + // waiting needs only this thread's own maintenance; fault or disk full + // releases the wait unclaimed (the write then fails fast). while (size == extents) { if (fault_.load() || !is_zero(space_.load())) - return; + return storage::eof; std::this_thread::yield(); maintain_(); std::tie(head, size) = unpack_word(window_.load(relaxed)); } + const auto start = logical_.load(); + if (is_add_overflow(start, count) || + ((start + count) > capacity_.load())) + return storage::eof; + auto& record = ring_.at((head + size) % extents); const auto generation = bit_and(add1(shift_right( record.state.load(relaxed), generation_shift)), generation_mask); @@ -206,6 +213,10 @@ void CLASS::record_(size_t start, size_t count) NOEXCEPT window_.store(pack_word(head, add1(size)), release); if (is_zero(size)) frontier_.store(start); + + logical_.store(start + count); + check_invariants_(); + return start; } // Pop completed extents from the head, advancing the frontier (locked). diff --git a/include/bitcoin/database/impl/memory/mmap_storage.ipp b/include/bitcoin/database/impl/memory/mmap_storage.ipp index cd7ad5ab8..b96adad1a 100644 --- a/include/bitcoin/database/impl/memory/mmap_storage.ipp +++ b/include/bitcoin/database/impl/memory/mmap_storage.ipp @@ -462,6 +462,42 @@ bool CLASS::truncate(size_t count) NOEXCEPT return true; } +// Iterated growth (callers hold the remap lock). Growth asks are amortized +// (rate surplus over the necessity), and each is admitted only while it +// leaves the configured headroom of the backing resource unclaimed (probed +// with the ask, released on grant), so exhaustion never consumes the +// system's final bytes. A large amortization step can be refused while the +// necessity fits, so iterate: halve the refused surplus toward the +// necessity. Refusal of the necessity is exhaustion, not store damage: +// published as disk full (space set, store intact, writes fail fast until +// cleared), it clears by settle drainage or operator relief, where teardown +// would convert a shortage into a restore. +TEMPLATE +bool CLASS::grow_(size_t end) NOEXCEPT +{ + if (!is_zero(space_.load())) + return false; + + using namespace system; + for (auto extended = to_growth(end); + !remap_all_(extended, sequence{}, false); + extended = ceilinged_add(end, + to_half(floored_subtract(extended, end)))) + { + if (fault_.load()) + return false; + + if (extended <= end) + { + set_disk_space(ceilinged_add(headroom_, ceilinged_multiply( + floored_subtract(end, capacity_.load()), stride))); + return false; + } + } + + return true; +} + TEMPLATE bool CLASS::expand(size_t count) NOEXCEPT { @@ -476,39 +512,8 @@ bool CLASS::expand(size_t count) NOEXCEPT if (count > capacity_.load()) { std::unique_lock remap_lock(remap_mutex_); - - // Growth asks are amortized (rate surplus over the necessity), and - // admission of an ask is a resource decision evaluated per request: - // the kernel charges a commitment against ram+swap, and the disk - // charges a provisioning extension against free space. Either can - // refuse a large amortization step while the necessity is easily - // backed, so iterate: halve the refused surplus toward the necessity. - // Growth asks are amortized (rate surplus over the necessity), and - // each is admitted only while it leaves the configured headroom of - // the backing resource unclaimed (probed with the ask, released on - // grant), so exhaustion never consumes the system's final bytes. A - // large amortization step can be refused while the necessity fits, - // so iterate: halve the refused surplus toward the necessity. - // Refusal of the necessity is exhaustion, not store damage: - // published as disk full (space set, store intact, writes fail fast - // until cleared), it clears by settle drainage or operator relief, - // where teardown would convert a shortage into a restore. - using namespace system; - for (auto extended = to_growth(count); - !remap_all_(extended, sequence{}, false); - extended = ceilinged_add(count, - to_half(floored_subtract(extended, count)))) - { - if (fault_.load()) - return false; - - if (extended <= count) - { - set_disk_space(ceilinged_add(headroom_, ceilinged_multiply( - floored_subtract(count, capacity_.load()), stride))); - return false; - } - } + if (!grow_(count)) + return false; } // Raise to at least count (concurrent claims may already exceed it). @@ -533,26 +538,8 @@ bool CLASS::reserve(size_t count) NOEXCEPT if (end > capacity_.load()) { std::unique_lock remap_lock(remap_mutex_); - - // Iterated growth (see expand): reduce a refused amortization step - // toward the necessity; refusal of the necessity is exhaustion of - // the refusing resource, published as disk full (recoverable), - // never teardown. - for (auto extended = to_growth(end); - !remap_all_(extended, sequence{}, false); - extended = ceilinged_add(end, - to_half(floored_subtract(extended, end)))) - { - if (fault_.load()) - return false; - - if (extended <= end) - { - set_disk_space(ceilinged_add(headroom_, ceilinged_multiply( - floored_subtract(end, capacity_.load()), stride))); - return false; - } - } + if (!grow_(end)) + return false; } // Same as allocate except logical does not change. @@ -567,14 +554,46 @@ bool CLASS::reserve(size_t count) NOEXCEPT TEMPLATE size_t CLASS::allocate(size_t count) NOEXCEPT { + using namespace system; + #if defined(MANAGE_STAGING) // Nothing is held here, so parking cannot deadlock (as with remap waits). throttle_(); -#endif + + // Staged claims serialize with extent recording (record_ locks on every + // claim regardless, so this adds no contention): a claim never exists + // outside the ring, so the frontier can never pass an unwritten extent. + if (staged_) + { + while (true) + { + if (fault_.load() || !loaded_.load()) + return storage::eof; + + const auto start = record_(count); + if (start != storage::eof) + return start; + + // Slow path: serialize capacity growth (at most one grower). + std::unique_lock field_lock(field_mutex_); + + const auto logical = logical_.load(); + if (is_add_overflow(logical, count)) + return storage::eof; + + const auto end = logical + count; + if (end > capacity_.load()) + { + std::unique_lock remap_lock(remap_mutex_); + if (!grow_(end)) + return storage::eof; + } + } + } +#endif // MANAGE_STAGING // Fast path: claim rows within published capacity (no locks). A failed // exchange implies another claim succeeded, so every retry is progress. - using namespace system; auto start = logical_.load(); while (true) { @@ -585,12 +604,7 @@ size_t CLASS::allocate(size_t count) NOEXCEPT break; if (logical_.compare_exchange_weak(start, start + count)) - { -#if defined(MANAGE_STAGING) - record_(start, count); -#endif return start; - } } // Slow path: serialize capacity growth (at most one grower). Fast paths @@ -607,23 +621,16 @@ size_t CLASS::allocate(size_t count) NOEXCEPT if (end <= capacity_.load()) { if (logical_.compare_exchange_weak(start, end)) - { -#if defined(MANAGE_STAGING) - record_(start, count); -#endif return start; - } continue; } - const auto extended = to_growth(end); - // TODO: Could loop over a try lock here and log deadlock warning. std::unique_lock remap_lock(remap_mutex_); // Disk full condition leaves store in valid state despite eof return. - if (!remap_all_(extended, sequence{})) + if (!grow_(end)) return storage::eof; } } diff --git a/include/bitcoin/database/memory/mmap.hpp b/include/bitcoin/database/memory/mmap.hpp index 2001cf1f3..414624fab 100644 --- a/include/bitcoin/database/memory/mmap.hpp +++ b/include/bitcoin/database/memory/mmap.hpp @@ -282,6 +282,7 @@ class mmap template bool remap_all_(size_t capacity, std::index_sequence, bool final=true) NOEXCEPT; + bool grow_(size_t end) NOEXCEPT; // mman wrappers, not thread safe. template @@ -325,7 +326,7 @@ class mmap // staging utilities, not thread safe (claim_ is lock-free thread safe). struct extent; - void record_(size_t start, size_t count) NOEXCEPT; + size_t record_(size_t count) NOEXCEPT; bool claim_(extent& record, size_t count) NOEXCEPT; void maintain_() NOEXCEPT; void discard_() NOEXCEPT; From bb9fe7a5478b375bfe19a5061cc65eb9d1efcdf5 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Fri, 7 Aug 2026 20:29:02 -0400 Subject: [PATCH 08/11] Avoid libbitcoin.org (currently squatted). --- builds/gnu/NEWS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builds/gnu/NEWS b/builds/gnu/NEWS index e1873049f..f9d5bbadd 100644 --- a/builds/gnu/NEWS +++ b/builds/gnu/NEWS @@ -1 +1 @@ -See https://libbitcoin.org \ No newline at end of file +See https://libbitcoin.info \ No newline at end of file From 415b4908df39f5e8a9f26f1c73c0ff646cbd4dc8 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Fri, 7 Aug 2026 21:47:14 -0400 Subject: [PATCH 09/11] Probe growth by wave, not column. --- .../database/impl/memory/mmap_private.ipp | 106 ++++++++++-------- include/bitcoin/database/memory/mmap.hpp | 1 + 2 files changed, 59 insertions(+), 48 deletions(-) diff --git a/include/bitcoin/database/impl/memory/mmap_private.ipp b/include/bitcoin/database/impl/memory/mmap_private.ipp index bd2cde291..8d4046723 100644 --- a/include/bitcoin/database/impl/memory/mmap_private.ipp +++ b/include/bitcoin/database/impl/memory/mmap_private.ipp @@ -110,6 +110,21 @@ template bool CLASS::remap_all_(size_t capacity, std::index_sequence, bool final) NOEXCEPT { + // Probe the wave's disk requirement before touching any column file: a + // refused wave then retains no surplus provisioning (columns cannot be + // trimmed after a partial wave, as msc cannot shrink a mapped file). + if (!probe_(capacity)) + { + if (final) + { + using namespace system; + set_disk_space(ceilinged_add(headroom_, ceilinged_multiply( + floored_subtract(capacity, file_.load()), stride))); + } + + return false; + } + if (!(remap_(capacity, final) && ...)) { // A non-final refusal leaves the maps and capacity intact for the @@ -333,73 +348,68 @@ bool CLASS::resize_(size_t size, bool final) NOEXCEPT const auto target = to_width(size); const auto capacity = to_width(extent); - // The extension is admitted only if it leaves the headroom free: a - // sibling reservation charges the headroom on the same volume across - // the extension and releases it upon the grant (the mapped file itself - // is never oversized, which msc cannot shrink under a live section). - auto probe = file::invalid; - path name{}; - if (!is_zero(headroom_)) - { - name = filenames_[Column]; - name += ".probe"; - if (file::create_file(name)) - probe = file::open(name); - + // Disk full detection, any other failure is an abort. The wave probe + // (remap_all_) precedes, so refusal here is a raced foreign consumer. #if !defined(WITHOUT_FALLOCATE) - if ((probe == file::invalid) || - (::fallocate(probe, 0, zero, headroom_) == fail)) + if (::fallocate(opened_[Column], 0, capacity, target - capacity) == fail) #else - if ((probe == file::invalid) || - (::ftruncate(probe, headroom_) == fail)) + if (::ftruncate(opened_[Column], target) == fail) #endif + { + // Disk full is the only restartable store failure (leave mapped). + // A non-final refusal is not published: the caller retries reduced. + // The published requirement includes the headroom (a retry probes). + if (errno == ENOSPC) { - if (probe != file::invalid) - file::close(probe); - - file::remove(name); if (final) set_disk_space(ceilinged_add(headroom_, ceilinged_multiply( floored_subtract(size, extent), stride))); return false; } + + set_first_code(error::ftruncate_failure); + unmap_(capacity_.load()); + return false; } - // Disk full detection, any other failure is an abort. + return true; +} + +// The wave probe reserves the whole extension plus headroom on the store +// volume (column widths sum to the stride), released upon the grant: a +// refused wave touches no column file, and a granted one leaves the +// headroom unclaimed. +TEMPLATE +bool CLASS::probe_(size_t capacity) NOEXCEPT +{ + using namespace system; + const auto bytes = ceilinged_multiply( + floored_subtract(capacity, file_.load()), stride); + + if (is_zero(bytes)) + return true; + + auto name = filenames_.front(); + name += ".probe"; + auto probe = file::invalid; + if (file::create_file(name)) + probe = file::open(name); + + const auto reserve = ceilinged_add(bytes, headroom_); #if !defined(WITHOUT_FALLOCATE) - const auto extended = - ::fallocate(opened_[Column], 0, capacity, target - capacity) != fail; + const auto held = (probe != file::invalid) && + (::fallocate(probe, 0, zero, reserve) != fail); #else - const auto extended = ::ftruncate(opened_[Column], target) != fail; + const auto held = (probe != file::invalid) && + (::ftruncate(probe, reserve) != fail); #endif - const auto full = !extended && (errno == ENOSPC); if (probe != file::invalid) - { file::close(probe); - file::remove(name); - } - - if (extended) - return true; - - // Disk full is the only restartable store failure (leave mapped). - // A non-final refusal is not published: the caller retries reduced. - // The published requirement includes the headroom: a retry re-probes, - // so resumption below the sum would only bounce back into suspension. - if (full) - { - if (final) - set_disk_space(ceilinged_add(headroom_, ceilinged_multiply( - floored_subtract(size, extent), stride))); - - return false; - } - set_first_code(error::ftruncate_failure); - unmap_(capacity_.load()); - return false; + file::remove(name); + return held; } // Finalize failure results in unmapped. diff --git a/include/bitcoin/database/memory/mmap.hpp b/include/bitcoin/database/memory/mmap.hpp index 414624fab..df161830f 100644 --- a/include/bitcoin/database/memory/mmap.hpp +++ b/include/bitcoin/database/memory/mmap.hpp @@ -283,6 +283,7 @@ class mmap bool remap_all_(size_t capacity, std::index_sequence, bool final=true) NOEXCEPT; bool grow_(size_t end) NOEXCEPT; + bool probe_(size_t capacity) NOEXCEPT; // mman wrappers, not thread safe. template From 48d214e45ab842f0a7ae7a4478901b21cc311223 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Fri, 7 Aug 2026 23:35:09 -0400 Subject: [PATCH 10/11] Compute darwin commit admission. --- src/memory/mstage.cpp | 50 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/src/memory/mstage.cpp b/src/memory/mstage.cpp index 325382e38..45e787f4d 100644 --- a/src/memory/mstage.cpp +++ b/src/memory/mstage.cpp @@ -31,6 +31,8 @@ #if defined(HAVE_APPLE) #include #include + #include + #include #endif #if defined(HAVE_LINUX) #include @@ -46,22 +48,68 @@ void* mmap_reserve(size_t size) NOEXCEPT -1, 0); } +#if defined(HAVE_APPLE) + +// Darwin admits every anonymous ask (exhaustion arrives at first touch), so +// admission is computed: free ram plus swap growth room (darwin swap is +// files created on demand in the vm volume). A failed measurement term +// contributes zero (conservative), and the bound moves at disk speed, so +// the headroom absorbs the measure-to-touch race. +static bool mmap_admit(size_t size) NOEXCEPT +{ + vm_statistics64_data_t stats{}; + mach_msg_type_number_t count = HOST_VM_INFO64_COUNT; + uint64_t ram{}; + if (::host_statistics64(::mach_host_self(), HOST_VM_INFO64, + reinterpret_cast(&stats), &count) == KERN_SUCCESS) + ram = ceilinged_multiply( + ceilinged_add(stats.free_count, stats.inactive_count), + possible_wide_cast(vm_page_size)); + + xsw_usage swap{}; + size_t length = sizeof(swap); + uint64_t slack{}; + if (::sysctlbyname("vm.swapusage", &swap, &length, nullptr, 0) == 0) + slack = floored_subtract(swap.xsu_total, swap.xsu_used); + + struct statfs volume{}; + uint64_t growth{}; + if (::statfs("/System/Volumes/VM", &volume) == 0) + growth = ceilinged_multiply(possible_wide_cast( + volume.f_bavail), possible_wide_cast(volume.f_bsize)); + + return possible_wide_cast(size) <= + ceilinged_add(ram, ceilinged_add(slack, growth)); +} + +#endif // HAVE_APPLE + // The commitment is granted only if it leaves the headroom free: the probe // charges the headroom alongside the request (atomic with its admission) -// and releases it upon the grant. +// and releases it upon the grant. Darwin refuses no charge, so its +// admission is computed rather than delegated. int mmap_commit(void* address, size_t size, size_t headroom) NOEXCEPT { if (is_zero(headroom)) return ::mprotect(address, size, PROT_READ | PROT_WRITE); +#if defined(HAVE_APPLE) + if (!mmap_admit(ceilinged_add(size, headroom))) + return -1; +#else const auto probe = ::mmap(nullptr, headroom, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (probe == MAP_FAILED) return -1; +#endif const auto result = ::mprotect(address, size, PROT_READ | PROT_WRITE); + +#if !defined(HAVE_APPLE) ::munmap(probe, headroom); +#endif + return result; } From a36512208d2652de8261e608194ac2bf3a0df117 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Sat, 8 Aug 2026 10:50:52 -0400 Subject: [PATCH 11/11] Comments. --- include/bitcoin/database/impl/memory/mmap_storage.ipp | 1 + 1 file changed, 1 insertion(+) diff --git a/include/bitcoin/database/impl/memory/mmap_storage.ipp b/include/bitcoin/database/impl/memory/mmap_storage.ipp index b96adad1a..d757cd0a3 100644 --- a/include/bitcoin/database/impl/memory/mmap_storage.ipp +++ b/include/bitcoin/database/impl/memory/mmap_storage.ipp @@ -186,6 +186,7 @@ code CLASS::reload() NOEXCEPT return error::success; } + // Locked by reader(s), as write suspension is a precondition. return error::reload_locked; }