diff --git a/docs/metrics.md b/docs/metrics.md index 5e4e97c050..6d6b8b5967 100644 --- a/docs/metrics.md +++ b/docs/metrics.md @@ -38,13 +38,13 @@ bucketlist-archive.size.bytes | counter | total size of the hot ar bucketlist.size.bytes | counter | total size of the BucketList in bytes bucketlist.entryCounts.- | counter | number of entries of type in the BucketList bucketlist.entrySizes.- | counter | size of entries of type in the BucketList -bucketlistDB.bloom.lookups | meter | number of bloom filter lookups -bucketlistDB.bloom.misses | meter | number of bloom filter false positives -bucketlistDB.bulk.loads | meter | number of entries BucketListDB queried to prefetch -bucketlistDB.bulk.inflationWinners | timer | time to load inflation winners -bucketlistDB.bulk.poolshareTrustlines | timer | time to load poolshare trustlines by accountID and assetID -bucketlistDB.bulk.prefetch | timer | time to prefetch -bucketlistDB.point. | timer | time to load single entry of type (if no bloom miss occurred) +bucketlistDB-.bloom.lookups | meter | number of bloom filter lookups on BucketList (live/hotArchive) +bucketlistDB-.bloom.misses | meter | number of bloom filter false positives on BucketList (live/hotArchive) +bucketlistDB-.bulk.loads | meter | number of entries BucketListDB queried to prefetch on BucketList (live/hot-archive) +bucketlistDB-live.bulk.inflationWinners | timer | time to load inflation winners +bucketlistDB-live.bulk.poolshareTrustlines | timer | time to load poolshare trustlines by accountID and assetID +bucketlistDB-live.bulk.prefetch | timer | time to prefetch +bucketlistDB-.point. | timer | time to load single entry of type on BucketList (live/hotArchive) crypto.verify.hit | meter | number of signature cache hits crypto.verify.miss | meter | number of signature cache misses crypto.verify.total | meter | sum of both hits and misses diff --git a/docs/stellar-core_example.cfg b/docs/stellar-core_example.cfg index 77699bbb94..062c9d9a38 100644 --- a/docs/stellar-core_example.cfg +++ b/docs/stellar-core_example.cfg @@ -235,13 +235,13 @@ MAX_DEX_TX_OPERATIONS_IN_TX_SET = 0 # 0, indiviudal index is always used. Default page size 16 kb. BUCKETLIST_DB_INDEX_PAGE_SIZE_EXPONENT = 14 -# BUCKETLIST_DB_INDEX_CUTOFF (Integer) default 20 +# BUCKETLIST_DB_INDEX_CUTOFF (Integer) default 250 # Size, in MB, determining whether a bucket should have an individual # key index or a key range index. If bucket size is below this value, range # based index will be used. If set to 0, all buckets are range indexed. If # BUCKETLIST_DB_INDEX_PAGE_SIZE_EXPONENT == 0, value ingnored and all # buckets have individual key index. -BUCKETLIST_DB_INDEX_CUTOFF = 20 +BUCKETLIST_DB_INDEX_CUTOFF = 250 # BUCKETLIST_DB_PERSIST_INDEX (bool) default true # Determines whether BucketListDB indexes are saved to disk for faster diff --git a/src/bucket/BucketApplicator.cpp b/src/bucket/BucketApplicator.cpp index 91f86531c6..ec83a7256d 100644 --- a/src/bucket/BucketApplicator.cpp +++ b/src/bucket/BucketApplicator.cpp @@ -82,7 +82,7 @@ shouldApplyEntry(BucketEntry const& e) { if (e.type() == LIVEENTRY || e.type() == INITENTRY) { - return BucketIndex::typeNotSupported(e.liveEntry().data.type()); + return LiveBucketIndex::typeNotSupported(e.liveEntry().data.type()); } if (e.type() != DEADENTRY) @@ -90,7 +90,7 @@ shouldApplyEntry(BucketEntry const& e) throw std::runtime_error( "Malformed bucket: unexpected non-INIT/LIVE/DEAD entry."); } - return BucketIndex::typeNotSupported(e.deadEntry().type()); + return LiveBucketIndex::typeNotSupported(e.deadEntry().type()); } size_t diff --git a/src/bucket/BucketBase.cpp b/src/bucket/BucketBase.cpp index 0917b20a82..d2ef06098e 100644 --- a/src/bucket/BucketBase.cpp +++ b/src/bucket/BucketBase.cpp @@ -7,7 +7,6 @@ // else. #include "util/asio.h" // IWYU pragma: keep #include "bucket/BucketBase.h" -#include "bucket/BucketIndex.h" #include "bucket/BucketInputIterator.h" #include "bucket/BucketManager.h" #include "bucket/BucketOutputIterator.h" @@ -30,8 +29,9 @@ namespace stellar { -BucketIndex const& -BucketBase::getIndex() const +template +IndexT const& +BucketBase::getIndex() const { ZoneScoped; releaseAssertOrThrow(!mFilename.empty()); @@ -39,27 +39,25 @@ BucketBase::getIndex() const return *mIndex; } +template bool -BucketBase::isIndexed() const +BucketBase::isIndexed() const { return static_cast(mIndex); } -std::optional> -BucketBase::getOfferRange() const -{ - return getIndex().getOfferRange(); -} - +template void -BucketBase::setIndex(std::unique_ptr&& index) +BucketBase::setIndex(std::unique_ptr&& index) { releaseAssertOrThrow(!mIndex); mIndex = std::move(index); } -BucketBase::BucketBase(std::string const& filename, Hash const& hash, - std::unique_ptr&& index) +template +BucketBase::BucketBase(std::string const& filename, + Hash const& hash, + std::unique_ptr&& index) : mFilename(filename), mHash(hash), mIndex(std::move(index)) { releaseAssert(filename.empty() || fs::exists(filename)); @@ -71,30 +69,34 @@ BucketBase::BucketBase(std::string const& filename, Hash const& hash, } } -BucketBase::BucketBase() +template BucketBase::BucketBase() { } +template Hash const& -BucketBase::getHash() const +BucketBase::getHash() const { return mHash; } +template std::filesystem::path const& -BucketBase::getFilename() const +BucketBase::getFilename() const { return mFilename; } +template size_t -BucketBase::getSize() const +BucketBase::getSize() const { return mSize; } +template bool -BucketBase::isEmpty() const +BucketBase::isEmpty() const { if (mFilename.empty() || isZero(mHash)) { @@ -105,14 +107,17 @@ BucketBase::isEmpty() const return false; } +template void -BucketBase::freeIndex() +BucketBase::freeIndex() { mIndex.reset(nullptr); } +template std::string -BucketBase::randomFileName(std::string const& tmpDir, std::string ext) +BucketBase::randomFileName(std::string const& tmpDir, + std::string ext) { ZoneScoped; for (;;) @@ -127,14 +132,16 @@ BucketBase::randomFileName(std::string const& tmpDir, std::string ext) } } +template std::string -BucketBase::randomBucketName(std::string const& tmpDir) +BucketBase::randomBucketName(std::string const& tmpDir) { return randomFileName(tmpDir, ".xdr"); } +template std::string -BucketBase::randomBucketIndexName(std::string const& tmpDir) +BucketBase::randomBucketIndexName(std::string const& tmpDir) { return randomFileName(tmpDir, ".index"); } @@ -172,7 +179,7 @@ BucketBase::randomBucketIndexName(std::string const& tmpDir) // and shadowing protocol simultaneously, the moment the first new-protocol // bucket enters the youngest level. At least one new bucket is in every merge's // shadows from then on in, so they all upgrade (and preserve lifecycle events). -template +template static void calculateMergeProtocolVersion( MergeCounters& mc, uint32_t maxProtocolVersion, @@ -253,7 +260,7 @@ calculateMergeProtocolVersion( // side, or entries that compare non-equal. In all these cases we just // take the lesser (or existing) entry and advance only one iterator, // not scrutinizing the entry type further. -template +template static bool mergeCasesWithDefaultAcceptance( BucketEntryIdCmp const& cmp, MergeCounters& mc, @@ -299,14 +306,15 @@ mergeCasesWithDefaultAcceptance( return false; } -template +template std::shared_ptr -BucketBase::merge(BucketManager& bucketManager, uint32_t maxProtocolVersion, - std::shared_ptr const& oldBucket, - std::shared_ptr const& newBucket, - std::vector> const& shadows, - bool keepTombstoneEntries, bool countMergeEvents, - asio::io_context& ctx, bool doFsync) +BucketBase::merge( + BucketManager& bucketManager, uint32_t maxProtocolVersion, + std::shared_ptr const& oldBucket, + std::shared_ptr const& newBucket, + std::vector> const& shadows, + bool keepTombstoneEntries, bool countMergeEvents, asio::io_context& ctx, + bool doFsync) { BUCKET_TYPE_ASSERT(BucketT); @@ -326,9 +334,9 @@ BucketBase::merge(BucketManager& bucketManager, uint32_t maxProtocolVersion, uint32_t protocolVersion; bool keepShadowedLifecycleEntries; - calculateMergeProtocolVersion(mc, maxProtocolVersion, oi, ni, - shadowIterators, protocolVersion, - keepShadowedLifecycleEntries); + calculateMergeProtocolVersion( + mc, maxProtocolVersion, oi, ni, shadowIterators, protocolVersion, + keepShadowedLifecycleEntries); auto timer = bucketManager.getMergeTimer().TimeScope(); BucketMetadata meta; @@ -340,14 +348,14 @@ BucketBase::merge(BucketManager& bucketManager, uint32_t maxProtocolVersion, { releaseAssertOrThrow(protocolVersionStartsFrom( maxProtocolVersion, - BucketBase::FIRST_PROTOCOL_SUPPORTING_PERSISTENT_EVICTION)); + BucketT::FIRST_PROTOCOL_SUPPORTING_PERSISTENT_EVICTION)); meta.ext = ni.getMetadata().ext; } else if (oi.getMetadata().ext.v() == 1) { releaseAssertOrThrow(protocolVersionStartsFrom( maxProtocolVersion, - BucketBase::FIRST_PROTOCOL_SUPPORTING_PERSISTENT_EVICTION)); + BucketT::FIRST_PROTOCOL_SUPPORTING_PERSISTENT_EVICTION)); meta.ext = oi.getMetadata().ext; } @@ -374,9 +382,9 @@ BucketBase::merge(BucketManager& bucketManager, uint32_t maxProtocolVersion, } } - if (!mergeCasesWithDefaultAcceptance(cmp, mc, oi, ni, out, - shadowIterators, protocolVersion, - keepShadowedLifecycleEntries)) + if (!mergeCasesWithDefaultAcceptance( + cmp, mc, oi, ni, out, shadowIterators, protocolVersion, + keepShadowedLifecycleEntries)) { BucketT::mergeCasesWithEqualKeys(mc, oi, ni, out, shadowIterators, protocolVersion, @@ -400,19 +408,6 @@ BucketBase::merge(BucketManager& bucketManager, uint32_t maxProtocolVersion, return out.getBucket(bucketManager, &mk); } -template std::shared_ptr BucketBase::merge( - BucketManager& bucketManager, uint32_t maxProtocolVersion, - std::shared_ptr const& oldBucket, - std::shared_ptr const& newBucket, - std::vector> const& shadows, - bool keepTombstoneEntries, bool countMergeEvents, asio::io_context& ctx, - bool doFsync); - -template std::shared_ptr BucketBase::merge( - BucketManager& bucketManager, uint32_t maxProtocolVersion, - std::shared_ptr const& oldBucket, - std::shared_ptr const& newBucket, - std::vector> const& shadows, - bool keepTombstoneEntries, bool countMergeEvents, asio::io_context& ctx, - bool doFsync); +template class BucketBase; +template class BucketBase; } \ No newline at end of file diff --git a/src/bucket/BucketBase.h b/src/bucket/BucketBase.h index bd472f6fef..cbd43236b8 100644 --- a/src/bucket/BucketBase.h +++ b/src/bucket/BucketBase.h @@ -4,7 +4,7 @@ // under the apache license, version 2.0. see the copying file at the root // of this distribution or at http://www.apache.org/licenses/license-2.0 -#include "bucket/BucketIndex.h" +#include "bucket/BucketUtils.h" #include "util/NonCopyable.h" #include "util/ProtocolVersion.h" #include "xdr/Stellar-types.h" @@ -47,17 +47,37 @@ enum class Loop INCOMPLETE }; +class HotArchiveBucket; +class LiveBucket; +class LiveBucketIndex; +class HotArchiveBucketIndex; + +template class BucketBase : public NonMovableOrCopyable { + BUCKET_TYPE_ASSERT(BucketT); + + // Because of the CRTP design with derived Bucket classes, this base class + // does not have direct access to BucketT::IndexT, so we take two templates + // and make this assert. + static_assert( + std::is_same_v< + IndexT, + std::conditional_t< + std::is_same_v, LiveBucketIndex, + std::conditional_t, + HotArchiveBucketIndex, void>>>, + "IndexT must match BucketT::IndexT"); + protected: std::filesystem::path const mFilename; Hash const mHash; size_t mSize{0}; - std::unique_ptr mIndex{}; + std::unique_ptr mIndex{}; // Returns index, throws if index not yet initialized - BucketIndex const& getIndex() const; + IndexT const& getIndex() const; static std::string randomFileName(std::string const& tmpDir, std::string ext); @@ -74,7 +94,7 @@ class BucketBase : public NonMovableOrCopyable // exists, but does not check that the hash is the bucket's hash. Caller // needs to ensure that. BucketBase(std::string const& filename, Hash const& hash, - std::unique_ptr&& index); + std::unique_ptr&& index); Hash const& getHash() const; std::filesystem::path const& getFilename() const; @@ -88,13 +108,8 @@ class BucketBase : public NonMovableOrCopyable // Returns true if bucket is indexed, false otherwise bool isIndexed() const; - // Returns [lowerBound, upperBound) of file offsets for all offers in the - // bucket, or std::nullopt if no offers exist - std::optional> - getOfferRange() const; - // Sets index, throws if index is already set - void setIndex(std::unique_ptr&& index); + void setIndex(std::unique_ptr&& index); // Merge two buckets together, producing a fresh one. Entries in `oldBucket` // are overridden in the fresh bucket by keywise-equal entries in @@ -107,7 +122,6 @@ class BucketBase : public NonMovableOrCopyable // `maxProtocolVersion` bounds this (for error checking) and should usually // be the protocol of the ledger header at which the merge is starting. An // exception will be thrown if any provided bucket versions exceed it. - template static std::shared_ptr merge(BucketManager& bucketManager, uint32_t maxProtocolVersion, std::shared_ptr const& oldBucket, @@ -120,7 +134,7 @@ class BucketBase : public NonMovableOrCopyable static std::string randomBucketIndexName(std::string const& tmpDir); #ifdef BUILD_TESTS - BucketIndex const& + IndexT const& getIndexForTesting() const { return getIndex(); @@ -128,8 +142,6 @@ class BucketBase : public NonMovableOrCopyable #endif // BUILD_TESTS - virtual uint32_t getBucketVersion() const = 0; - - template friend class BucketSnapshotBase; + template friend class BucketSnapshotBase; }; } diff --git a/src/bucket/BucketIndex.h b/src/bucket/BucketIndex.h deleted file mode 100644 index ee15031fc1..0000000000 --- a/src/bucket/BucketIndex.h +++ /dev/null @@ -1,147 +0,0 @@ -#pragma once - -// Copyright 2022 Stellar Development Foundation and contributors. Licensed -// under the Apache License, Version 2.0. See the COPYING file at the root -// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 - -#include "util/GlobalChecks.h" -#include "util/NonCopyable.h" -#include "util/XDROperators.h" // IWYU pragma: keep -#include "xdr/Stellar-ledger-entries.h" -#include -#include -#include -#include - -#include - -namespace asio -{ -class io_context; -} - -namespace stellar -{ - -/** - * BucketIndex is an in-memory mapping of LedgerKey's to the file offset - * of the associated LedgerEntry in a given Bucket file. Because the set of - * LedgerKeys is too large to keep in memory, BucketIndex can either index - * individual keys or key ranges. - * - * For small buckets, an individual index is used for faster index lookup. For - * larger buckets, the range index is used. The range index cannot give an exact - * position for a given LedgerEntry or tell if it exists in the bucket, but can - * give an offset range for where the entry would be if it exists. Config flags - * determine the size of the range index, as well as what bucket size should use - * the individual index vs range index. - */ - -class BucketManager; -struct BucketEntryCounters; - -// BucketIndex abstract interface -class BucketIndex : public NonMovableOrCopyable -{ - public: - // maps smallest and largest LedgerKey on a given page inclusively - // [lowerBound, upperbound] - struct RangeEntry - { - LedgerKey lowerBound; - LedgerKey upperBound; - - RangeEntry() = default; - RangeEntry(LedgerKey low, LedgerKey high) - : lowerBound(low), upperBound(high) - { - releaseAssert(low < high || low == high); - } - - inline bool - operator==(RangeEntry const& in) const - { - return lowerBound == in.lowerBound && upperBound == in.upperBound; - } - - template - void - serialize(Archive& ar) - { - ar(lowerBound, upperBound); - } - }; - - using IndividualEntry = LedgerKey; - using RangeIndex = std::vector>; - using IndividualIndex = - std::vector>; - using Iterator = std::variant; - - inline static const std::string DB_BACKEND_STATE = "bl"; - inline static const uint32_t BUCKET_INDEX_VERSION = 4; - - // Returns true if LedgerEntryType not supported by BucketListDB - static bool typeNotSupported(LedgerEntryType t); - - // Builds index for given bucketfile. This is expensive (> 20 seconds for - // the largest buckets) and should only be called once. If pageSize == 0 or - // if file size is less than the cutoff, individual key index is used. - // Otherwise range index is used, with the range defined by pageSize. - template - static std::unique_ptr - createIndex(BucketManager& bm, std::filesystem::path const& filename, - Hash const& hash, asio::io_context& ctx); - - // Loads index from given file. If file does not exist or if saved - // index does not have same parameters as current config, return null - static std::unique_ptr - load(BucketManager const& bm, std::filesystem::path const& filename, - size_t bucketFileSize); - - virtual ~BucketIndex() = default; - - // Returns offset in the bucket file for the given key, or std::nullopt if - // the key is not found - virtual std::optional lookup(LedgerKey const& k) const = 0; - - // Begins searching for LegerKey k from start. - // Returns pair of: - // file offset in the bucket file for k, or std::nullopt if not found - // iterator that points to the first index entry not less than k, or - // BucketIndex::end() - virtual std::pair, Iterator> - scan(Iterator start, LedgerKey const& k) const = 0; - - // Returns lower bound and upper bound for poolshare trustline entry - // positions associated with the given accountID. If no trustlines found, - // returns nullopt - virtual std::optional> - getPoolshareTrustlineRange(AccountID const& accountID) const = 0; - - // Return all PoolIDs that contain the given asset on either side of the - // pool - virtual std::vector const& - getPoolIDsByAsset(Asset const& asset) const = 0; - - // Returns lower bound and upper bound for offer entry positions in the - // given bucket, or std::nullopt if no offers exist - virtual std::optional> - getOfferRange() const = 0; - - // Returns page size for index. InidividualIndex returns 0 for page size - virtual std::streamoff getPageSize() const = 0; - - virtual Iterator begin() const = 0; - - virtual Iterator end() const = 0; - - virtual void markBloomMiss() const = 0; - virtual void markBloomLookup() const = 0; - virtual BucketEntryCounters const& getBucketEntryCounters() const = 0; -#ifdef BUILD_TESTS - virtual bool operator==(BucketIndex const& inRaw) const = 0; -#endif -}; -} \ No newline at end of file diff --git a/src/bucket/BucketIndexImpl.cpp b/src/bucket/BucketIndexImpl.cpp deleted file mode 100644 index e010f93e65..0000000000 --- a/src/bucket/BucketIndexImpl.cpp +++ /dev/null @@ -1,705 +0,0 @@ -// Copyright 2022 Stellar Development Foundation and contributors. Licensed -// under the Apache License, Version 2.0. See the COPYING file at the root -// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 - -#include "bucket/BucketIndexImpl.h" -#include "bucket/BucketIndex.h" -#include "bucket/BucketManager.h" -#include "bucket/BucketUtils.h" -#include "bucket/HotArchiveBucket.h" -#include "bucket/LiveBucket.h" -#include "crypto/Hex.h" -#include "crypto/ShortHash.h" -#include "ledger/LedgerTypeUtils.h" -#include "main/Config.h" -#include "util/BinaryFuseFilter.h" -#include "util/BufferedAsioCerealOutputArchive.h" -#include "util/Fs.h" -#include "util/LogSlowExecution.h" -#include "util/Logging.h" -#include "util/XDRStream.h" - -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include - -namespace stellar -{ - -// Returns a poolshare trustline key with the given accountID and a PoolID -// filled with the fill byte -static LedgerKey -getDummyPoolShareTrustlineKey(AccountID const& accountID, uint8_t fill) -{ - LedgerKey key(TRUSTLINE); - key.trustLine().accountID = accountID; - key.trustLine().asset.type(ASSET_TYPE_POOL_SHARE); - key.trustLine().asset.liquidityPoolID().fill(fill); - return key; -} - -// Returns pagesize for given index based on config parameters and bucket size, -// in bytes -static inline std::streamoff -effectivePageSize(Config const& cfg, size_t bucketSize) -{ - // Convert cfg param from MB to bytes - if (auto cutoff = cfg.BUCKETLIST_DB_INDEX_CUTOFF * 1000000; - bucketSize < cutoff) - { - return 0; - } - - auto pageSizeExp = cfg.BUCKETLIST_DB_INDEX_PAGE_SIZE_EXPONENT; - releaseAssertOrThrow(pageSizeExp < 32); - return pageSizeExp == 0 ? 0 : 1UL << pageSizeExp; -} - -bool -BucketIndex::typeNotSupported(LedgerEntryType t) -{ - return t == OFFER; -} - -template -template -BucketIndexImpl::BucketIndexImpl(BucketManager& bm, - std::filesystem::path const& filename, - std::streamoff pageSize, - Hash const& hash, - asio::io_context& ctx, - BucketEntryT const& typeTag) - : mBloomMissMeter(bm.getBloomMissMeter()) - , mBloomLookupMeter(bm.getBloomLookupMeter()) -{ - static_assert(std::is_same_v || - std::is_same_v); - - ZoneScoped; - releaseAssert(!filename.empty()); - - { - auto timer = LogSlowExecution("Indexing bucket"); - mData.pageSize = pageSize; - - // We don't have a good way of estimating IndividualIndex size since - // keys are variable size, so only reserve range indexes since we know - // the page size ahead of time - if constexpr (std::is_same::value) - { - auto fileSize = std::filesystem::file_size(filename); - auto estimatedIndexEntries = fileSize / mData.pageSize; - mData.keysToOffset.reserve(estimatedIndexEntries); - } - - XDRInputFileStream in; - in.open(filename.string()); - std::streamoff pos = 0; - std::streamoff pageUpperBound = 0; - BucketEntryT be; - size_t iter = 0; - [[maybe_unused]] size_t count = 0; - - std::vector keyHashes; - auto seed = shortHash::getShortHashInitKey(); - - auto countEntry = [&](BucketEntry const& be) { - if (be.type() == METAENTRY) - { - // Do not count meta entries. - return; - } - auto ledt = bucketEntryToLedgerEntryAndDurabilityType(be); - mData.counters.entryTypeCounts[ledt]++; - mData.counters.entryTypeSizes[ledt] += xdr::xdr_size(be); - }; - - while (in && in.readOne(be)) - { - // peridocially check if bucket manager is exiting to stop indexing - // gracefully - if (++iter >= 1000) - { - iter = 0; - if (bm.isShutdown()) - { - throw std::runtime_error("Incomplete bucket index due to " - "BucketManager shutdown"); - } - } - - auto isMeta = [](auto const& be) { - if constexpr (std::is_same_v) - { - return be.type() == METAENTRY; - } - else - { - static_assert( - std::is_same_v, - "unexpected bucket type"); - return be.type() == HOT_ARCHIVE_METAENTRY; - } - }; - - if (!isMeta(be)) - { - ++count; - LedgerKey key = getBucketLedgerKey(be); - - if constexpr (std::is_same_v) - { - // We need an asset to poolID mapping for - // loadPoolshareTrustlineByAccountAndAsset queries. For this - // query, we only need to index INIT entries because: - // 1. PoolID is the hash of the Assets it refers to, so this - // index cannot be invalidated by newer LIVEENTRY updates - // 2. We do a join over all bucket indexes so we avoid - // storing - // multiple redundant index entries (i.e. LIVEENTRY - // updates) - // 3. We only use this index to collect the possible set of - // Trustline keys, then we load those keys. This means - // that we don't need to keep track of DEADENTRY. Even if - // a given INITENTRY has been deleted by a newer - // DEADENTRY, the trustline load will not return deleted - // trustlines, so the load result is still correct even - // if the index has a few deleted mappings. - if (be.type() == INITENTRY && key.type() == LIQUIDITY_POOL) - { - auto const& poolParams = be.liveEntry() - .data.liquidityPool() - .body.constantProduct() - .params; - mData.assetToPoolID[poolParams.assetA].emplace_back( - key.liquidityPool().liquidityPoolID); - mData.assetToPoolID[poolParams.assetB].emplace_back( - key.liquidityPool().liquidityPoolID); - } - } - - if constexpr (std::is_same_v) - { - auto keyBuf = xdr::xdr_to_opaque(key); - SipHash24 hasher(seed.data()); - hasher.update(keyBuf.data(), keyBuf.size()); - keyHashes.emplace_back(hasher.digest()); - - if (pos >= pageUpperBound) - { - pageUpperBound = - roundDown(pos, mData.pageSize) + mData.pageSize; - mData.keysToOffset.emplace_back(RangeEntry(key, key), - pos); - } - else - { - auto& rangeEntry = mData.keysToOffset.back().first; - releaseAssert(rangeEntry.upperBound < key); - rangeEntry.upperBound = key; - } - } - else - { - static_assert(std::is_same_v, - "unexpected index type"); - mData.keysToOffset.emplace_back(key, pos); - } - - if constexpr (std::is_same_v) - { - countEntry(be); - } - } - - pos = in.pos(); - } - - if constexpr (std::is_same_v) - { - // Binary Fuse filter requires at least 2 elements - if (keyHashes.size() > 1) - { - // There is currently an access error that occurs very rarely - // for some random seed values. If this occurs, simply rotate - // the seed and try again. - for (int i = 0; i < 10; ++i) - { - try - { - mData.filter = std::make_unique( - keyHashes, seed); - } - catch (std::out_of_range& e) - { - auto seedToStr = [](auto seed) { - std::string result; - for (auto b : seed) - { - fmt::format_to(std::back_inserter(result), - "{:02x}", b); - } - return result; - }; - - CLOG_ERROR(Bucket, - "Bad memory access in BinaryFuseFilter with " - "seed {}, retrying", - seedToStr(seed)); - seed[0]++; - } - } - } - } - - CLOG_DEBUG(Bucket, "Indexed {} positions in {}", - mData.keysToOffset.size(), filename.filename()); - ZoneValue(static_cast(count)); - } - - if (bm.getConfig().BUCKETLIST_DB_PERSIST_INDEX) - { - saveToDisk(bm, hash, ctx); - } -} - -// Individual indexes are associated with small buckets, so it's more efficient -// to just always recreate them instead of serializing to disk -template <> -void -BucketIndexImpl::saveToDisk( - BucketManager& bm, Hash const& hash, asio::io_context& ctx) const -{ -} - -template <> -void -BucketIndexImpl::saveToDisk( - BucketManager& bm, Hash const& hash, asio::io_context& ctx) const -{ - ZoneScoped; - releaseAssert(bm.getConfig().BUCKETLIST_DB_PERSIST_INDEX); - auto timer = - LogSlowExecution("Saving index", LogSlowExecution::Mode::AUTOMATIC_RAII, - "took", std::chrono::milliseconds(100)); - - std::filesystem::path tmpFilename = - BucketBase::randomBucketIndexName(bm.getTmpDir()); - CLOG_DEBUG(Bucket, "Saving bucket index for {}: {}", hexAbbrev(hash), - tmpFilename); - - { - OutputFileStream out(ctx, !bm.getConfig().DISABLE_XDR_FSYNC); - out.open(tmpFilename.string()); - cereal::BufferedAsioOutputArchive ar(out); - ar(mData); - } - - std::filesystem::path canonicalName = bm.bucketIndexFilename(hash); - CLOG_DEBUG(Bucket, "Adopting bucket index file {} as {}", tmpFilename, - canonicalName); - if (!bm.renameBucketDirFile(tmpFilename, canonicalName)) - { - std::string err("Failed to rename bucket index :"); - err += strerror(errno); - // it seems there is a race condition with external systems - // retry after sleeping for a second works around the problem - std::this_thread::sleep_for(std::chrono::seconds(1)); - if (!bm.renameBucketDirFile(tmpFilename, canonicalName)) - { - // if rename fails again, surface the original error - throw std::runtime_error(err); - } - } -} - -template -template -BucketIndexImpl::BucketIndexImpl(BucketManager const& bm, Archive& ar, - std::streamoff pageSize) - : mBloomMissMeter(bm.getBloomMissMeter()) - , mBloomLookupMeter(bm.getBloomLookupMeter()) -{ - mData.pageSize = pageSize; - ar(mData); -} - -// Returns true if the key is not contained within the given IndexEntry. -// Range index: check if key is outside range of indexEntry -// Individual index: check if key does not match indexEntry key -template -static bool -keyNotInIndexEntry(LedgerKey const& key, IndexEntryT const& indexEntry) -{ - if constexpr (std::is_same_v) - { - return key < indexEntry.lowerBound || indexEntry.upperBound < key; - } - else - { - static_assert(std::is_same_v, - "unexpected index entry type"); - return !(key == indexEntry); - } -} - -// std::lower_bound predicate. Returns true if index comes "before" key and does -// not contain it -// If key is too small for indexEntry bounds: return false -// If key is contained within indexEntry bounds: return false -// If key is too large for indexEntry bounds: return true -template -static bool -lower_bound_pred(IndexEntryT const& indexEntry, LedgerKey const& key) -{ - if constexpr (std::is_same_v) - { - return indexEntry.first.upperBound < key; - } - else - { - static_assert(std::is_same_v, - "unexpected index entry type"); - return indexEntry.first < key; - } -} - -// std::upper_bound predicate. Returns true if key comes "before" and is not -// contained within the indexEntry. -// If key is too small for indexEntry bounds: return true -// If key is contained within indexEntry bounds: return false -// If key is too large for indexEntry bounds: return false -template -static bool -upper_bound_pred(LedgerKey const& key, IndexEntryT const& indexEntry) -{ - if constexpr (std::is_same_v) - { - return key < indexEntry.first.lowerBound; - } - else - { - static_assert(std::is_same_v, - "unexpected index entry type"); - return key < indexEntry.first; - } -} - -template -std::unique_ptr -BucketIndex::createIndex(BucketManager& bm, - std::filesystem::path const& filename, - Hash const& hash, asio::io_context& ctx) -{ - BUCKET_TYPE_ASSERT(BucketT); - - ZoneScoped; - auto const& cfg = bm.getConfig(); - releaseAssertOrThrow(!filename.empty()); - auto pageSize = effectivePageSize(cfg, fs::size(filename.string())); - - try - { - if (pageSize == 0) - { - CLOG_DEBUG(Bucket, - "BucketIndex::createIndex() indexing individual keys in " - "bucket {}", - filename); - return std::unique_ptr const>( - new BucketIndexImpl( - bm, filename, 0, hash, ctx, typename BucketT::EntryT{})); - } - else - { - CLOG_DEBUG(Bucket, - "BucketIndex::createIndex() indexing key range with " - "page size " - "{} in bucket {}", - pageSize, filename); - return std::unique_ptr const>( - new BucketIndexImpl(bm, filename, pageSize, hash, - ctx, - typename BucketT::EntryT{})); - } - } - // BucketIndexImpl throws if BucketManager shuts down before index finishes, - // so return empty index instead of partial index - catch (std::runtime_error&) - { - return {}; - } -} - -std::unique_ptr -BucketIndex::load(BucketManager const& bm, - std::filesystem::path const& filename, size_t bucketFileSize) -{ - std::ifstream in(filename, std::ios::binary); - if (!in) - { - throw std::runtime_error( - fmt::format(FMT_STRING("Error opening file {}"), filename)); - } - - std::streamoff pageSize; - uint32_t version; - cereal::BinaryInputArchive ar(in); - ar(version, pageSize); - - // Make sure on-disk index was built with correct version and config - // parameters before deserializing whole file - if (version != BUCKET_INDEX_VERSION || - pageSize != effectivePageSize(bm.getConfig(), bucketFileSize)) - { - return {}; - } - - if (pageSize == 0) - { - return std::unique_ptr const>( - new BucketIndexImpl(bm, ar, pageSize)); - } - else - { - return std::unique_ptr const>( - new BucketIndexImpl(bm, ar, pageSize)); - } -} - -template -std::optional -BucketIndexImpl::lookup(LedgerKey const& k) const -{ - ZoneScoped; - return scan(begin(), k).first; -} - -template -std::pair, BucketIndex::Iterator> -BucketIndexImpl::scan(Iterator start, LedgerKey const& k) const -{ - ZoneScoped; - ZoneValue(static_cast(mData.keysToOffset.size())); - - // Search for the key in the index before checking the bloom filter so we - // return the correct iterator to the caller. This may be slightly less - // effecient then checking the bloom filter first, but the filter's primary - // purpose is to avoid disk lookups, not to avoid in-memory index search. - auto internalStart = std::get(start); - auto keyIter = - std::lower_bound(internalStart, mData.keysToOffset.end(), k, - lower_bound_pred); - - // If the key is not in the bloom filter or in the lower bounded index - // entry, return nullopt - markBloomLookup(); - if ((mData.filter && !mData.filter->contains(k)) || - keyIter == mData.keysToOffset.end() || - keyNotInIndexEntry(k, keyIter->first)) - { - return {std::nullopt, keyIter}; - } - else - { - return {keyIter->second, keyIter}; - } -} - -template -std::optional> -BucketIndexImpl::getOffsetBounds(LedgerKey const& lowerBound, - LedgerKey const& upperBound) const -{ - // Get the index iterators for the bounds - auto startIter = std::lower_bound( - mData.keysToOffset.begin(), mData.keysToOffset.end(), lowerBound, - lower_bound_pred); - if (startIter == mData.keysToOffset.end()) - { - return std::nullopt; - } - - auto endIter = std::upper_bound( - std::next(startIter), mData.keysToOffset.end(), upperBound, - upper_bound_pred); - - // Get file offsets based on lower and upper bound iterators - std::streamoff startOff = startIter->second; - std::streamoff endOff = std::numeric_limits::max(); - - // If we hit the end of the index then upper bound should be EOF - if (endIter != mData.keysToOffset.end()) - { - endOff = endIter->second; - } - - return std::make_pair(startOff, endOff); -} - -template -std::vector const& -BucketIndexImpl::getPoolIDsByAsset(Asset const& asset) const -{ - static const std::vector emptyVec = {}; - - auto iter = mData.assetToPoolID.find(asset); - if (iter == mData.assetToPoolID.end()) - { - return emptyVec; - } - - return iter->second; -} - -template -std::optional> -BucketIndexImpl::getPoolshareTrustlineRange( - AccountID const& accountID) const -{ - // Get the smallest and largest possible trustline keys for the given - // accountID - auto upperBound = getDummyPoolShareTrustlineKey( - accountID, std::numeric_limits::max()); - auto lowerBound = getDummyPoolShareTrustlineKey( - accountID, std::numeric_limits::min()); - - return getOffsetBounds(lowerBound, upperBound); -} - -template -std::optional> -BucketIndexImpl::getOfferRange() const -{ - // Get the smallest and largest possible offer keys - LedgerKey upperBound(OFFER); - upperBound.offer().sellerID.ed25519().fill( - std::numeric_limits::max()); - upperBound.offer().offerID = std::numeric_limits::max(); - - LedgerKey lowerBound(OFFER); - lowerBound.offer().sellerID.ed25519().fill( - std::numeric_limits::min()); - lowerBound.offer().offerID = std::numeric_limits::min(); - - return getOffsetBounds(lowerBound, upperBound); -} - -#ifdef BUILD_TESTS -template -bool -BucketIndexImpl::operator==(BucketIndex const& inRaw) const -{ - if (getPageSize() != inRaw.getPageSize()) - { - return false; - } - - auto const& in = dynamic_cast const&>(inRaw); - if (mData.keysToOffset.size() != in.mData.keysToOffset.size()) - { - return false; - } - - if constexpr (std::is_same_v) - { - // If both indexes have a filter, check if they are equal - if (mData.filter && in.mData.filter) - { - if (!(*(mData.filter) == *(in.mData.filter))) - { - return false; - } - } - else - { - // If both indexes don't fave a filter, check that each filter is - // null - if (mData.filter || in.mData.filter) - { - return false; - } - } - } - else - { - static_assert(std::is_same_v, - "unexpected index type"); - releaseAssert(!mData.filter); - releaseAssert(!in.mData.filter); - } - - for (size_t i = 0; i < mData.keysToOffset.size(); ++i) - { - auto const& lhsPair = mData.keysToOffset[i]; - auto const& rhsPair = in.mData.keysToOffset[i]; - if (!(lhsPair == rhsPair)) - { - return false; - } - } - - if (mData.counters != in.mData.counters) - { - return false; - } - - return true; -} -#endif - -template -void -BucketIndexImpl::markBloomMiss() const -{ -} - -template <> -void -BucketIndexImpl::markBloomMiss() const -{ - mBloomMissMeter.Mark(); -} - -template -void -BucketIndexImpl::markBloomLookup() const -{ -} - -template <> -void -BucketIndexImpl::markBloomLookup() const -{ - mBloomLookupMeter.Mark(); -} - -template -BucketEntryCounters const& -BucketIndexImpl::getBucketEntryCounters() const -{ - return mData.counters; -} - -template std::unique_ptr -BucketIndex::createIndex(BucketManager& bm, - std::filesystem::path const& filename, - Hash const& hash, asio::io_context& ctx); -template std::unique_ptr -BucketIndex::createIndex( - BucketManager& bm, std::filesystem::path const& filename, Hash const& hash, - asio::io_context& ctx); -} diff --git a/src/bucket/BucketIndexImpl.h b/src/bucket/BucketIndexImpl.h deleted file mode 100644 index 52630a70e6..0000000000 --- a/src/bucket/BucketIndexImpl.h +++ /dev/null @@ -1,129 +0,0 @@ -#pragma once - -// Copyright 2023 Stellar Development Foundation and contributors. Licensed -// under the Apache License, Version 2.0. See the COPYING file at the root -// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 - -#include "bucket/BucketIndex.h" -#include "bucket/LiveBucket.h" -#include "medida/meter.h" -#include "util/BinaryFuseFilter.h" -#include "xdr/Stellar-types.h" - -#include "util/BufferedAsioCerealOutputArchive.h" -#include -#include -#include - -namespace stellar -{ -// Index maps either individual keys or a key range of BucketEntry's to the -// associated offset within the bucket file. Index stored as vector of pairs: -// First: LedgerKey/Key ranges sorted in the same scheme as LedgerEntryCmp -// Second: offset into the bucket file for a given key/ key range. -// pageSize determines how large, in bytes, each range should be. pageSize == 0 -// indicates individual keys used instead of ranges. -template class BucketIndexImpl : public BucketIndex -{ - // Cereal doesn't like templated classes that derive from pure-virtual - // interfaces, so we serialize this inner struct that is not polymorphic - // instead of the actual BucketIndexImpl class - struct SerializableBucketIndex - { - IndexT keysToOffset{}; - std::streamoff pageSize{}; - std::unique_ptr filter{}; - std::map> assetToPoolID{}; - BucketEntryCounters counters{}; - - template - void - save(Archive& ar) const - { - auto version = BUCKET_INDEX_VERSION; - ar(version, pageSize, assetToPoolID, keysToOffset, filter, - counters); - } - - // Note: version and pageSize must be loaded before this function is - // called. pageSize determines template type, so pageSize should be - // loaded, checked, and then call this function with the appropriate - // template type - template - void - load(Archive& ar) - { - ar(assetToPoolID, keysToOffset, filter, counters); - } - } mData; - - medida::Meter& mBloomMissMeter; - medida::Meter& mBloomLookupMeter; - - // Templated constructors are valid C++, but since this is a templated class - // already, there's no way for the compiler to deduce the type without a - // templated parameter, hence the tag - template - BucketIndexImpl(BucketManager& bm, std::filesystem::path const& filename, - std::streamoff pageSize, Hash const& hash, - asio::io_context& ctx, BucketEntryT const& typeTag); - - template - BucketIndexImpl(BucketManager const& bm, Archive& ar, - std::streamoff pageSize); - - // Saves index to disk, overwriting any preexisting file for this index - void saveToDisk(BucketManager& bm, Hash const& hash, - asio::io_context& ctx) const; - - // Returns [lowFileOffset, highFileOffset) that contain the key ranges - // [lowerBound, upperBound]. If no file offsets exist, returns [0, 0] - std::optional> - getOffsetBounds(LedgerKey const& lowerBound, - LedgerKey const& upperBound) const; - - friend BucketIndex; - - public: - virtual std::optional - lookup(LedgerKey const& k) const override; - - virtual std::pair, Iterator> - scan(Iterator start, LedgerKey const& k) const override; - - virtual std::optional> - getPoolshareTrustlineRange(AccountID const& accountID) const override; - - virtual std::vector const& - getPoolIDsByAsset(Asset const& asset) const override; - - virtual std::optional> - getOfferRange() const override; - - virtual std::streamoff - getPageSize() const override - { - return mData.pageSize; - } - - virtual Iterator - begin() const override - { - return mData.keysToOffset.begin(); - } - - virtual Iterator - end() const override - { - return mData.keysToOffset.end(); - } - - virtual void markBloomMiss() const override; - virtual void markBloomLookup() const override; - virtual BucketEntryCounters const& getBucketEntryCounters() const override; - -#ifdef BUILD_TESTS - virtual bool operator==(BucketIndex const& inRaw) const override; -#endif -}; -} \ No newline at end of file diff --git a/src/bucket/BucketIndexUtils.cpp b/src/bucket/BucketIndexUtils.cpp new file mode 100644 index 0000000000..7ca30eaf54 --- /dev/null +++ b/src/bucket/BucketIndexUtils.cpp @@ -0,0 +1,104 @@ +// Copyright 2025 Stellar Development Foundation and contributors. Licensed +// under the Apache License, Version 2.0. See the COPYING file at the root +// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 + +#include "bucket/BucketIndexUtils.h" +#include "bucket/BucketManager.h" +#include "bucket/DiskIndex.h" +#include "bucket/HotArchiveBucket.h" +#include "bucket/HotArchiveBucketIndex.h" +#include "bucket/LiveBucket.h" +#include "bucket/LiveBucketIndex.h" +#include "main/Config.h" +#include "util/Fs.h" +#include + +namespace stellar +{ + +std::streamoff +getPageSizeFromConfig(Config const& cfg) +{ + if (cfg.BUCKETLIST_DB_INDEX_PAGE_SIZE_EXPONENT == 0) + { + return 0; + } + + return 1UL << cfg.BUCKETLIST_DB_INDEX_PAGE_SIZE_EXPONENT; +} + +template +std::unique_ptr +createIndex(BucketManager& bm, std::filesystem::path const& filename, + Hash const& hash, asio::io_context& ctx) +{ + BUCKET_TYPE_ASSERT(BucketT); + + ZoneScoped; + releaseAssertOrThrow(!filename.empty()); + + try + { + return std::unique_ptr( + new typename BucketT::IndexT(bm, filename, hash, ctx)); + } + // BucketIndex throws if BucketManager shuts down before index finishes, + // so return empty index instead of partial index + catch (std::runtime_error&) + { + return {}; + } +} + +template +std::unique_ptr +loadIndex(BucketManager const& bm, std::filesystem::path const& filename, + std::size_t fileSize) +{ + std::ifstream in(filename, std::ios::binary); + if (!in) + { + throw std::runtime_error(fmt::format( + FMT_STRING("Error opening file {}"), filename.string())); + } + + std::streamoff pageSize; + uint32_t version; + cereal::BinaryInputArchive ar(in); + DiskIndex::preLoad(ar, version, pageSize); + + // Page size based on current settings. These may have changed since the + // on-disk index was serialized. + auto expectedPageSize = + BucketT::IndexT::getPageSize(bm.getConfig(), fileSize); + + // Make sure on-disk index was built with correct version and config + // parameters before deserializing whole file + if (version != BucketT::IndexT::BUCKET_INDEX_VERSION || + pageSize != expectedPageSize) + { + return {}; + } + + return std::unique_ptr( + new typename BucketT::IndexT(bm, ar, pageSize)); +} + +template std::unique_ptr +createIndex(BucketManager& bm, + std::filesystem::path const& filename, Hash const& hash, + asio::io_context& ctx); +template std::unique_ptr +createIndex(BucketManager& bm, + std::filesystem::path const& filename, + Hash const& hash, asio::io_context& ctx); + +template std::unique_ptr +loadIndex(BucketManager const& bm, + std::filesystem::path const& filename, + std::size_t fileSize); +template std::unique_ptr +loadIndex(BucketManager const& bm, + std::filesystem::path const& filename, + std::size_t fileSize); +} diff --git a/src/bucket/BucketIndexUtils.h b/src/bucket/BucketIndexUtils.h new file mode 100644 index 0000000000..7d8087eae6 --- /dev/null +++ b/src/bucket/BucketIndexUtils.h @@ -0,0 +1,111 @@ +#pragma once + +// Copyright 2025 Stellar Development Foundation and contributors. Licensed +// under the Apache License, Version 2.0. See the COPYING file at the root +// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 + +#include "util/GlobalChecks.h" +#include "util/XDROperators.h" // IWYU pragma: keep +#include "xdr/Stellar-ledger-entries.h" +#include +#include +#include +#include +#include + +#include "util/XDRCereal.h" +#include +#include +#include + +namespace asio +{ +class io_context; +} + +namespace stellar +{ + +class BucketManager; +class Config; + +using AssetPoolIDMap = std::map>; +using IndexPtrT = std::shared_ptr; + +// Querying a BucketIndex can return one of three states: +// 1. CACHE_HIT: The entry is in the cache. Can either be a live or dead entry. +// 2. FILE_OFFSET: The entry is not in the cache, but the entry potentially +// exists at the given offset. +// 3. NOT_FOUND: The entry does not exist in the bucket. +enum IndexReturnState +{ + CACHE_HIT, + FILE_OFFSET, + NOT_FOUND +}; + +class IndexReturnT +{ + private: + // Payload maps to the possible return states: + // CACHE_HIT: IndexPtrT + // FILE_OFFSET: std::streamoff + // NOT_FOUND: std::monostate + using PayloadT = std::variant; + PayloadT mPayload; + IndexReturnState mState; + + public: + IndexReturnT(IndexPtrT entry) + : mPayload(entry), mState(IndexReturnState::CACHE_HIT) + { + releaseAssertOrThrow(entry); + } + IndexReturnT(std::streamoff offset) + : mPayload(offset), mState(IndexReturnState::FILE_OFFSET) + { + } + IndexReturnT() + : mPayload(std::monostate{}), mState(IndexReturnState::NOT_FOUND) + { + } + + IndexReturnState + getState() const + { + return mState; + } + IndexPtrT + cacheHit() const + { + releaseAssertOrThrow(mState == IndexReturnState::CACHE_HIT); + releaseAssertOrThrow(std::holds_alternative(mPayload)); + return std::get(mPayload); + } + std::streamoff + fileOffset() const + { + releaseAssertOrThrow(mState == IndexReturnState::FILE_OFFSET); + releaseAssertOrThrow(std::holds_alternative(mPayload)); + return std::get(mPayload); + } +}; + +// Returns pagesize, in bytes, from BUCKETLIST_DB_INDEX_CUTOFF config param +std::streamoff getPageSizeFromConfig(Config const& cfg); + +// Builds index for given bucketfile. This is expensive (> 20 seconds +// for the largest buckets) and should only be called once. Constructs a +// DiskIndex or InMemoryIndex depending on config and Bucket size. +template +std::unique_ptr +createIndex(BucketManager& bm, std::filesystem::path const& filename, + Hash const& hash, asio::io_context& ctx); + +// Loads index from given file. If file does not exist or if saved +// index does not have expected version or pageSize, return null +template +std::unique_ptr +loadIndex(BucketManager const& bm, std::filesystem::path const& filename, + std::size_t fileSize); +} diff --git a/src/bucket/BucketListSnapshotBase.cpp b/src/bucket/BucketListSnapshotBase.cpp index cf2504fcfb..549a52d20e 100644 --- a/src/bucket/BucketListSnapshotBase.cpp +++ b/src/bucket/BucketListSnapshotBase.cpp @@ -7,8 +7,10 @@ #include "bucket/LiveBucket.h" #include "crypto/SecretKey.h" // IWYU pragma: keep #include "ledger/LedgerTxn.h" +#include "main/AppConnector.h" #include "util/GlobalChecks.h" +#include #include #include @@ -81,18 +83,25 @@ SearchableBucketListSnapshotBase::loopAllBuckets( } template -std::shared_ptr +std::shared_ptr SearchableBucketListSnapshotBase::load(LedgerKey const& k) const { ZoneScoped; - std::shared_ptr result{}; - auto sawBloomMiss = false; + std::shared_ptr result{}; + auto timerIter = mPointTimers.find(k.type()); + releaseAssert(timerIter != mPointTimers.end()); + auto timer = timerIter->second.TimeScope(); // Search function called on each Bucket in BucketList until we find the key auto loadKeyBucketLoop = [&](auto const& b) { auto [be, bloomMiss] = b.getBucketEntry(k); - sawBloomMiss = sawBloomMiss || bloomMiss; + if (bloomMiss) + { + // Reset timer on bloom miss to avoid outlier metrics, since we + // really only want to measure disk performance + timer.Reset(); + } if (be) { @@ -105,19 +114,8 @@ SearchableBucketListSnapshotBase::load(LedgerKey const& k) const } }; - if (threadIsMain()) - { - mSnapshotManager.startPointLoadTimer(); - loopAllBuckets(loadKeyBucketLoop, *mSnapshot); - mSnapshotManager.endPointLoadTimer(k.type(), sawBloomMiss); - return result; - } - else - { - // TODO: Background metrics - loopAllBuckets(loadKeyBucketLoop, *mSnapshot); - return result; - } + loopAllBuckets(loadKeyBucketLoop, *mSnapshot); + return result; } template @@ -138,13 +136,29 @@ BucketLevelSnapshot::BucketLevelSnapshot( template SearchableBucketListSnapshotBase::SearchableBucketListSnapshotBase( - BucketSnapshotManager const& snapshotManager, + BucketSnapshotManager const& snapshotManager, AppConnector const& app, SnapshotPtrT&& snapshot, std::map>&& historicalSnapshots) : mSnapshotManager(snapshotManager) , mSnapshot(std::move(snapshot)) , mHistoricalSnapshots(std::move(historicalSnapshots)) + , mAppConnector(app) + , mBulkLoadMeter(app.getMetrics().NewMeter( + {BucketT::METRIC_STRING, "query", "loads"}, "query")) + , mBloomMisses(app.getMetrics().NewMeter( + {BucketT::METRIC_STRING, "bloom", "misses"}, "bloom")) + , mBloomLookups(app.getMetrics().NewMeter( + {BucketT::METRIC_STRING, "bloom", "lookups"}, "bloom")) { + // Initialize point load timers for each LedgerEntry type + for (auto t : xdr::xdr_traits::enum_values()) + { + auto const& label = xdr::xdr_traits::enum_name( + static_cast(t)); + auto& metric = + app.getMetrics().NewTimer({BucketT::METRIC_STRING, "point", label}); + mPointTimers.emplace(static_cast(t), metric); + } } template @@ -152,6 +166,27 @@ SearchableBucketListSnapshotBase::~SearchableBucketListSnapshotBase() { } +template +medida::Timer& +SearchableBucketListSnapshotBase::getBulkLoadTimer( + std::string const& label, size_t numEntries) const +{ + if (numEntries != 0) + { + mBulkLoadMeter.Mark(numEntries); + } + + auto iter = mBulkTimers.find(label); + if (iter == mBulkTimers.end()) + { + auto& metric = mAppConnector.getMetrics().NewTimer( + {BucketT::METRIC_STRING, "bulk", label}); + iter = mBulkTimers.emplace(label, metric).first; + } + + return iter->second; +} + template struct BucketLevelSnapshot; template struct BucketLevelSnapshot; template class BucketListSnapshot; diff --git a/src/bucket/BucketListSnapshotBase.h b/src/bucket/BucketListSnapshotBase.h index c5e6245b75..8b8ce68b6b 100644 --- a/src/bucket/BucketListSnapshotBase.h +++ b/src/bucket/BucketListSnapshotBase.h @@ -89,6 +89,15 @@ class SearchableBucketListSnapshotBase : public NonMovableOrCopyable // Snapshot managed by SnapshotManager SnapshotPtrT mSnapshot{}; std::map> mHistoricalSnapshots; + AppConnector const& mAppConnector; + + // Metrics + UnorderedMap mPointTimers{}; + mutable UnorderedMap mBulkTimers{}; + + medida::Meter& mBulkLoadMeter; + medida::Meter& mBloomMisses; + medida::Meter& mBloomLookups; // Loops through all buckets, starting with curr at level 0, then snap at // level 0, etc. Calls f on each bucket. Exits early if function @@ -98,7 +107,7 @@ class SearchableBucketListSnapshotBase : public NonMovableOrCopyable SearchableBucketListSnapshotBase( BucketSnapshotManager const& snapshotManager, - SnapshotPtrT&& snapshot, + AppConnector const& appConnector, SnapshotPtrT&& snapshot, std::map>&& historicalSnapshots); std::optional> @@ -106,6 +115,9 @@ class SearchableBucketListSnapshotBase : public NonMovableOrCopyable LedgerKeyMeter* lkMeter, std::optional ledgerSeq) const; + medida::Timer& getBulkLoadTimer(std::string const& label, + size_t numEntries) const; + public: uint32_t getLedgerSeq() const @@ -125,6 +137,7 @@ class SearchableBucketListSnapshotBase : public NonMovableOrCopyable loadKeysFromLedger(std::set const& inKeys, uint32_t ledgerSeq) const; - std::shared_ptr load(LedgerKey const& k) const; + std::shared_ptr + load(LedgerKey const& k) const; }; } \ No newline at end of file diff --git a/src/bucket/BucketManager.cpp b/src/bucket/BucketManager.cpp index 358745e50c..4c5cdb8d58 100644 --- a/src/bucket/BucketManager.cpp +++ b/src/bucket/BucketManager.cpp @@ -153,10 +153,6 @@ BucketManager::BucketManager(Application& app) , mBucketSnapMerge(app.getMetrics().NewTimer({"bucket", "snap", "merge"})) , mSharedBucketsSize( app.getMetrics().NewCounter({"bucket", "memory", "shared"})) - , mBucketListDBBloomMisses(app.getMetrics().NewMeter( - {"bucketlistDB", "bloom", "misses"}, "bloom")) - , mBucketListDBBloomLookups(app.getMetrics().NewMeter( - {"bucketlistDB", "bloom", "lookups"}, "bloom")) , mLiveBucketListSizeCounter( app.getMetrics().NewCounter({"bucketlist", "size", "bytes"})) , mArchiveBucketListSizeCounter( @@ -330,6 +326,24 @@ BucketManager::getMergeTimer() return mBucketSnapMerge; } +template +medida::Meter& +BucketManager::getBloomMissMeter() const +{ + BUCKET_TYPE_ASSERT(BucketT); + return mApp.getMetrics().NewMeter( + {BucketT::METRIC_STRING, "bloom", "misses"}, "bloom"); +} + +template +medida::Meter& +BucketManager::getBloomLookupMeter() const +{ + BUCKET_TYPE_ASSERT(BucketT); + return mApp.getMetrics().NewMeter( + {BucketT::METRIC_STRING, "bloom", "lookups"}, "bloom"); +} + MergeCounters BucketManager::readMergeCounters() { @@ -361,9 +375,9 @@ BucketManager::renameBucketDirFile(std::filesystem::path const& src, template <> std::shared_ptr -BucketManager::adoptFileAsBucket(std::string const& filename, - uint256 const& hash, MergeKey* mergeKey, - std::unique_ptr index) +BucketManager::adoptFileAsBucket( + std::string const& filename, uint256 const& hash, MergeKey* mergeKey, + std::unique_ptr index) { return adoptFileAsBucketInternal(filename, hash, mergeKey, std::move(index), mSharedLiveBuckets, mLiveBucketFutures); @@ -371,9 +385,9 @@ BucketManager::adoptFileAsBucket(std::string const& filename, template <> std::shared_ptr -BucketManager::adoptFileAsBucket(std::string const& filename, - uint256 const& hash, MergeKey* mergeKey, - std::unique_ptr index) +BucketManager::adoptFileAsBucket( + std::string const& filename, uint256 const& hash, MergeKey* mergeKey, + std::unique_ptr index) { return adoptFileAsBucketInternal(filename, hash, mergeKey, std::move(index), mSharedHotArchiveBuckets, @@ -384,8 +398,8 @@ template std::shared_ptr BucketManager::adoptFileAsBucketInternal( std::string const& filename, uint256 const& hash, MergeKey* mergeKey, - std::unique_ptr index, BucketMapT& bucketMap, - FutureMapT& futureMap) + std::unique_ptr index, + BucketMapT& bucketMap, FutureMapT& futureMap) { BUCKET_TYPE_ASSERT(BucketT); ZoneScoped; @@ -952,7 +966,7 @@ BucketManager::addHotArchiveBatch( releaseAssertOrThrow(app.getConfig().MODE_ENABLES_BUCKETLIST); releaseAssertOrThrow(protocolVersionStartsFrom( header.ledgerVersion, - BucketBase::FIRST_PROTOCOL_SUPPORTING_PERSISTENT_EVICTION)); + HotArchiveBucket::FIRST_PROTOCOL_SUPPORTING_PERSISTENT_EVICTION)); #ifdef BUILD_TESTS if (mUseFakeTestValuesForNextClose) { @@ -1011,7 +1025,8 @@ BucketManager::snapshotLedger(LedgerHeader& currentHeader) { if (protocolVersionStartsFrom( currentHeader.ledgerVersion, - BucketBase::FIRST_PROTOCOL_SUPPORTING_PERSISTENT_EVICTION)) + HotArchiveBucket:: + FIRST_PROTOCOL_SUPPORTING_PERSISTENT_EVICTION)) { // TODO: Hash Archive Bucket // Dependency: HAS supports Hot Archive BucketList @@ -1036,9 +1051,11 @@ BucketManager::snapshotLedger(LedgerHeader& currentHeader) calculateSkipValues(currentHeader); } +template void -BucketManager::maybeSetIndex(std::shared_ptr b, - std::unique_ptr&& index) +BucketManager::maybeSetIndex( + std::shared_ptr b, + std::unique_ptr&& index) { ZoneScoped; @@ -1168,18 +1185,6 @@ BucketManager::resolveBackgroundEvictionScan( return EvictedStateVectors{deletedKeys, archivedEntries}; } -medida::Meter& -BucketManager::getBloomMissMeter() const -{ - return mBucketListDBBloomMisses; -} - -medida::Meter& -BucketManager::getBloomLookupMeter() const -{ - return mBucketListDBBloomLookups; -} - void BucketManager::calculateSkipValues(LedgerHeader& currentHeader) { @@ -1634,4 +1639,17 @@ BucketManager::reportBucketEntryCountMetrics() bucketEntryCounters.entryTypeSizes.at(type)); } } + +template void BucketManager::maybeSetIndex( + std::shared_ptr b, + std::unique_ptr&& index); +template void BucketManager::maybeSetIndex( + std::shared_ptr b, + std::unique_ptr&& index); +template medida::Meter& BucketManager::getBloomMissMeter() const; +template medida::Meter& BucketManager::getBloomLookupMeter() const; +template medida::Meter& +BucketManager::getBloomMissMeter() const; +template medida::Meter& +BucketManager::getBloomLookupMeter() const; } diff --git a/src/bucket/BucketManager.h b/src/bucket/BucketManager.h index 38df819a81..15551a7720 100644 --- a/src/bucket/BucketManager.h +++ b/src/bucket/BucketManager.h @@ -2,7 +2,9 @@ #include "bucket/BucketMergeMap.h" #include "main/Config.h" +#include "util/TmpDir.h" #include "util/types.h" +#include "work/BasicWork.h" #include "xdr/Stellar-ledger.h" #include @@ -32,8 +34,6 @@ class Application; class Bucket; class LiveBucketList; class HotArchiveBucketList; -class BucketBase; -class BucketIndex; class BucketSnapshotManager; class SearchableLiveBucketListSnapshot; struct BucketEntryCounters; @@ -98,8 +98,6 @@ class BucketManager : NonMovableOrCopyable medida::Timer& mBucketAddArchiveBatch; medida::Timer& mBucketSnapMerge; medida::Counter& mSharedBucketsSize; - medida::Meter& mBucketListDBBloomMisses; - medida::Meter& mBucketListDBBloomLookups; medida::Counter& mLiveBucketListSizeCounter; medida::Counter& mArchiveBucketListSizeCounter; EvictionCounters mBucketListEvictionCounters; @@ -136,16 +134,12 @@ class BucketManager : NonMovableOrCopyable void deleteTmpDirAndUnlockBucketDir(); void deleteEntireBucketDir(); - medida::Timer& recordBulkLoadMetrics(std::string const& label, - size_t numEntries) const; - medida::Timer& getPointLoadTimer(LedgerEntryType t) const; - void updateSharedBucketSize(); template std::shared_ptr adoptFileAsBucketInternal( std::string const& filename, uint256 const& hash, MergeKey* mergeKey, - std::unique_ptr index, + std::unique_ptr index, BucketMapT& bucketMap, FutureMapT& futureMap); template @@ -201,6 +195,9 @@ class BucketManager : NonMovableOrCopyable medida::Timer& getMergeTimer(); + template medida::Meter& getBloomMissMeter() const; + template medida::Meter& getBloomLookupMeter() const; + // Reading and writing the merge counters is done in bulk, and takes a lock // briefly; this can be done from any thread. MergeCounters readMergeCounters(); @@ -221,7 +218,7 @@ class BucketManager : NonMovableOrCopyable std::shared_ptr adoptFileAsBucket(std::string const& filename, uint256 const& hash, MergeKey* mergeKey, - std::unique_ptr index); + std::unique_ptr index); // Companion method to `adoptFileAsLiveBucket` also called from the // `BucketOutputIterator::getBucket` merge-completion path. This method @@ -292,8 +289,9 @@ class BucketManager : NonMovableOrCopyable // for each bucket. However, during startup there are race conditions where // a bucket may be indexed twice. If there is an index race, set index with // this function, otherwise use BucketBase::setIndex(). - void maybeSetIndex(std::shared_ptr b, - std::unique_ptr&& index); + template + void maybeSetIndex(std::shared_ptr b, + std::unique_ptr&& index); // Scans BucketList for non-live entries to evict starting at the entry // pointed to by EvictionIterator. Evicts until `maxEntriesToEvict` entries diff --git a/src/bucket/BucketOutputIterator.cpp b/src/bucket/BucketOutputIterator.cpp index 17bc9ca494..cb24ef519f 100644 --- a/src/bucket/BucketOutputIterator.cpp +++ b/src/bucket/BucketOutputIterator.cpp @@ -3,7 +3,7 @@ // of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 #include "bucket/BucketOutputIterator.h" -#include "bucket/BucketIndex.h" +#include "bucket/BucketIndexUtils.h" #include "bucket/BucketManager.h" #include "bucket/HotArchiveBucket.h" #include "bucket/LiveBucket.h" @@ -27,7 +27,7 @@ BucketOutputIterator::BucketOutputIterator(std::string const& tmpDir, MergeCounters& mc, asio::io_context& ctx, bool doFsync) - : mFilename(BucketBase::randomBucketName(tmpDir)) + : mFilename(BucketT::randomBucketName(tmpDir)) , mOut(ctx, doFsync) , mCtx(ctx) , mBuf(nullptr) @@ -59,7 +59,7 @@ BucketOutputIterator::BucketOutputIterator(std::string const& tmpDir, "unexpected bucket type"); releaseAssertOrThrow(protocolVersionStartsFrom( meta.ledgerVersion, - BucketBase::FIRST_PROTOCOL_SUPPORTING_PERSISTENT_EVICTION)); + BucketT::FIRST_PROTOCOL_SUPPORTING_PERSISTENT_EVICTION)); HotArchiveBucketEntry bme; bme.type(HOT_ARCHIVE_METAENTRY); @@ -191,15 +191,14 @@ BucketOutputIterator::getBucket(BucketManager& bucketManager, } auto hash = mHasher.finish(); - std::unique_ptr index{}; + std::unique_ptr index{}; // either it's a new bucket or we just reconstructed a bucket // we already have, in any case ensure we have an index if (auto b = bucketManager.getBucketIfExists(hash); !b || !b->isIndexed()) { - index = BucketIndex::createIndex(bucketManager, mFilename, - hash, mCtx); + index = createIndex(bucketManager, mFilename, hash, mCtx); } return bucketManager.adoptFileAsBucket(mFilename.string(), hash, diff --git a/src/bucket/BucketSnapshot.cpp b/src/bucket/BucketSnapshot.cpp index 05c43f31a3..05b120ca92 100644 --- a/src/bucket/BucketSnapshot.cpp +++ b/src/bucket/BucketSnapshot.cpp @@ -3,7 +3,6 @@ // of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 #include "bucket/BucketSnapshot.h" -#include "bucket/BucketIndex.h" #include "bucket/HotArchiveBucket.h" #include "bucket/LiveBucket.h" #include "bucket/SearchableBucketList.h" @@ -67,13 +66,12 @@ BucketSnapshotBase::getEntryAtOffset(LedgerKey const& k, return {std::make_shared(be), false}; } - // Mark entry miss for metrics mBucket->getIndex().markBloomMiss(); return {nullptr, true}; } template -std::pair, bool> +std::pair, bool> BucketSnapshotBase::getBucketEntry(LedgerKey const& k) const { ZoneScoped; @@ -82,14 +80,24 @@ BucketSnapshotBase::getBucketEntry(LedgerKey const& k) const return {nullptr, false}; } - auto pos = mBucket->getIndex().lookup(k); - if (pos.has_value()) + auto indexRes = mBucket->getIndex().lookup(k); + switch (indexRes.getState()) { - return getEntryAtOffset(k, pos.value(), + case IndexReturnState::CACHE_HIT: + if constexpr (std::is_same_v) + { + return {indexRes.cacheHit(), false}; + } + else + { + throw std::runtime_error("Hot Archive reported cache hit"); + } + case IndexReturnState::FILE_OFFSET: + return getEntryAtOffset(k, indexRes.fileOffset(), mBucket->getIndex().getPageSize()); + case IndexReturnState::NOT_FOUND: + return {nullptr, false}; } - - return {nullptr, false}; } // When searching for an entry, BucketList calls this function on every bucket. @@ -114,51 +122,74 @@ BucketSnapshotBase::loadKeys( auto indexIter = index.begin(); while (currKeyIt != keys.end() && indexIter != index.end()) { - auto [offOp, newIndexIter] = index.scan(indexIter, *currKeyIt); + // Scan for current key. Iterator returned is the lower_bound of the key + // which will be our starting point for search for the next key + auto [indexRes, newIndexIter] = index.scan(indexIter, *currKeyIt); indexIter = newIndexIter; - if (offOp) + + // Check if the index actually found the key + std::shared_ptr entryOp; + switch (indexRes.getState()) { - auto [entryOp, bloomMiss] = getEntryAtOffset( - *currKeyIt, *offOp, mBucket->getIndex().getPageSize()); - if (entryOp) + // Index had entry in cache + case IndexReturnState::CACHE_HIT: + if constexpr (std::is_same_v) + { + entryOp = indexRes.cacheHit(); + } + else + { + throw std::runtime_error("Hot Archive reported cache hit"); + } + break; + // Index had entry offset, so we need to load the entry + case IndexReturnState::FILE_OFFSET: + std::tie(entryOp, std::ignore) = + getEntryAtOffset(*currKeyIt, indexRes.fileOffset(), + mBucket->getIndex().getPageSize()); + break; + // Index did not have entry or offset, move on to search for next key + case IndexReturnState::NOT_FOUND: + ++currKeyIt; + continue; + } + + if (entryOp) + { + // Don't return tombstone entries, as these do not exist wrt + // ledger state + if (!BucketT::isTombstoneEntry(*entryOp)) { - // Don't return tombstone entries, as these do not exist wrt - // ledger state - if (!BucketT::isTombstoneEntry(*entryOp)) + // Only live bucket loads can be metered + if constexpr (std::is_same_v) { - // Only live bucket loads can be metered - if constexpr (std::is_same_v) + bool addEntry = true; + if (lkMeter) { - bool addEntry = true; - if (lkMeter) - { - // Here, we are metering after the entry has been - // loaded. This is because we need to know the size - // of the entry to meter it. Future work will add - // metering at the xdr level. - auto entrySize = - xdr::xdr_size(entryOp->liveEntry()); - addEntry = lkMeter->canLoad(*currKeyIt, entrySize); - lkMeter->updateReadQuotasForKey(*currKeyIt, - entrySize); - } - if (addEntry) - { - result.push_back(entryOp->liveEntry()); - } + // Here, we are metering after the entry has been + // loaded. This is because we need to know the size + // of the entry to meter it. Future work will add + // metering at the xdr level. + auto entrySize = xdr::xdr_size(entryOp->liveEntry()); + addEntry = lkMeter->canLoad(*currKeyIt, entrySize); + lkMeter->updateReadQuotasForKey(*currKeyIt, entrySize); } - else + if (addEntry) { - static_assert(std::is_same_v, - "unexpected bucket type"); - result.push_back(*entryOp); + result.push_back(entryOp->liveEntry()); } } - - currKeyIt = keys.erase(currKeyIt); - continue; + else + { + static_assert(std::is_same_v, + "unexpected bucket type"); + result.push_back(*entryOp); + } } + + currKeyIt = keys.erase(currKeyIt); + continue; } ++currKeyIt; @@ -225,7 +256,7 @@ LiveBucketSnapshot::scanForEviction( auto isEvictableType = [ledgerVers](auto const& le) { if (protocolVersionIsBefore( ledgerVers, - BucketBase::FIRST_PROTOCOL_SUPPORTING_PERSISTENT_EVICTION)) + LiveBucket::FIRST_PROTOCOL_SUPPORTING_PERSISTENT_EVICTION)) { return isTemporaryEntry(le); } diff --git a/src/bucket/BucketSnapshot.h b/src/bucket/BucketSnapshot.h index 0fdd6034e6..435a0239d5 100644 --- a/src/bucket/BucketSnapshot.h +++ b/src/bucket/BucketSnapshot.h @@ -57,7 +57,7 @@ template class BucketSnapshotBase : public NonMovable // Loads bucket entry for LedgerKey k. Returns , // where bloomMiss is true if a bloomMiss occurred during the load. - std::pair, bool> + std::pair, bool> getBucketEntry(LedgerKey const& k) const; // Loads LedgerEntry's for given keys. When a key is found, the diff --git a/src/bucket/BucketSnapshotManager.cpp b/src/bucket/BucketSnapshotManager.cpp index aaa85a3e44..9c474db5e4 100644 --- a/src/bucket/BucketSnapshotManager.cpp +++ b/src/bucket/BucketSnapshotManager.cpp @@ -7,13 +7,11 @@ #include "bucket/HotArchiveBucket.h" #include "bucket/LiveBucket.h" #include "bucket/SearchableBucketList.h" +#include "main/AppConnector.h" #include "main/Application.h" #include "util/GlobalChecks.h" #include "util/XDRStream.h" // IWYU pragma: keep -#include "medida/meter.h" -#include "medida/metrics_registry.h" -#include "xdr/Stellar-ledger-entries.h" #include #include @@ -24,32 +22,16 @@ BucketSnapshotManager::BucketSnapshotManager( Application& app, SnapshotPtrT&& snapshot, SnapshotPtrT&& hotArchiveSnapshot, uint32_t numLiveHistoricalSnapshots) - : mApp(app) + : mAppConnector(app) , mCurrLiveSnapshot(std::move(snapshot)) , mCurrHotArchiveSnapshot(std::move(hotArchiveSnapshot)) , mLiveHistoricalSnapshots() , mHotArchiveHistoricalSnapshots() , mNumHistoricalSnapshots(numLiveHistoricalSnapshots) - , mBulkLoadMeter(app.getMetrics().NewMeter( - {"bucketlistDB", "query", "loads"}, "query")) - , mBloomMisses(app.getMetrics().NewMeter( - {"bucketlistDB", "bloom", "misses"}, "bloom")) - , mBloomLookups(app.getMetrics().NewMeter( - {"bucketlistDB", "bloom", "lookups"}, "bloom")) { releaseAssert(threadIsMain()); releaseAssert(mCurrLiveSnapshot); releaseAssert(mCurrHotArchiveSnapshot); - - // Initialize point load timers for each LedgerEntry type - for (auto t : xdr::xdr_traits::enum_values()) - { - auto const& label = xdr::xdr_traits::enum_name( - static_cast(t)); - auto& metric = - mApp.getMetrics().NewTimer({"bucketlistDB", "point", label}); - mPointTimers.emplace(static_cast(t), metric); - } } template @@ -73,7 +55,7 @@ BucketSnapshotManager::copySearchableLiveBucketListSnapshot() const // Can't use std::make_shared due to private constructor return std::shared_ptr( new SearchableLiveBucketListSnapshot( - *this, + *this, mAppConnector, std::make_unique>( *mCurrLiveSnapshot), copyHistoricalSnapshots(mLiveHistoricalSnapshots))); @@ -86,35 +68,12 @@ BucketSnapshotManager::copySearchableHotArchiveBucketListSnapshot() const // Can't use std::make_shared due to private constructor return std::shared_ptr( new SearchableHotArchiveBucketListSnapshot( - *this, + *this, mAppConnector, std::make_unique>( *mCurrHotArchiveSnapshot), copyHistoricalSnapshots(mHotArchiveHistoricalSnapshots))); } -medida::Timer& -BucketSnapshotManager::recordBulkLoadMetrics(std::string const& label, - size_t numEntries) const -{ - // For now, only keep metrics for the main thread. We can decide on what - // metrics make sense when more background services are added later. - - if (numEntries != 0) - { - mBulkLoadMeter.Mark(numEntries); - } - - auto iter = mBulkTimers.find(label); - if (iter == mBulkTimers.end()) - { - auto& metric = - mApp.getMetrics().NewTimer({"bucketlistDB", "bulk", label}); - iter = mBulkTimers.emplace(label, metric).first; - } - - return iter->second; -} - void BucketSnapshotManager::maybeCopySearchableBucketListSnapshot( SearchableSnapshotConstPtr& snapshot) @@ -183,32 +142,4 @@ BucketSnapshotManager::updateCurrentSnapshot( updateSnapshot(mCurrHotArchiveSnapshot, mHotArchiveHistoricalSnapshots, hotArchiveSnapshot); } - -void -BucketSnapshotManager::startPointLoadTimer() const -{ - releaseAssert(threadIsMain()); - releaseAssert(!mTimerStart); - mTimerStart = mApp.getClock().now(); -} - -void -BucketSnapshotManager::endPointLoadTimer(LedgerEntryType t, - bool bloomMiss) const -{ - releaseAssert(threadIsMain()); - releaseAssert(mTimerStart); - auto duration = mApp.getClock().now() - *mTimerStart; - mTimerStart.reset(); - - // We expect about 0.1% of lookups to encounter a bloom miss. To avoid noise - // in disk performance metrics, we only track metrics for entries that did - // not encounter a bloom miss. - if (!bloomMiss) - { - auto iter = mPointTimers.find(t); - releaseAssert(iter != mPointTimers.end()); - iter->second.Update(duration); - } -} } \ No newline at end of file diff --git a/src/bucket/BucketSnapshotManager.h b/src/bucket/BucketSnapshotManager.h index 948a7c0ee0..f14d2309df 100644 --- a/src/bucket/BucketSnapshotManager.h +++ b/src/bucket/BucketSnapshotManager.h @@ -7,8 +7,8 @@ #include "bucket/BucketManager.h" #include "bucket/HotArchiveBucket.h" #include "bucket/LiveBucket.h" +#include "main/AppConnector.h" #include "util/NonCopyable.h" -#include "util/UnorderedMap.h" #include #include @@ -43,7 +43,7 @@ using SearchableHotArchiveSnapshotConstPtr = class BucketSnapshotManager : NonMovableOrCopyable { private: - Application& mApp; + AppConnector mAppConnector; // Snapshot that is maintained and periodically updated by BucketManager on // the main thread. When background threads need to generate or refresh a @@ -61,15 +61,6 @@ class BucketSnapshotManager : NonMovableOrCopyable // Lock must be held when accessing any member variables holding snapshots mutable std::shared_mutex mSnapshotMutex; - mutable UnorderedMap mPointTimers{}; - mutable UnorderedMap mBulkTimers{}; - - medida::Meter& mBulkLoadMeter; - medida::Meter& mBloomMisses; - medida::Meter& mBloomLookups; - - mutable std::optional mTimerStart; - public: // Called by main thread to update snapshots whenever the BucketList // is updated @@ -98,11 +89,5 @@ class BucketSnapshotManager : NonMovableOrCopyable maybeCopySearchableBucketListSnapshot(SearchableSnapshotConstPtr& snapshot); void maybeCopySearchableHotArchiveBucketListSnapshot( SearchableHotArchiveSnapshotConstPtr& snapshot); - - // All metric recording functions must only be called by the main thread - void startPointLoadTimer() const; - void endPointLoadTimer(LedgerEntryType t, bool bloomMiss) const; - medida::Timer& recordBulkLoadMetrics(std::string const& label, - size_t numEntries) const; }; } \ No newline at end of file diff --git a/src/bucket/BucketUtils.cpp b/src/bucket/BucketUtils.cpp index 0647f4064d..9c6ce0d2e9 100644 --- a/src/bucket/BucketUtils.cpp +++ b/src/bucket/BucketUtils.cpp @@ -3,11 +3,53 @@ // of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 #include "bucket/BucketUtils.h" +#include "bucket/HotArchiveBucket.h" +#include "bucket/LiveBucket.h" +#include "ledger/LedgerTypeUtils.h" +#include "main/Application.h" +#include "util/types.h" +#include "xdr/Stellar-ledger-entries.h" +#include +#include #include namespace stellar { +std::string +toString(LedgerEntryTypeAndDurability const type) +{ + switch (type) + { + case LedgerEntryTypeAndDurability::ACCOUNT: + return "ACCOUNT"; + case LedgerEntryTypeAndDurability::TRUSTLINE: + return "TRUSTLINE"; + case LedgerEntryTypeAndDurability::OFFER: + return "OFFER"; + case LedgerEntryTypeAndDurability::DATA: + return "DATA"; + case LedgerEntryTypeAndDurability::CLAIMABLE_BALANCE: + return "CLAIMABLE_BALANCE"; + case LedgerEntryTypeAndDurability::LIQUIDITY_POOL: + return "LIQUIDITY_POOL"; + case LedgerEntryTypeAndDurability::TEMPORARY_CONTRACT_DATA: + return "TEMPORARY_CONTRACT_DATA"; + case LedgerEntryTypeAndDurability::PERSISTENT_CONTRACT_DATA: + return "PERSISTENT_CONTRACT_DATA"; + case LedgerEntryTypeAndDurability::CONTRACT_CODE: + return "CONTRACT_CODE"; + case LedgerEntryTypeAndDurability::CONFIG_SETTING: + return "CONFIG_SETTING"; + case LedgerEntryTypeAndDurability::TTL: + return "TTL"; + default: + throw std::runtime_error( + fmt::format(FMT_STRING("unknown LedgerEntryTypeAndDurability {:d}"), + static_cast(type))); + } +} + MergeCounters& MergeCounters::operator+=(MergeCounters const& delta) { @@ -150,4 +192,153 @@ EvictionStatistics::submitMetricsAndRestartCycle(uint32_t currLedgerSeq, mNumEntriesEvicted = 0; mEvictionCycleStartLedger = currLedgerSeq; } + +template <> +LedgerEntryTypeAndDurability +bucketEntryToLedgerEntryAndDurabilityType( + LiveBucket::EntryT const& be) +{ + auto bet = be.type(); + LedgerKey key; + if (bet == INITENTRY || bet == LIVEENTRY) + { + key = LedgerEntryKey(be.liveEntry()); + } + else if (bet == DEADENTRY) + { + key = be.deadEntry(); + } + else + { + throw std::runtime_error("Unexpected Bucket Meta Entry"); + } + + switch (key.type()) + { + case ACCOUNT: + return LedgerEntryTypeAndDurability::ACCOUNT; + case TRUSTLINE: + return LedgerEntryTypeAndDurability::TRUSTLINE; + case OFFER: + return LedgerEntryTypeAndDurability::OFFER; + case DATA: + return LedgerEntryTypeAndDurability::DATA; + case CLAIMABLE_BALANCE: + return LedgerEntryTypeAndDurability::CLAIMABLE_BALANCE; + case LIQUIDITY_POOL: + return LedgerEntryTypeAndDurability::LIQUIDITY_POOL; + case CONTRACT_DATA: + return isTemporaryEntry(key) + ? LedgerEntryTypeAndDurability::TEMPORARY_CONTRACT_DATA + : LedgerEntryTypeAndDurability::PERSISTENT_CONTRACT_DATA; + case CONTRACT_CODE: + return LedgerEntryTypeAndDurability::CONTRACT_CODE; + case CONFIG_SETTING: + return LedgerEntryTypeAndDurability::CONFIG_SETTING; + case TTL: + return LedgerEntryTypeAndDurability::TTL; + default: + auto label = xdr::xdr_traits::enum_name(key.type()); + throw std::runtime_error( + fmt::format("Unknown LedgerEntryType {}", label)); + } +} + +template <> +LedgerEntryTypeAndDurability +bucketEntryToLedgerEntryAndDurabilityType( + HotArchiveBucket::EntryT const& be) +{ + auto bet = be.type(); + LedgerKey key; + if (bet == HOT_ARCHIVE_ARCHIVED) + { + key = LedgerEntryKey(be.archivedEntry()); + } + else if (bet == HOT_ARCHIVE_LIVE || bet == HOT_ARCHIVE_DELETED) + { + key = be.key(); + } + else + { + throw std::runtime_error("Unexpected Hot Archive Meta Entry"); + } + + if (key.type() != CONTRACT_DATA && isTemporaryEntry(key)) + { + throw std::runtime_error("HotArchiveBucketEntry is temporary"); + } + + switch (key.type()) + { + case CONTRACT_DATA: + return LedgerEntryTypeAndDurability::PERSISTENT_CONTRACT_DATA; + case CONTRACT_CODE: + return LedgerEntryTypeAndDurability::CONTRACT_CODE; + default: + auto label = xdr::xdr_traits::enum_name(key.type()); + throw std::runtime_error( + fmt::format("Unexpected LedgerEntryType in HotArchive: {}", label)); + } +} + +template <> +bool +isBucketMetaEntry(HotArchiveBucket::EntryT const& be) +{ + return be.type() == HOT_ARCHIVE_METAENTRY; +} + +template <> +bool +isBucketMetaEntry(LiveBucket::EntryT const& be) +{ + return be.type() == METAENTRY; +} + +template +void +BucketEntryCounters::count(typename BucketT::EntryT const& be) +{ + if (isBucketMetaEntry(be)) + { + // Do not count meta entries. + return; + } + auto ledt = bucketEntryToLedgerEntryAndDurabilityType(be); + entryTypeCounts[ledt]++; + entryTypeSizes[ledt] += xdr::xdr_size(be); +} + +BucketEntryCounters& +BucketEntryCounters::operator+=(BucketEntryCounters const& other) +{ + for (auto [type, count] : other.entryTypeCounts) + { + this->entryTypeCounts[type] += count; + } + for (auto [type, size] : other.entryTypeSizes) + { + this->entryTypeSizes[type] += size; + } + return *this; +} + +bool +BucketEntryCounters::operator==(BucketEntryCounters const& other) const +{ + return this->entryTypeCounts == other.entryTypeCounts && + this->entryTypeSizes == other.entryTypeSizes; +} + +bool +BucketEntryCounters::operator!=(BucketEntryCounters const& other) const +{ + return !(*this == other); +} + +template void +BucketEntryCounters::count(LiveBucket::EntryT const& be); +template void BucketEntryCounters::count( + HotArchiveBucket::EntryT const& be); } diff --git a/src/bucket/BucketUtils.h b/src/bucket/BucketUtils.h index dad92ac008..b458051576 100644 --- a/src/bucket/BucketUtils.h +++ b/src/bucket/BucketUtils.h @@ -4,10 +4,11 @@ // under the Apache License, Version 2.0. See the COPYING file at the root // of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 -#include "main/Application.h" #include "xdr/Stellar-ledger-entries.h" #include #include +#include +#include #include namespace medida @@ -18,12 +19,25 @@ namespace stellar { class Application; +class LiveBucket; +class HotArchiveBucket; +template class BucketListSnapshot; +class SearchableLiveBucketListSnapshot; +class SearchableHotArchiveBucketListSnapshot; #define BUCKET_TYPE_ASSERT(BucketT) \ static_assert(std::is_same_v || \ std::is_same_v, \ "BucketT must be a Bucket type") +// BucketList types +template +using SnapshotPtrT = std::unique_ptr const>; +using SearchableSnapshotConstPtr = + std::shared_ptr; +using SearchableHotArchiveSnapshotConstPtr = + std::shared_ptr; + // A fine-grained merge-operation-counter structure for tracking various // events during merges. These are not medida counters because we do not // want or need to publish this level of granularity outside of testing, and @@ -156,4 +170,46 @@ class EvictionStatistics void submitMetricsAndRestartCycle(uint32_t currLedgerSeq, EvictionCounters& counters); }; + +enum class LedgerEntryTypeAndDurability : uint32_t +{ + ACCOUNT = 0, + TRUSTLINE = 1, + OFFER = 2, + DATA = 3, + CLAIMABLE_BALANCE = 4, + LIQUIDITY_POOL = 5, + TEMPORARY_CONTRACT_DATA = 6, + PERSISTENT_CONTRACT_DATA = 7, + CONTRACT_CODE = 8, + CONFIG_SETTING = 9, + TTL = 10, + NUM_TYPES = 11, +}; + +struct BucketEntryCounters +{ + std::map entryTypeCounts; + std::map entryTypeSizes; + + template void count(typename BucketT::EntryT const& be); + BucketEntryCounters& operator+=(BucketEntryCounters const& other); + bool operator==(BucketEntryCounters const& other) const; + bool operator!=(BucketEntryCounters const& other) const; + + template + void + serialize(Archive& ar) + { + ar(entryTypeCounts, entryTypeSizes); + } +}; + +template +bool isBucketMetaEntry(typename BucketT::EntryT const& be); + +template +LedgerEntryTypeAndDurability +bucketEntryToLedgerEntryAndDurabilityType(typename BucketT::EntryT const& be); +std::string toString(LedgerEntryTypeAndDurability let); } \ No newline at end of file diff --git a/src/bucket/DiskIndex.cpp b/src/bucket/DiskIndex.cpp new file mode 100644 index 0000000000..8fb0b7c9fd --- /dev/null +++ b/src/bucket/DiskIndex.cpp @@ -0,0 +1,417 @@ +// Copyright 2025 Stellar Development Foundation and contributors. Licensed +// under the Apache License, Version 2.0. See the COPYING file at the root +// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 + +#include "bucket/DiskIndex.h" +#include "bucket/BucketIndexUtils.h" +#include "bucket/BucketManager.h" +#include "bucket/BucketUtils.h" +#include "bucket/HotArchiveBucket.h" +#include "bucket/LiveBucket.h" +#include "crypto/Hex.h" +#include "crypto/ShortHash.h" +#include "util/BufferedAsioCerealOutputArchive.h" +#include "util/Fs.h" +#include "util/GlobalChecks.h" +#include "util/LogSlowExecution.h" +#include "util/Logging.h" +#include + +#include + +namespace stellar +{ + +namespace +{ +// Returns true if the key is not contained within the given IndexEntry. +// Range index: check if key is outside range of indexEntry +// Individual index: check if key does not match indexEntry key +bool +keyNotInIndexEntry(LedgerKey const& key, RangeEntry const& indexEntry) +{ + return key < indexEntry.lowerBound || indexEntry.upperBound < key; +} + +// std::lower_bound predicate. Returns true if index comes "before" key and does +// not contain it +// If key is too small for indexEntry bounds: return false +// If key is contained within indexEntry bounds: return false +// If key is too large for indexEntry bounds: return true +bool +lower_bound_pred(RangeIndex::value_type const& indexEntry, LedgerKey const& key) +{ + return indexEntry.first.upperBound < key; +} + +// std::upper_bound predicate. Returns true if key comes "before" and is not +// contained within the indexEntry. +// If key is too small for indexEntry bounds: return true +// If key is contained within indexEntry bounds: return false +// If key is too large for indexEntry bounds: return false +bool +upper_bound_pred(LedgerKey const& key, RangeIndex::value_type const& indexEntry) +{ + return key < indexEntry.first.lowerBound; +} +} + +template +std::pair::IterT> +DiskIndex::scan(IterT start, LedgerKey const& k) const +{ + ZoneScoped; + ZoneValue(static_cast(mData.keysToOffset.size())); + + // Search for the key in the index before checking the bloom filter so we + // return the correct iterator to the caller. This may be slightly less + // effecient then checking the bloom filter first, but the filter's primary + // purpose is to avoid disk lookups, not to avoid in-memory index search. + auto keyIter = + std::lower_bound(start, mData.keysToOffset.end(), k, lower_bound_pred); + + // If the key is not in the bloom filter or in the lower bounded index + // entry, return nullopt + mBloomLookupMeter.Mark(); + if ((mData.filter && !mData.filter->contains(k)) || + keyIter == mData.keysToOffset.end() || + keyNotInIndexEntry(k, keyIter->first)) + { + return {IndexReturnT(), keyIter}; + } + else + { + return {keyIter->second, keyIter}; + } +} + +template +std::optional> +DiskIndex::getOffsetBounds(LedgerKey const& lowerBound, + LedgerKey const& upperBound) const +{ + // Get the index iterators for the bounds + auto startIter = + std::lower_bound(mData.keysToOffset.begin(), mData.keysToOffset.end(), + lowerBound, lower_bound_pred); + if (startIter == mData.keysToOffset.end()) + { + return std::nullopt; + } + + auto endIter = + std::upper_bound(std::next(startIter), mData.keysToOffset.end(), + upperBound, upper_bound_pred); + + // Get file offsets based on lower and upper bound iterators + std::streamoff startOff = startIter->second; + std::streamoff endOff = std::numeric_limits::max(); + + // If we hit the end of the index then upper bound should be EOF + if (endIter != mData.keysToOffset.end()) + { + endOff = endIter->second; + } + + return std::make_pair(startOff, endOff); +} + +template +DiskIndex::DiskIndex(BucketManager& bm, + std::filesystem::path const& filename, + std::streamoff pageSize, Hash const& hash, + asio::io_context& ctx) + : mBloomLookupMeter(bm.getBloomLookupMeter()) + , mBloomMissMeter(bm.getBloomMissMeter()) +{ + ZoneScoped; + mData.pageSize = pageSize; + + // Only LiveBucket needs an asset to poolID mapping + if constexpr (std::is_same_v) + { + mData.assetToPoolID = std::make_unique(); + } + + auto fileSize = fs::size(filename); + auto estimatedIndexEntries = fileSize / pageSize; + mData.keysToOffset.reserve(estimatedIndexEntries); + + XDRInputFileStream in; + in.open(filename.string()); + std::streamoff pos = 0; + std::streamoff pageUpperBound = 0; + typename BucketT::EntryT be; + size_t iter = 0; + [[maybe_unused]] size_t count = 0; + + std::vector keyHashes; + auto seed = shortHash::getShortHashInitKey(); + + while (in && in.readOne(be)) + { + // peridocially check if bucket manager is exiting to stop indexing + // gracefully + if (++iter >= 1000) + { + iter = 0; + if (bm.isShutdown()) + { + throw std::runtime_error("Incomplete bucket index due to " + "BucketManager shutdown"); + } + } + + if (!isBucketMetaEntry(be)) + { + ++count; + LedgerKey key = getBucketLedgerKey(be); + + if constexpr (std::is_same_v) + { + // We need an asset to poolID mapping for + // loadPoolshareTrustlineByAccountAndAsset queries. For this + // query, we only need to index INIT entries because: + // 1. PoolID is the hash of the Assets it refers to, so this + // index cannot be invalidated by newer LIVEENTRY updates + // 2. We do a join over all bucket indexes so we avoid + // storing + // multiple redundant index entries (i.e. LIVEENTRY + // updates) + // 3. We only use this index to collect the possible set of + // Trustline keys, then we load those keys. This means + // that we don't need to keep track of DEADENTRY. Even if + // a given INITENTRY has been deleted by a newer + // DEADENTRY, the trustline load will not return deleted + // trustlines, so the load result is still correct even + // if the index has a few deleted mappings. + if (be.type() == INITENTRY && key.type() == LIQUIDITY_POOL) + { + auto const& poolParams = be.liveEntry() + .data.liquidityPool() + .body.constantProduct() + .params; + (*mData.assetToPoolID)[poolParams.assetA].emplace_back( + key.liquidityPool().liquidityPoolID); + (*mData.assetToPoolID)[poolParams.assetB].emplace_back( + key.liquidityPool().liquidityPoolID); + } + } + else + { + static_assert(std::is_same_v); + } + + auto keyBuf = xdr::xdr_to_opaque(key); + SipHash24 hasher(seed.data()); + hasher.update(keyBuf.data(), keyBuf.size()); + keyHashes.emplace_back(hasher.digest()); + + if (pos >= pageUpperBound) + { + pageUpperBound = + roundDown(pos, mData.pageSize) + mData.pageSize; + mData.keysToOffset.emplace_back(RangeEntry(key, key), pos); + } + else + { + auto& rangeEntry = mData.keysToOffset.back().first; + releaseAssert(rangeEntry.upperBound < key); + rangeEntry.upperBound = key; + } + + mData.counters.template count(be); + } + + pos = in.pos(); + } + + // Binary Fuse filter requires at least 2 elements + if (keyHashes.size() > 1) + { + // There is currently an access error that occurs very rarely + // for some random seed values. If this occurs, simply rotate + // the seed and try again. + for (int i = 0; i < 10; ++i) + { + try + { + mData.filter = + std::make_unique(keyHashes, seed); + } + catch (std::out_of_range& e) + { + auto seedToStr = [](auto seed) { + std::string result; + for (auto b : seed) + { + fmt::format_to(std::back_inserter(result), "{:02x}", b); + } + return result; + }; + + CLOG_ERROR(Bucket, + "Bad memory access in BinaryFuseFilter with " + "seed {}, retrying", + seedToStr(seed)); + seed[0]++; + } + } + } + + CLOG_DEBUG(Bucket, "Indexed {} positions in {}", mData.keysToOffset.size(), + filename.filename()); + ZoneValue(static_cast(count)); + + if (bm.getConfig().BUCKETLIST_DB_PERSIST_INDEX) + { + saveToDisk(bm, hash, ctx); + } +} + +template +template +DiskIndex::DiskIndex(Archive& ar, BucketManager const& bm, + std::streamoff pageSize) + : mBloomLookupMeter(bm.getBloomLookupMeter()) + , mBloomMissMeter(bm.getBloomMissMeter()) +{ + releaseAssertOrThrow(pageSize != 0); + mData.pageSize = pageSize; + ar(mData); + if constexpr (std::is_same_v) + { + releaseAssertOrThrow(mData.assetToPoolID); + } + else + { + static_assert(std::is_same_v); + releaseAssertOrThrow(!mData.assetToPoolID); + } +} + +template +void +DiskIndex::saveToDisk(BucketManager& bm, Hash const& hash, + asio::io_context& ctx) const +{ + ZoneScoped; + releaseAssert(bm.getConfig().BUCKETLIST_DB_PERSIST_INDEX); + if constexpr (std::is_same_v) + { + releaseAssertOrThrow(mData.assetToPoolID); + } + else + { + static_assert(std::is_same_v); + releaseAssertOrThrow(!mData.assetToPoolID); + } + + auto timer = + LogSlowExecution("Saving index", LogSlowExecution::Mode::AUTOMATIC_RAII, + "took", std::chrono::milliseconds(100)); + + std::filesystem::path tmpFilename = + BucketT::randomBucketIndexName(bm.getTmpDir()); + CLOG_DEBUG(Bucket, "Saving bucket index for {}: {}", hexAbbrev(hash), + tmpFilename); + + { + OutputFileStream out(ctx, !bm.getConfig().DISABLE_XDR_FSYNC); + out.open(tmpFilename.string()); + cereal::BufferedAsioOutputArchive ar(out); + ar(mData); + } + + std::filesystem::path canonicalName = bm.bucketIndexFilename(hash); + CLOG_DEBUG(Bucket, "Adopting bucket index file {} as {}", tmpFilename, + canonicalName); + if (!bm.renameBucketDirFile(tmpFilename, canonicalName)) + { + std::string err("Failed to rename bucket index :"); + err += strerror(errno); + // it seems there is a race condition with external systems + // retry after sleeping for a second works around the problem + std::this_thread::sleep_for(std::chrono::seconds(1)); + if (!bm.renameBucketDirFile(tmpFilename, canonicalName)) + { + // if rename fails again, surface the original error + throw std::runtime_error(err); + } + } +} + +template +void +DiskIndex::markBloomMiss() const +{ + mBloomMissMeter.Mark(); +} + +#ifdef BUILD_TESTS +template +bool +DiskIndex::operator==(DiskIndex const& in) const +{ + if (getPageSize() != in.getPageSize()) + { + return false; + } + + if (mData.keysToOffset != in.mData.keysToOffset) + { + return false; + } + + // If both indexes have a filter, check if they are equal + if (mData.filter && in.mData.filter) + { + if (!(*(mData.filter) == *(in.mData.filter))) + { + return false; + } + } + else + { + // If both indexes don't fave a filter, check that each filter is + // null + if (mData.filter || in.mData.filter) + { + return false; + } + } + + if (mData.assetToPoolID && in.mData.assetToPoolID) + { + if (!(*(mData.assetToPoolID) == *(in.mData.assetToPoolID))) + { + return false; + } + } + else + { + if (mData.assetToPoolID || in.mData.assetToPoolID) + { + return false; + } + } + + if (mData.counters != in.mData.counters) + { + return false; + } + + return true; +} +#endif + +template class DiskIndex; +template class DiskIndex; + +template DiskIndex::DiskIndex(cereal::BinaryInputArchive& ar, + BucketManager const& bm, + std::streamoff pageSize); +template DiskIndex::DiskIndex(cereal::BinaryInputArchive& ar, + BucketManager const& bm, + std::streamoff pageSize); +} \ No newline at end of file diff --git a/src/bucket/DiskIndex.h b/src/bucket/DiskIndex.h new file mode 100644 index 0000000000..f8e58b8790 --- /dev/null +++ b/src/bucket/DiskIndex.h @@ -0,0 +1,194 @@ +#pragma once + +// Copyright 2025 Stellar Development Foundation and contributors. Licensed +// under the Apache License, Version 2.0. See the COPYING file at the root +// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 + +#include "bucket/BucketIndexUtils.h" +#include "bucket/BucketUtils.h" +#include "util/BinaryFuseFilter.h" +#include "util/GlobalChecks.h" +#include "util/XDROperators.h" // IWYU pragma: keep + +#include +#include +#include +#include + +#include +#include + +namespace medida +{ +class Meter; +} + +namespace asio +{ +class io_context; +} + +namespace stellar +{ +class BucketManager; + +// maps smallest and largest LedgerKey on a given page inclusively +// [lowerBound, upperbound] +struct RangeEntry +{ + LedgerKey lowerBound; + LedgerKey upperBound; + + RangeEntry() = default; + RangeEntry(LedgerKey low, LedgerKey high) + : lowerBound(low), upperBound(high) + { + releaseAssert(low < high || low == high); + } + + inline bool + operator==(RangeEntry const& in) const + { + return lowerBound == in.lowerBound && upperBound == in.upperBound; + } + + template + void + serialize(Archive& ar) + { + ar(lowerBound, upperBound); + } +}; + +using RangeIndex = std::vector>; + +// For large Buckets, we cannot cache all contents in memory. Instead, we use a +// random eviction cache for partial cacheing, and a range based index + binary +// fuse filter for disk lookups. Creating this index is expensive, so we persist +// it to disk. We do not persist the random eviction cache. +template class DiskIndex : public NonMovableOrCopyable +{ + BUCKET_TYPE_ASSERT(BucketT); + + // Fields from here are persisted on disk. Cereal doesn't like templates so + // we define an inner struct to hold all serializable fields. + struct Data + { + std::streamoff pageSize{}; + RangeIndex keysToOffset; + std::unique_ptr filter{}; + + // Note: mAssetToPoolID is null for HotArchive Bucket types + std::unique_ptr assetToPoolID{}; + BucketEntryCounters counters{}; + + template + void + save(Archive& ar) const + { + auto version = BucketT::IndexT::BUCKET_INDEX_VERSION; + ar(version, pageSize, keysToOffset, filter, assetToPoolID, + counters); + } + + // Note: version and pageSize must be loaded before this + // function is called. Caller is responsible for deserializing these + // fields first via the preload function. If any of these values does + // not match the expected values, it indicates an upgrade has occurred + // since serialization. The on-disk version is outdated and must be + // replaced by a newly created index. + template + void + load(Archive& ar) + { + ar(keysToOffset, filter, assetToPoolID, counters); + } + + } mData; + + medida::Meter& mBloomLookupMeter; + medida::Meter& mBloomMissMeter; + + // Saves index to disk, overwriting any preexisting file for this index + void saveToDisk(BucketManager& bm, Hash const& hash, + asio::io_context& ctx) const; + + public: + using IterT = RangeIndex::const_iterator; + + // Constructor for creating a fresh index. + DiskIndex(BucketManager& bm, std::filesystem::path const& filename, + std::streamoff pageSize, Hash const& hash, asio::io_context& ctx); + + // Constructor for loading pre-existing index from disk. Must call preLoad + // before calling this constructor to properly deserialize index. + template + DiskIndex(Archive& ar, BucketManager const& bm, std::streamoff pageSize); + + // Begins searching for LegerKey k from start. + // Returns pair of: + // file offset in the bucket file for k, or std::nullopt if not found + // iterator that points to the first index entry not less than k, or + // BucketIndex::end() + std::pair scan(IterT start, LedgerKey const& k) const; + + // Returns [lowFileOffset, highFileOffset) that contain the key ranges + // [lowerBound, upperBound]. If no file offsets exist, returns [0, 0] + std::optional> + getOffsetBounds(LedgerKey const& lowerBound, + LedgerKey const& upperBound) const; + + // Returns page size for index + std::streamoff + getPageSize() const + { + return mData.pageSize; + } + + BucketEntryCounters const& + getBucketEntryCounters() const + { + return mData.counters; + } + + IterT + begin() const + { + return mData.keysToOffset.begin(); + } + + IterT + end() const + { + return mData.keysToOffset.end(); + } + + // Loads index version and pageSize from serialized index archive. Must be + // called before DiskIndex(Archive& ar, std::streamoff pageSize) to properly + // deserialize index. + template + static void + preLoad(Archive& ar, uint32_t& version, std::streamoff& pageSize) + { + ar(version, pageSize); + } + + // This messy template makes is such that this function is only defined + // when BucketT == LiveBucket + template , bool> = true> + AssetPoolIDMap const& + getAssetPoolIDMap() const + { + static_assert(std::is_same_v); + releaseAssert(mData.assetToPoolID); + return *mData.assetToPoolID; + } + + void markBloomMiss() const; + +#ifdef BUILD_TESTS + bool operator==(DiskIndex const& inRaw) const; +#endif +}; +} \ No newline at end of file diff --git a/src/bucket/FutureBucket.cpp b/src/bucket/FutureBucket.cpp index 151b46c297..73ce4ed785 100644 --- a/src/bucket/FutureBucket.cpp +++ b/src/bucket/FutureBucket.cpp @@ -66,7 +66,8 @@ FutureBucket::FutureBucket( if (!snap->isEmpty() && protocolVersionIsBefore( snap->getBucketVersion(), - BucketBase::FIRST_PROTOCOL_SUPPORTING_PERSISTENT_EVICTION)) + HotArchiveBucket:: + FIRST_PROTOCOL_SUPPORTING_PERSISTENT_EVICTION)) { throw std::runtime_error( "Invalid ArchivalFutureBucket: ledger version doesn't support " @@ -408,7 +409,7 @@ FutureBucket::startMerge(Application& app, uint32_t maxProtocolVersion, ZoneNamedN(mergeZone, "Merge task", true); ZoneValueV(mergeZone, static_cast(level)); - auto res = BucketBase::merge( + auto res = BucketT::merge( bm, maxProtocolVersion, curr, snap, shadows, BucketListBase::keepTombstoneEntries(level), countMergeEvents, ctx, doFsync); diff --git a/src/bucket/HotArchiveBucket.cpp b/src/bucket/HotArchiveBucket.cpp index 6ce3ed7041..c01d2eeeab 100644 --- a/src/bucket/HotArchiveBucket.cpp +++ b/src/bucket/HotArchiveBucket.cpp @@ -122,9 +122,9 @@ HotArchiveBucket::getBucketVersion() const return it.getMetadata().ledgerVersion; } -HotArchiveBucket::HotArchiveBucket(std::string const& filename, - Hash const& hash, - std::unique_ptr&& index) +HotArchiveBucket::HotArchiveBucket( + std::string const& filename, Hash const& hash, + std::unique_ptr&& index) : BucketBase(filename, hash, std::move(index)) { } @@ -139,8 +139,9 @@ HotArchiveBucket::isTombstoneEntry(HotArchiveBucketEntry const& e) return e.type() == HOT_ARCHIVE_LIVE; } -std::shared_ptr -HotArchiveBucket::bucketEntryToLoadResult(std::shared_ptr const& be) +std::shared_ptr +HotArchiveBucket::bucketEntryToLoadResult( + std::shared_ptr const& be) { return isTombstoneEntry(*be) ? nullptr : be; } diff --git a/src/bucket/HotArchiveBucket.h b/src/bucket/HotArchiveBucket.h index 772ec0c22d..dd69393181 100644 --- a/src/bucket/HotArchiveBucket.h +++ b/src/bucket/HotArchiveBucket.h @@ -6,6 +6,7 @@ #include "bucket/BucketBase.h" #include "bucket/BucketUtils.h" +#include "bucket/HotArchiveBucketIndex.h" #include "xdr/Stellar-ledger-entries.h" namespace stellar @@ -22,8 +23,9 @@ typedef BucketOutputIterator HotArchiveBucketOutputIterator; * Hot Archive Buckets are used by the HotBucketList to store recently evicted * entries. They contain entries of type HotArchiveBucketEntry. */ -class HotArchiveBucket : public BucketBase, - public std::enable_shared_from_this +class HotArchiveBucket + : public BucketBase, + public std::enable_shared_from_this { static std::vector convertToBucketEntry(std::vector const& archivedEntries, @@ -37,13 +39,18 @@ class HotArchiveBucket : public BucketBase, // Entry type returned by loadKeys using LoadT = HotArchiveBucketEntry; + using IndexT = HotArchiveBucketIndex; + + static inline constexpr char const* METRIC_STRING = + "bucketlistDB-hotArchive"; + HotArchiveBucket(); virtual ~HotArchiveBucket() { } HotArchiveBucket(std::string const& filename, Hash const& hash, - std::unique_ptr&& index); - uint32_t getBucketVersion() const override; + std::unique_ptr&& index); + uint32_t getBucketVersion() const; static std::shared_ptr fresh(BucketManager& bucketManager, uint32_t protocolVersion, @@ -88,8 +95,8 @@ class HotArchiveBucket : public BucketBase, std::vector& shadowIterators, uint32_t protocolVersion, bool keepShadowedLifecycleEntries); - static std::shared_ptr - bucketEntryToLoadResult(std::shared_ptr const& be); + static std::shared_ptr + bucketEntryToLoadResult(std::shared_ptr const& be); friend class HotArchiveBucketSnapshot; }; diff --git a/src/bucket/HotArchiveBucketIndex.cpp b/src/bucket/HotArchiveBucketIndex.cpp new file mode 100644 index 0000000000..09ac0deda4 --- /dev/null +++ b/src/bucket/HotArchiveBucketIndex.cpp @@ -0,0 +1,76 @@ +// Copyright 2025 Stellar Development Foundation and contributors. Licensed +// under the Apache License, Version 2.0. See the COPYING file at the root +// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 + +#include "bucket/HotArchiveBucketIndex.h" +#include "bucket/BucketIndexUtils.h" +#include "bucket/BucketManager.h" +#include "util/GlobalChecks.h" +#include "util/Logging.h" +#include "xdr/Stellar-ledger-entries.h" +#include + +namespace stellar +{ + +HotArchiveBucketIndex::HotArchiveBucketIndex( + BucketManager& bm, std::filesystem::path const& filename, Hash const& hash, + asio::io_context& ctx) + : mDiskIndex(bm, filename, getPageSize(bm.getConfig(), 0), hash, ctx) +{ + ZoneScoped; + releaseAssert(!filename.empty()); + + CLOG_DEBUG(Bucket, + "HotArchiveBucketIndex::createIndex() indexing key range with " + "page size {} in bucket {}", + mDiskIndex.getPageSize(), filename); +} + +std::streamoff +HotArchiveBucketIndex::getPageSize(Config const& cfg, size_t bucketSize) +{ + auto ret = getPageSizeFromConfig(cfg); + if (ret == 0) + { + // HotArchive doesn't support individual indexes, use default 16 KB + // value even if config is set to 0 + return 16'384; + } + return ret; +} + +template +HotArchiveBucketIndex::HotArchiveBucketIndex(BucketManager const& bm, + Archive& ar, + std::streamoff pageSize) + : mDiskIndex(ar, bm, pageSize) +{ + // HotArchive only supports disk indexes + releaseAssertOrThrow(pageSize != 0); +} + +std::pair +HotArchiveBucketIndex::scan(IterT start, LedgerKey const& k) const +{ + ZoneScoped; + return mDiskIndex.scan(start, k); +} + +#ifdef BUILD_TESTS +bool +HotArchiveBucketIndex::operator==(HotArchiveBucketIndex const& in) const +{ + if (!(mDiskIndex == in.mDiskIndex)) + { + return false; + } + + return true; +} +#endif + +template HotArchiveBucketIndex::HotArchiveBucketIndex( + BucketManager const& bm, cereal::BinaryInputArchive& ar, + std::streamoff pageSize); +} \ No newline at end of file diff --git a/src/bucket/HotArchiveBucketIndex.h b/src/bucket/HotArchiveBucketIndex.h new file mode 100644 index 0000000000..8b1afb9e23 --- /dev/null +++ b/src/bucket/HotArchiveBucketIndex.h @@ -0,0 +1,112 @@ +#pragma once + +// Copyright 2025 Stellar Development Foundation and contributors. Licensed +// under the Apache License, Version 2.0. See the COPYING file at the root +// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 + +#include "bucket/BucketUtils.h" +#include "bucket/DiskIndex.h" +#include "bucket/HotArchiveBucket.h" +#include "bucket/LedgerCmp.h" +#include "util/NonCopyable.h" +#include "util/XDROperators.h" // IWYU pragma: keep +#include "xdr/Stellar-ledger-entries.h" +#include +#include + +#include + +namespace asio +{ +class io_context; +} + +namespace medida +{ +class Meter; +} + +namespace stellar +{ + +/** + * BucketIndex is an in-memory mapping of LedgerKey's to the file offset + * of the associated LedgerEntry in a given Bucket file. Because the set of + * LedgerKeys is too large to keep in memory, BucketIndex can either index + * individual keys or key ranges. + * + * For small buckets, an individual index is used for faster index lookup. For + * larger buckets, the range index is used. The range index cannot give an exact + * position for a given LedgerEntry or tell if it exists in the bucket, but can + * give an offset range for where the entry would be if it exists. Config flags + * determine the size of the range index, as well as what bucket size should use + * the individual index vs range index. + */ + +class BucketManager; + +class HotArchiveBucketIndex : public NonMovableOrCopyable +{ + private: + DiskIndex const mDiskIndex; + + public: + inline static const uint32_t BUCKET_INDEX_VERSION = 0; + + using IterT = DiskIndex::IterT; + + HotArchiveBucketIndex(BucketManager& bm, + std::filesystem::path const& filename, + Hash const& hash, asio::io_context& ctx); + + template + HotArchiveBucketIndex(BucketManager const& bm, Archive& ar, + std::streamoff pageSize); + + // Returns pagesize for given index based on config parameters. BucketSize + // is ignored, but we keep the parameter for consistency with + // LiveBucketIndex. + static std::streamoff getPageSize(Config const& cfg, size_t bucketSize); + + IndexReturnT + lookup(LedgerKey const& k) const + { + return mDiskIndex.scan(mDiskIndex.begin(), k).first; + } + + std::pair scan(IterT start, LedgerKey const& k) const; + + BucketEntryCounters const& + getBucketEntryCounters() const + { + return mDiskIndex.getBucketEntryCounters(); + } + + uint32_t + getPageSize() const + { + return mDiskIndex.getPageSize(); + } + + IterT + begin() const + { + return mDiskIndex.begin(); + } + + IterT + end() const + { + return mDiskIndex.end(); + } + + void + markBloomMiss() const + { + mDiskIndex.markBloomMiss(); + } +#ifdef BUILD_TESTS + bool operator==(HotArchiveBucketIndex const& in) const; +#endif +}; +} \ No newline at end of file diff --git a/src/bucket/HotArchiveBucketList.cpp b/src/bucket/HotArchiveBucketList.cpp index ce180a734a..4784b77b3c 100644 --- a/src/bucket/HotArchiveBucketList.cpp +++ b/src/bucket/HotArchiveBucketList.cpp @@ -18,7 +18,7 @@ HotArchiveBucketList::addBatch(Application& app, uint32_t currLedger, ZoneScoped; releaseAssertOrThrow(protocolVersionStartsFrom( currLedgerProtocol, - BucketBase::FIRST_PROTOCOL_SUPPORTING_PERSISTENT_EVICTION)); + HotArchiveBucket::FIRST_PROTOCOL_SUPPORTING_PERSISTENT_EVICTION)); addBatchInternal(app, currLedger, currLedgerProtocol, archiveEntries, restoredEntries, deletedEntries); } diff --git a/src/bucket/InMemoryIndex.cpp b/src/bucket/InMemoryIndex.cpp new file mode 100644 index 0000000000..95de28f7e4 --- /dev/null +++ b/src/bucket/InMemoryIndex.cpp @@ -0,0 +1,134 @@ +// Copyright 2025 Stellar Development Foundation and contributors. Licensed +// under the Apache License, Version 2.0. See the COPYING file at the root +// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 + +#include "bucket/InMemoryIndex.h" +#include "bucket/BucketManager.h" +#include "bucket/LedgerCmp.h" +#include "bucket/LiveBucket.h" +#include "util/XDRStream.h" +#include "util/types.h" +#include "xdr/Stellar-ledger-entries.h" +#include +#include + +namespace stellar +{ + +void +InMemoryBucketState::pushBack(BucketEntry const& be) +{ + if (!mEntries.empty()) + { + if (!BucketEntryIdCmp{}(*mEntries.back(), be)) + { + throw std::runtime_error( + "InMemoryBucketState::push_back: Inserted out of order entry!"); + } + } + + mEntries.push_back(std::make_shared(be)); +} + +// Perform a binary search using start iter as lower bound for search key. +std::pair +InMemoryBucketState::scan(IterT start, LedgerKey const& searchKey) const +{ + auto it = + std::lower_bound(start, mEntries.end(), searchKey, + [](std::shared_ptr const& element, + LedgerKey const& key) { + return getBucketLedgerKey(*element) < key; + }); + + // If we found the key + if (it != mEntries.end() && getBucketLedgerKey(**it) == searchKey) + { + return {IndexReturnT(*it), it}; + } + + return {IndexReturnT(), it}; +} + +InMemoryIndex::InMemoryIndex(BucketManager const& bm, + std::filesystem::path const& filename) +{ + XDRInputFileStream in; + in.open(filename.string()); + BucketEntry be; + size_t iter = 0; + std::streamoff lastOffset = 0; + std::optional firstOffer; + std::optional lastOffer; + + while (in && in.readOne(be)) + { + if (++iter >= 1000) + { + iter = 0; + if (bm.isShutdown()) + { + throw std::runtime_error("Incomplete bucket index due to " + "BucketManager shutdown"); + } + } + + if (be.type() == METAENTRY) + { + lastOffset = in.pos(); + continue; + } + + // Populate assetPoolIDMap + LedgerKey lk = getBucketLedgerKey(be); + if (be.type() == INITENTRY) + { + if (lk.type() == LIQUIDITY_POOL) + { + auto const& poolParams = be.liveEntry() + .data.liquidityPool() + .body.constantProduct() + .params; + mAssetPoolIDMap[poolParams.assetA].emplace_back( + lk.liquidityPool().liquidityPoolID); + mAssetPoolIDMap[poolParams.assetB].emplace_back( + lk.liquidityPool().liquidityPoolID); + } + } + + // Populate inMemoryState + mInMemoryState.pushBack(be); + + // Populate offerRange + if (!firstOffer && lk.type() == OFFER) + { + firstOffer = lastOffset; + } + if (!lastOffer && lk.type() > OFFER) + { + lastOffer = lastOffset; + } + + lastOffset = in.pos(); + } + + if (firstOffer) + { + if (lastOffer) + { + mOfferRange = {*firstOffer, *lastOffer}; + } + // If we didn't see any entries after offers, then the upper bound is + // EOF + else + { + mOfferRange = {*firstOffer, + std::numeric_limits::max()}; + } + } + else + { + mOfferRange = std::nullopt; + } +} +} \ No newline at end of file diff --git a/src/bucket/InMemoryIndex.h b/src/bucket/InMemoryIndex.h new file mode 100644 index 0000000000..170ae6396d --- /dev/null +++ b/src/bucket/InMemoryIndex.h @@ -0,0 +1,116 @@ +#pragma once + +// Copyright 2025 Stellar Development Foundation and contributors. Licensed +// under the Apache License, Version 2.0. See the COPYING file at the root +// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 + +#include "bucket/BucketIndexUtils.h" +#include "bucket/BucketUtils.h" +#include "xdr/Stellar-ledger-entries.h" + +#include +#include + +namespace stellar +{ + +// For small Buckets, we can cache all contents in memory. Because we cache all +// entries, the index is just as large as the Bucket itself, so we never persist +// this index type. It is always recreated on startup. +class InMemoryBucketState : public NonMovableOrCopyable +{ + // Entries sorted by LedgerKey. INIT/LIVE entries stored as + // LedgerEntry, DEADENTRY stored as LedgerKey. + std::vector mEntries; + + public: + using IterT = std::vector::const_iterator; + + // Insert a LedgerEntry (INIT/LIVE) into the ordered container. Entries must + // be ordered. + void pushBack(BucketEntry const& be); + + // Find a LedgerEntry by key starting from the given iterator. + std::pair scan(IterT start, + LedgerKey const& searchKey) const; + + IterT + begin() const + { + return mEntries.begin(); + } + IterT + end() const + { + return mEntries.end(); + } + +#ifdef BUILD_TESTS + bool + operator==(InMemoryBucketState const& in) const + { + return mEntries == in.mEntries; + } +#endif +}; + +class InMemoryIndex +{ + private: + InMemoryBucketState mInMemoryState; + AssetPoolIDMap mAssetPoolIDMap; + BucketEntryCounters mCounters{}; + std::optional> mOfferRange; + + public: + using IterT = InMemoryBucketState::IterT; + + InMemoryIndex(BucketManager const& bm, + std::filesystem::path const& filename); + + IterT + begin() const + { + return mInMemoryState.begin(); + } + IterT + end() const + { + return mInMemoryState.end(); + } + + AssetPoolIDMap const& + getAssetPoolIDMap() const + { + return mAssetPoolIDMap; + } + + BucketEntryCounters const& + getBucketEntryCounters() const + { + return mCounters; + } + + std::pair + scan(IterT start, LedgerKey const& searchKey) const + { + return mInMemoryState.scan(start, searchKey); + } + + std::optional> + getOfferRange() const + { + return mOfferRange; + } + +#ifdef BUILD_TESTS + bool + operator==(InMemoryIndex const& in) const + { + return mInMemoryState == in.mInMemoryState && + mAssetPoolIDMap == in.mAssetPoolIDMap && + mCounters == in.mCounters; + } +#endif +}; +} \ No newline at end of file diff --git a/src/bucket/LiveBucket.cpp b/src/bucket/LiveBucket.cpp index 7001baa9cc..208f2ce8ca 100644 --- a/src/bucket/LiveBucket.cpp +++ b/src/bucket/LiveBucket.cpp @@ -310,6 +310,12 @@ LiveBucket::apply(Application& app) const } #endif // BUILD_TESTS +std::optional> +LiveBucket::getOfferRange() const +{ + return getIndex().getOfferRange(); +} + std::vector LiveBucket::convertToBucketEntry(bool useInit, std::vector const& initEntries, @@ -368,7 +374,7 @@ LiveBucket::fresh(BucketManager& bucketManager, uint32_t protocolVersion, if (protocolVersionStartsFrom( protocolVersion, - BucketBase::FIRST_PROTOCOL_SUPPORTING_PERSISTENT_EVICTION)) + LiveBucket::FIRST_PROTOCOL_SUPPORTING_PERSISTENT_EVICTION)) { meta.ext.v(1); meta.ext.bucketListType() = BucketListType::LIVE; @@ -409,7 +415,7 @@ LiveBucket::checkProtocolLegality(BucketEntry const& entry, } LiveBucket::LiveBucket(std::string const& filename, Hash const& hash, - std::unique_ptr&& index) + std::unique_ptr&& index) : BucketBase(filename, hash, std::move(index)) { } @@ -438,39 +444,11 @@ LiveBucket::isTombstoneEntry(BucketEntry const& e) return e.type() == DEADENTRY; } -std::shared_ptr -LiveBucket::bucketEntryToLoadResult(std::shared_ptr const& be) +std::shared_ptr +LiveBucket::bucketEntryToLoadResult(std::shared_ptr const& be) { return isTombstoneEntry(*be) ? nullptr : std::make_shared(be->liveEntry()); } - -BucketEntryCounters& -BucketEntryCounters::operator+=(BucketEntryCounters const& other) -{ - for (auto [type, count] : other.entryTypeCounts) - { - this->entryTypeCounts[type] += count; - } - for (auto [type, size] : other.entryTypeSizes) - { - this->entryTypeSizes[type] += size; - } - return *this; -} - -bool -BucketEntryCounters::operator==(BucketEntryCounters const& other) const -{ - return this->entryTypeCounts == other.entryTypeCounts && - this->entryTypeSizes == other.entryTypeSizes; -} - -bool -BucketEntryCounters::operator!=(BucketEntryCounters const& other) const -{ - return !(*this == other); -} - } \ No newline at end of file diff --git a/src/bucket/LiveBucket.h b/src/bucket/LiveBucket.h index cc3fe34c0b..91429f2a31 100644 --- a/src/bucket/LiveBucket.h +++ b/src/bucket/LiveBucket.h @@ -6,7 +6,7 @@ #include "bucket/BucketBase.h" #include "bucket/BucketUtils.h" -#include "ledger/LedgerTypeUtils.h" +#include "bucket/LiveBucketIndex.h" namespace medida { @@ -24,28 +24,12 @@ template class BucketInputIterator; typedef BucketOutputIterator LiveBucketOutputIterator; typedef BucketInputIterator LiveBucketInputIterator; -struct BucketEntryCounters -{ - std::map entryTypeCounts; - std::map entryTypeSizes; - - BucketEntryCounters& operator+=(BucketEntryCounters const& other); - bool operator==(BucketEntryCounters const& other) const; - bool operator!=(BucketEntryCounters const& other) const; - - template - void - serialize(Archive& ar) - { - ar(entryTypeCounts, entryTypeSizes); - } -}; /* * Live Buckets are used by the LiveBucketList to store the current canonical * state of the ledger. They contain entries of type BucketEntry. */ -class LiveBucket : public BucketBase, +class LiveBucket : public BucketBase, public std::enable_shared_from_this { public: @@ -55,12 +39,16 @@ class LiveBucket : public BucketBase, // Entry type returned by loadKeys using LoadT = LedgerEntry; + using IndexT = LiveBucketIndex; + + static inline constexpr char const* METRIC_STRING = "bucketlistDB-live"; + LiveBucket(); virtual ~LiveBucket() { } LiveBucket(std::string const& filename, Hash const& hash, - std::unique_ptr&& index); + std::unique_ptr&& index); // Returns true if a BucketEntry that is key-wise identical to the given // BucketEntry exists in the bucket. For testing. @@ -97,6 +85,11 @@ class LiveBucket : public BucketBase, void apply(Application& app) const; #endif + // Returns [lowerBound, upperBound) of file offsets for all offers in the + // bucket, or std::nullopt if no offers exist + std::optional> + getOfferRange() const; + // Create a fresh bucket from given vectors of init (created) and live // (updated) LedgerEntries, and dead LedgerEntryKeys. The bucket will // be sorted, hashed, and adopted in the provided BucketManager. @@ -111,8 +104,8 @@ class LiveBucket : public BucketBase, // level bucket (i.e. DEADENTRY) static bool isTombstoneEntry(BucketEntry const& e); - static std::shared_ptr - bucketEntryToLoadResult(std::shared_ptr const& be); + static std::shared_ptr + bucketEntryToLoadResult(std::shared_ptr const& be); // Whenever a given BucketEntry is "eligible" to be written as the merge // result in the output bucket, this function writes the entry to the output @@ -125,7 +118,7 @@ class LiveBucket : public BucketBase, static void countOldEntryType(MergeCounters& mc, BucketEntry const& e); static void countNewEntryType(MergeCounters& mc, BucketEntry const& e); - uint32_t getBucketVersion() const override; + uint32_t getBucketVersion() const; BucketEntryCounters const& getBucketEntryCounters() const; diff --git a/src/bucket/LiveBucketIndex.cpp b/src/bucket/LiveBucketIndex.cpp new file mode 100644 index 0000000000..e203f3d7d3 --- /dev/null +++ b/src/bucket/LiveBucketIndex.cpp @@ -0,0 +1,243 @@ +// Copyright 2025 Stellar Development Foundation and contributors. Licensed +// under the Apache License, Version 2.0. See the COPYING file at the root +// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 + +#include "bucket/LiveBucketIndex.h" +#include "bucket/BucketIndexUtils.h" +#include "bucket/BucketManager.h" +#include "bucket/DiskIndex.h" +#include "util/Fs.h" +#include "util/GlobalChecks.h" +#include "util/Logging.h" +#include "xdr/Stellar-ledger-entries.h" +#include +#include + +namespace stellar +{ + +bool +LiveBucketIndex::typeNotSupported(LedgerEntryType t) +{ + return t == OFFER; +} + +std::streamoff +LiveBucketIndex::getPageSize(Config const& cfg, size_t bucketSize) +{ + // Convert cfg param from MB to bytes + if (auto cutoff = cfg.BUCKETLIST_DB_INDEX_CUTOFF * 1'000'000; + bucketSize < cutoff) + { + return 0; + } + + return getPageSizeFromConfig(cfg); +} + +LiveBucketIndex::LiveBucketIndex(BucketManager& bm, + std::filesystem::path const& filename, + Hash const& hash, asio::io_context& ctx) +{ + ZoneScoped; + releaseAssert(!filename.empty()); + + auto pageSize = getPageSize(bm.getConfig(), fs::size(filename)); + if (pageSize == 0) + { + + CLOG_DEBUG(Bucket, + "LiveBucketIndex::createIndex() using in-memory index for " + "bucket {}", + filename); + mInMemoryIndex = std::make_unique(bm, filename); + } + else + { + CLOG_DEBUG(Bucket, + "LiveBucketIndex::createIndex() indexing key range with " + "page size {} in bucket {}", + pageSize, filename); + mDiskIndex = std::make_unique>( + bm, filename, pageSize, hash, ctx); + } +} + +template +LiveBucketIndex::LiveBucketIndex(BucketManager const& bm, Archive& ar, + std::streamoff pageSize) + + : mDiskIndex(std::make_unique>(ar, bm, pageSize)) +{ + // Only disk indexes are serialized + releaseAssertOrThrow(pageSize != 0); +} + +LiveBucketIndex::IterT +LiveBucketIndex::begin() const +{ + if (mDiskIndex) + { + return mDiskIndex->begin(); + } + else + { + releaseAssertOrThrow(mInMemoryIndex); + return mInMemoryIndex->begin(); + } +} + +LiveBucketIndex::IterT +LiveBucketIndex::end() const +{ + if (mDiskIndex) + { + return mDiskIndex->end(); + } + else + { + releaseAssertOrThrow(mInMemoryIndex); + return mInMemoryIndex->end(); + } +} + +void +LiveBucketIndex::markBloomMiss() const +{ + releaseAssertOrThrow(mDiskIndex); + mDiskIndex->markBloomMiss(); +} + +IndexReturnT +LiveBucketIndex::lookup(LedgerKey const& k) const +{ + if (mDiskIndex) + { + return mDiskIndex->scan(mDiskIndex->begin(), k).first; + } + else + { + releaseAssertOrThrow(mInMemoryIndex); + return mInMemoryIndex->scan(mInMemoryIndex->begin(), k).first; + } +} + +std::pair +LiveBucketIndex::scan(IterT start, LedgerKey const& k) const +{ + if (mDiskIndex) + { + return mDiskIndex->scan(getDiskIter(start), k); + } + + releaseAssertOrThrow(mInMemoryIndex); + return mInMemoryIndex->scan(getInMemoryIter(start), k); +} + +std::vector const& +LiveBucketIndex::getPoolIDsByAsset(Asset const& asset) const +{ + static const std::vector emptyVec = {}; + + if (mDiskIndex) + { + auto iter = mDiskIndex->getAssetPoolIDMap().find(asset); + if (iter == mDiskIndex->getAssetPoolIDMap().end()) + { + return emptyVec; + } + return iter->second; + } + else + { + releaseAssertOrThrow(mInMemoryIndex); + auto iter = mInMemoryIndex->getAssetPoolIDMap().find(asset); + if (iter == mInMemoryIndex->getAssetPoolIDMap().end()) + { + return emptyVec; + } + return iter->second; + } +} + +std::optional> +LiveBucketIndex::getOfferRange() const +{ + if (mDiskIndex) + { + // Get the smallest and largest possible offer keys + LedgerKey upperBound(OFFER); + upperBound.offer().sellerID.ed25519().fill( + std::numeric_limits::max()); + upperBound.offer().offerID = std::numeric_limits::max(); + + LedgerKey lowerBound(OFFER); + lowerBound.offer().sellerID.ed25519().fill( + std::numeric_limits::min()); + lowerBound.offer().offerID = std::numeric_limits::min(); + + return mDiskIndex->getOffsetBounds(lowerBound, upperBound); + } + + releaseAssertOrThrow(mInMemoryIndex); + return mInMemoryIndex->getOfferRange(); +} + +uint32_t +LiveBucketIndex::getPageSize() const +{ + if (mDiskIndex) + { + return mDiskIndex->getPageSize(); + } + + releaseAssertOrThrow(mInMemoryIndex); + return 0; +} + +BucketEntryCounters const& +LiveBucketIndex::getBucketEntryCounters() const +{ + if (mDiskIndex) + { + return mDiskIndex->getBucketEntryCounters(); + } + + releaseAssertOrThrow(mInMemoryIndex); + return mInMemoryIndex->getBucketEntryCounters(); +} + +#ifdef BUILD_TESTS +bool +LiveBucketIndex::operator==(LiveBucketIndex const& in) const +{ + if (mDiskIndex) + { + if (!in.mDiskIndex) + { + return false; + } + + if (!(*mDiskIndex == *in.mDiskIndex)) + { + return false; + } + } + else + { + if (in.mDiskIndex) + { + return false; + } + + return *mInMemoryIndex == *in.mInMemoryIndex; + } + + return true; +} +#endif + +template LiveBucketIndex::LiveBucketIndex(BucketManager const& bm, + cereal::BinaryInputArchive& ar, + std::streamoff pageSize); +} \ No newline at end of file diff --git a/src/bucket/LiveBucketIndex.h b/src/bucket/LiveBucketIndex.h new file mode 100644 index 0000000000..5a54d2e674 --- /dev/null +++ b/src/bucket/LiveBucketIndex.h @@ -0,0 +1,110 @@ +#pragma once + +// Copyright 2025 Stellar Development Foundation and contributors. Licensed +// under the Apache License, Version 2.0. See the COPYING file at the root +// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 + +#include "bucket/BucketUtils.h" +#include "bucket/DiskIndex.h" +#include "bucket/InMemoryIndex.h" +#include "bucket/LedgerCmp.h" +#include "bucket/LiveBucket.h" +#include "util/NonCopyable.h" +#include "util/XDROperators.h" // IWYU pragma: keep +#include "xdr/Stellar-ledger-entries.h" +#include +#include + +#include + +namespace asio +{ +class io_context; +} + +namespace medida +{ +class Meter; +} + +namespace stellar +{ + +/** + * BucketIndex is an in-memory mapping of LedgerKey's to the file offset + * of the associated LedgerEntry in a given Bucket file. Because the set of + * LedgerKeys is too large to keep in memory, BucketIndex can either index + * individual keys or key ranges. + * + * For small buckets, an individual index is used for faster index lookup. For + * larger buckets, the range index is used. The range index cannot give an exact + * position for a given LedgerEntry or tell if it exists in the bucket, but can + * give an offset range for where the entry would be if it exists. Config flags + * determine the size of the range index, as well as what bucket size should use + * the individual index vs range index. + */ + +class BucketManager; + +class LiveBucketIndex : public NonMovableOrCopyable +{ + public: + using IterT = + std::variant::IterT>; + + private: + std::unique_ptr const> mDiskIndex; + std::unique_ptr mInMemoryIndex; + + static inline DiskIndex::IterT + getDiskIter(IterT const& iter) + { + return std::get::IterT>(iter); + } + + static inline InMemoryIndex::IterT + getInMemoryIter(IterT const& iter) + { + return std::get(iter); + } + + public: + inline static const std::string DB_BACKEND_STATE = "bl"; + inline static const uint32_t BUCKET_INDEX_VERSION = 5; + + // Constructor for creating new index from Bucketfile + LiveBucketIndex(BucketManager& bm, std::filesystem::path const& filename, + Hash const& hash, asio::io_context& ctx); + + // Constructor for loading pre-existing index from disk + template + LiveBucketIndex(BucketManager const& bm, Archive& ar, + std::streamoff pageSize); + + // Returns true if LedgerEntryType not supported by BucketListDB + static bool typeNotSupported(LedgerEntryType t); + + // Returns pagesize for given index based on config parameters and bucket + // size, in bytes + static std::streamoff getPageSize(Config const& cfg, size_t bucketSize); + + IndexReturnT lookup(LedgerKey const& k) const; + + std::pair scan(IterT start, LedgerKey const& k) const; + + std::vector const& getPoolIDsByAsset(Asset const& asset) const; + + std::optional> + getOfferRange() const; + + BucketEntryCounters const& getBucketEntryCounters() const; + uint32_t getPageSize() const; + + IterT begin() const; + IterT end() const; + void markBloomMiss() const; +#ifdef BUILD_TESTS + bool operator==(LiveBucketIndex const& in) const; +#endif +}; +} \ No newline at end of file diff --git a/src/bucket/SearchableBucketList.cpp b/src/bucket/SearchableBucketList.cpp index 47fac8e742..23e010024e 100644 --- a/src/bucket/SearchableBucketList.cpp +++ b/src/bucket/SearchableBucketList.cpp @@ -6,6 +6,7 @@ #include "bucket/BucketInputIterator.h" #include "bucket/BucketListSnapshotBase.h" #include "bucket/LiveBucketList.h" +#include "ledger/LedgerTxn.h" #include "util/GlobalChecks.h" #include @@ -130,10 +131,9 @@ SearchableLiveBucketListSnapshot::loadPoolShareTrustLinesByAccountAndAsset( loopAllBuckets(trustLineLoop, *mSnapshot); - auto timer = mSnapshotManager - .recordBulkLoadMetrics("poolshareTrustlines", - trustlinesToLoad.size()) - .TimeScope(); + auto timer = + getBulkLoadTimer("poolshareTrustlines", trustlinesToLoad.size()) + .TimeScope(); std::vector result; auto loadKeysLoop = [&](auto const& b) { @@ -154,8 +154,7 @@ SearchableLiveBucketListSnapshot::loadInflationWinners(size_t maxWinners, // This is a legacy query, should only be called by main thread during // catchup - auto timer = mSnapshotManager.recordBulkLoadMetrics("inflationWinners", 0) - .TimeScope(); + auto timer = getBulkLoadTimer("inflationWinners", 0).TimeScope(); UnorderedMap voteCount; UnorderedSet seen; @@ -219,7 +218,8 @@ SearchableLiveBucketListSnapshot::loadInflationWinners(size_t maxWinners, winners.size() < maxWinners && iter->first >= minBalance; ++iter) { // push back {AccountID, voteCount} - winners.push_back({iter->second->first, iter->first}); + winners.push_back( + InflationWinner{iter->second->first, iter->first}); } } else @@ -241,16 +241,7 @@ SearchableLiveBucketListSnapshot::loadKeysWithLimits( std::set const& inKeys, LedgerKeyMeter* lkMeter) const { - if (threadIsMain()) - { - auto timer = - mSnapshotManager.recordBulkLoadMetrics("prefetch", inKeys.size()) - .TimeScope(); - auto op = loadKeysInternal(inKeys, lkMeter, std::nullopt); - releaseAssertOrThrow(op); - return std::move(*op); - } - + auto timer = getBulkLoadTimer("prefetch", inKeys.size()).TimeScope(); auto op = loadKeysInternal(inKeys, lkMeter, std::nullopt); releaseAssertOrThrow(op); return std::move(*op); @@ -258,19 +249,21 @@ SearchableLiveBucketListSnapshot::loadKeysWithLimits( SearchableLiveBucketListSnapshot::SearchableLiveBucketListSnapshot( BucketSnapshotManager const& snapshotManager, - SnapshotPtrT&& snapshot, + AppConnector const& appConnector, SnapshotPtrT&& snapshot, std::map>&& historicalSnapshots) : SearchableBucketListSnapshotBase( - snapshotManager, std::move(snapshot), std::move(historicalSnapshots)) + snapshotManager, appConnector, std::move(snapshot), + std::move(historicalSnapshots)) { } SearchableHotArchiveBucketListSnapshot::SearchableHotArchiveBucketListSnapshot( BucketSnapshotManager const& snapshotManager, - SnapshotPtrT&& snapshot, + AppConnector const& appConnector, SnapshotPtrT&& snapshot, std::map>&& historicalSnapshots) : SearchableBucketListSnapshotBase( - snapshotManager, std::move(snapshot), std::move(historicalSnapshots)) + snapshotManager, appConnector, std::move(snapshot), + std::move(historicalSnapshots)) { } diff --git a/src/bucket/SearchableBucketList.h b/src/bucket/SearchableBucketList.h index 41a737d630..a033402a60 100644 --- a/src/bucket/SearchableBucketList.h +++ b/src/bucket/SearchableBucketList.h @@ -15,7 +15,7 @@ class SearchableLiveBucketListSnapshot { SearchableLiveBucketListSnapshot( BucketSnapshotManager const& snapshotManager, - SnapshotPtrT&& snapshot, + AppConnector const& appConnector, SnapshotPtrT&& snapshot, std::map>&& historicalSnapshots); public: @@ -44,6 +44,7 @@ class SearchableHotArchiveBucketListSnapshot { SearchableHotArchiveBucketListSnapshot( BucketSnapshotManager const& snapshotManager, + AppConnector const& appConnector, SnapshotPtrT&& snapshot, std::map>&& historicalSnapshots); diff --git a/src/bucket/readme.md b/src/bucket/readme.md index 34439828e7..f7a7892a48 100644 --- a/src/bucket/readme.md +++ b/src/bucket/readme.md @@ -89,9 +89,8 @@ lookup speed and memory overhead. The following configuration flags control thes Larger values slow down lookup speed but decrease memory usage. - `BUCKETLIST_DB_INDEX_CUTOFF` - - Bucket file size, in MB, that determines wether the `IndividualIndex` or - `RangeIndex` is used. - Default value is 20 MB, which indexes the first ~3 levels with the `IndividualIndex`. + - Bucket file size, in MB, that determines wether the Bucket is cached in memory or not. + Default value is 250 MB, which indexes the first ~5 levels with the `IndividualIndex`. Larger values speed up lookups but increase memory usage. - `BUCKETLIST_DB_PERSIST_INDEX` - When set to true, BucketListDB indexes are saved to disk to avoid reindexing diff --git a/src/bucket/test/BucketIndexTests.cpp b/src/bucket/test/BucketIndexTests.cpp index e123c6b90e..afe91617b0 100644 --- a/src/bucket/test/BucketIndexTests.cpp +++ b/src/bucket/test/BucketIndexTests.cpp @@ -7,6 +7,7 @@ #include "bucket/BucketManager.h" #include "bucket/BucketSnapshotManager.h" +#include "bucket/LiveBucket.h" #include "bucket/LiveBucketList.h" #include "bucket/test/BucketTestUtils.h" #include "ledger/test/LedgerTestUtils.h" @@ -643,7 +644,7 @@ TEST_CASE("serialize bucket indexes", "[bucket][bucketindex]") REQUIRE(b->isIndexed()); auto onDiskIndex = - BucketIndex::load(test.getBM(), indexFilename, b->getSize()); + loadIndex(test.getBM(), indexFilename, b->getSize()); REQUIRE(onDiskIndex); auto& inMemoryIndex = b->getIndexForTesting(); @@ -686,7 +687,7 @@ TEST_CASE("serialize bucket indexes", "[bucket][bucketindex]") // Check if on-disk index rewritten with correct config params auto indexFilename = test.getBM().bucketIndexFilename(bucketHash); auto onDiskIndex = - BucketIndex::load(test.getBM(), indexFilename, b->getSize()); + loadIndex(test.getBM(), indexFilename, b->getSize()); REQUIRE((inMemoryIndex == *onDiskIndex)); } } @@ -712,37 +713,38 @@ TEST_CASE("hot archive bucket lookups", "[bucket][bucketindex][archive]") .getBucketSnapshotManager() .copySearchableHotArchiveBucketListSnapshot(); - auto checkLoad = [&](LedgerKey const& k, - std::shared_ptr entryPtr) { - // Restored entries should be null - if (expectedRestoredEntries.find(k) != - expectedRestoredEntries.end()) - { - REQUIRE(!entryPtr); - } + auto checkLoad = + [&](LedgerKey const& k, + std::shared_ptr entryPtr) { + // Restored entries should be null + if (expectedRestoredEntries.find(k) != + expectedRestoredEntries.end()) + { + REQUIRE(!entryPtr); + } - // Deleted entries should be HotArchiveBucketEntry of type - // DELETED - else if (expectedDeletedEntries.find(k) != - expectedDeletedEntries.end()) - { - REQUIRE(entryPtr); - REQUIRE(entryPtr->type() == - HotArchiveBucketEntryType::HOT_ARCHIVE_DELETED); - REQUIRE(entryPtr->key() == k); - } + // Deleted entries should be HotArchiveBucketEntry of type + // DELETED + else if (expectedDeletedEntries.find(k) != + expectedDeletedEntries.end()) + { + REQUIRE(entryPtr); + REQUIRE(entryPtr->type() == + HotArchiveBucketEntryType::HOT_ARCHIVE_DELETED); + REQUIRE(entryPtr->key() == k); + } - // Archived entries should contain full LedgerEntry - else - { - auto expectedIter = expectedArchiveEntries.find(k); - REQUIRE(expectedIter != expectedArchiveEntries.end()); - REQUIRE(entryPtr); - REQUIRE(entryPtr->type() == - HotArchiveBucketEntryType::HOT_ARCHIVE_ARCHIVED); - REQUIRE(entryPtr->archivedEntry() == expectedIter->second); - } - }; + // Archived entries should contain full LedgerEntry + else + { + auto expectedIter = expectedArchiveEntries.find(k); + REQUIRE(expectedIter != expectedArchiveEntries.end()); + REQUIRE(entryPtr); + REQUIRE(entryPtr->type() == + HotArchiveBucketEntryType::HOT_ARCHIVE_ARCHIVED); + REQUIRE(entryPtr->archivedEntry() == expectedIter->second); + } + }; auto checkResult = [&] { LedgerKeySet bulkLoadKeys; @@ -810,7 +812,7 @@ TEST_CASE("hot archive bucket lookups", "[bucket][bucketindex][archive]") app->getLedgerManager().getLastClosedLedgerHeader().header; header.ledgerSeq += 1; header.ledgerVersion = static_cast( - BucketBase::FIRST_PROTOCOL_SUPPORTING_PERSISTENT_EVICTION); + HotArchiveBucket::FIRST_PROTOCOL_SUPPORTING_PERSISTENT_EVICTION); addHotArchiveBatchAndUpdateSnapshot(*app, header, archivedEntries, restoredEntries, deletedEntries); app->getBucketManager() diff --git a/src/bucket/test/BucketListTests.cpp b/src/bucket/test/BucketListTests.cpp index 0a5b545097..ead788e61c 100644 --- a/src/bucket/test/BucketListTests.cpp +++ b/src/bucket/test/BucketListTests.cpp @@ -959,7 +959,7 @@ TEST_CASE_VERSIONS("eviction scan", "[bucketlist][archival]") bool tempOnly = protocolVersionIsBefore( cfg.TESTING_UPGRADE_LEDGER_PROTOCOL_VERSION, - BucketBase::FIRST_PROTOCOL_SUPPORTING_PERSISTENT_EVICTION); + LiveBucket::FIRST_PROTOCOL_SUPPORTING_PERSISTENT_EVICTION); LedgerManagerForBucketTests& lm = app->getLedgerManager(); auto& bm = app->getBucketManager(); diff --git a/src/bucket/test/BucketTestUtils.cpp b/src/bucket/test/BucketTestUtils.cpp index c5b79d9f04..99a6aa7df8 100644 --- a/src/bucket/test/BucketTestUtils.cpp +++ b/src/bucket/test/BucketTestUtils.cpp @@ -237,7 +237,7 @@ LedgerManagerForBucketTests::transferLedgerEntriesToBucketList( .getSorobanNetworkConfigForApply()); if (protocolVersionStartsFrom( initialLedgerVers, - BucketBase:: + LiveBucket:: FIRST_PROTOCOL_SUPPORTING_PERSISTENT_EVICTION)) { std::vector restoredKeys; diff --git a/src/bucket/test/BucketTests.cpp b/src/bucket/test/BucketTests.cpp index 6277f96f83..18e85dd2ef 100644 --- a/src/bucket/test/BucketTests.cpp +++ b/src/bucket/test/BucketTests.cpp @@ -81,7 +81,7 @@ TEST_CASE_VERSIONS("file backed buckets", "[bucket][bucketbench]") dead = LedgerTestUtils::generateValidLedgerEntryKeysWithExclusions( {CONFIG_SETTING}, 1000); { - b1 = BucketBase::merge( + b1 = LiveBucket::merge( app->getBucketManager(), app->getConfig().LEDGER_PROTOCOL_VERSION, b1, LiveBucket::fresh(app->getBucketManager(), @@ -170,7 +170,7 @@ TEST_CASE_VERSIONS("merging bucket entries", "[bucket]") /*countMergeEvents=*/true, clock.getIOContext(), /*doFsync=*/true); - auto b1 = BucketBase::merge( + auto b1 = LiveBucket::merge( bm, vers, bLive, bDead, /*shadows=*/{}, /*keepTombstoneEntries=*/true, /*countMergeEvents=*/true, clock.getIOContext(), @@ -209,7 +209,7 @@ TEST_CASE_VERSIONS("merging bucket entries", "[bucket]") /*countMergeEvents=*/true, clock.getIOContext(), /*doFsync=*/true); - auto b1 = BucketBase::merge(bm, vers, bLive, bDead, /*shadows=*/{}, + auto b1 = LiveBucket::merge(bm, vers, bLive, bDead, /*shadows=*/{}, /*keepTombstoneEntries=*/true, /*countMergeEvents=*/true, clock.getIOContext(), @@ -263,7 +263,7 @@ TEST_CASE_VERSIONS("merging bucket entries", "[bucket]") app->getBucketManager(), getAppLedgerVersion(app), {}, live, dead, /*countMergeEvents=*/true, clock.getIOContext(), /*doFsync=*/true); - std::shared_ptr b3 = BucketBase::merge( + std::shared_ptr b3 = LiveBucket::merge( app->getBucketManager(), app->getConfig().LEDGER_PROTOCOL_VERSION, b1, b2, /*shadows=*/{}, /*keepTombstoneEntries=*/true, @@ -320,11 +320,11 @@ TEST_CASE_VERSIONS("merging hot archive bucket entries", "[bucket][archival]") // e2 -> ARCHIVED // e3 -> LIVE // e4 -> DELETED - auto merged = BucketBase::merge(bm, vers, b1, b2, /*shadows=*/{}, - /*keepTombstoneEntries=*/true, - /*countMergeEvents=*/true, - clock.getIOContext(), - /*doFsync=*/true); + auto merged = HotArchiveBucket::merge( + bm, vers, b1, b2, /*shadows=*/{}, + /*keepTombstoneEntries=*/true, + /*countMergeEvents=*/true, clock.getIOContext(), + /*doFsync=*/true); bool seen1 = false; bool seen4 = false; @@ -463,7 +463,7 @@ TEST_CASE("merges proceed old-style despite newer shadows", { // With proto 12, new bucket version solely depends on the snap version auto bucket = - BucketBase::merge(bm, v12, b11first, b11second, + LiveBucket::merge(bm, v12, b11first, b11second, /*shadows=*/{b12first}, /*keepTombstoneEntries=*/true, /*countMergeEvents=*/true, clock.getIOContext(), @@ -475,7 +475,7 @@ TEST_CASE("merges proceed old-style despite newer shadows", // Merging older version (10) buckets, with mixed versions of shadows // (11, 12) Pick initentry (11) style merge auto bucket = - BucketBase::merge(bm, v12, b10first, b10second, + LiveBucket::merge(bm, v12, b10first, b10second, /*shadows=*/{b12first, b11second}, /*keepTombstoneEntries=*/true, /*countMergeEvents=*/true, clock.getIOContext(), @@ -484,7 +484,7 @@ TEST_CASE("merges proceed old-style despite newer shadows", } SECTION("refuse to merge new version with shadow") { - REQUIRE_THROWS_AS(BucketBase::merge(bm, v12, b12first, b12second, + REQUIRE_THROWS_AS(LiveBucket::merge(bm, v12, b12first, b12second, /*shadows=*/{b12first}, /*keepTombstoneEntries=*/true, /*countMergeEvents=*/true, @@ -520,7 +520,7 @@ TEST_CASE("merges refuse to exceed max protocol version", LiveBucket::fresh(bm, vers, {}, {otherLiveA}, {}, /*countMergeEvents=*/true, clock.getIOContext(), /*doFsync=*/true); - REQUIRE_THROWS_AS(BucketBase::merge(bm, vers - 1, bnew1, bnew2, + REQUIRE_THROWS_AS(LiveBucket::merge(bm, vers - 1, bnew1, bnew2, /*shadows=*/{}, /*keepTombstoneEntries=*/true, /*countMergeEvents=*/true, @@ -596,7 +596,7 @@ TEST_CASE_VERSIONS("merging bucket entries with initentry", /*countMergeEvents=*/true, clock.getIOContext(), /*doFsync=*/true); - auto b1 = BucketBase::merge( + auto b1 = LiveBucket::merge( bm, cfg.LEDGER_PROTOCOL_VERSION, bInit, bDead, /*shadows=*/{}, /*keepTombstoneEntries=*/true, /*countMergeEvents=*/true, clock.getIOContext(), @@ -636,12 +636,12 @@ TEST_CASE_VERSIONS("merging bucket entries with initentry", /*countMergeEvents=*/true, clock.getIOContext(), /*doFsync=*/true); - auto bmerge1 = BucketBase::merge( + auto bmerge1 = LiveBucket::merge( bm, cfg.LEDGER_PROTOCOL_VERSION, bInit, bLive, /*shadows=*/{}, /*keepTombstoneEntries=*/true, /*countMergeEvents=*/true, clock.getIOContext(), /*doFsync=*/true); - auto b1 = BucketBase::merge( + auto b1 = LiveBucket::merge( bm, cfg.LEDGER_PROTOCOL_VERSION, bmerge1, bDead, /*shadows=*/{}, /*keepTombstoneEntries=*/true, /*countMergeEvents=*/true, clock.getIOContext(), @@ -706,12 +706,12 @@ TEST_CASE_VERSIONS("merging bucket entries with initentry", CHECK(enew.nLive == 0); CHECK(enew.nDead == 1); - auto bmerge1 = BucketBase::merge( + auto bmerge1 = LiveBucket::merge( bm, cfg.LEDGER_PROTOCOL_VERSION, bold, bmed, /*shadows=*/{}, /*keepTombstoneEntries=*/true, /*countMergeEvents=*/true, clock.getIOContext(), /*doFsync=*/true); - auto bmerge2 = BucketBase::merge( + auto bmerge2 = LiveBucket::merge( bm, cfg.LEDGER_PROTOCOL_VERSION, bmerge1, bnew, /*shadows=*/{}, /*keepTombstoneEntries=*/true, /*countMergeEvents=*/true, clock.getIOContext(), @@ -793,7 +793,7 @@ TEST_CASE_VERSIONS("merging bucket entries with initentry with shadows", /*countMergeEvents=*/true, clock.getIOContext(), /*doFsync=*/true); - auto merged = BucketBase::merge( + auto merged = LiveBucket::merge( bm, cfg.LEDGER_PROTOCOL_VERSION, b1, b2, /*shadows=*/{shadow}, /*keepTombstoneEntries=*/true, @@ -848,7 +848,7 @@ TEST_CASE_VERSIONS("merging bucket entries with initentry with shadows", // risking shadowing-out level 3. Level 4 is a placeholder here, // just to be a thing-to-merge-level-3-with in the presence of // shadowing from 1 and 2. - auto merge43 = BucketBase::merge( + auto merge43 = LiveBucket::merge( bm, cfg.LEDGER_PROTOCOL_VERSION, level4, level3, /*shadows=*/{level2, level1}, /*keepTombstoneEntries=*/true, @@ -874,7 +874,7 @@ TEST_CASE_VERSIONS("merging bucket entries with initentry with shadows", // Do a merge between level 2 and 1, producing potentially // an annihilation of their INIT and DEAD pair. - auto merge21 = BucketBase::merge( + auto merge21 = LiveBucket::merge( bm, cfg.LEDGER_PROTOCOL_VERSION, level2, level1, /*shadows=*/{}, /*keepTombstoneEntries=*/true, @@ -900,13 +900,13 @@ TEST_CASE_VERSIONS("merging bucket entries with initentry with shadows", // Do two more merges: one between the two merges we've // done so far, and then finally one with level 5. - auto merge4321 = BucketBase::merge( + auto merge4321 = LiveBucket::merge( bm, cfg.LEDGER_PROTOCOL_VERSION, merge43, merge21, /*shadows=*/{}, /*keepTombstoneEntries=*/true, /*countMergeEvents=*/true, clock.getIOContext(), /*doFsync=*/true); - auto merge54321 = BucketBase::merge( + auto merge54321 = LiveBucket::merge( bm, cfg.LEDGER_PROTOCOL_VERSION, level5, merge4321, /*shadows=*/{}, /*keepTombstoneEntries=*/true, @@ -957,7 +957,7 @@ TEST_CASE_VERSIONS("merging bucket entries with initentry with shadows", // shadowing-out the init on level 3. Level 2 is a placeholder here, // just to be a thing-to-merge-level-3-with in the presence of // shadowing from 1. - auto merge32 = BucketBase::merge( + auto merge32 = LiveBucket::merge( bm, cfg.LEDGER_PROTOCOL_VERSION, level3, level2, /*shadows=*/{level1}, /*keepTombstoneEntries=*/true, @@ -984,7 +984,7 @@ TEST_CASE_VERSIONS("merging bucket entries with initentry with shadows", // Now do a merge between that 3+2 merge and level 1, and we risk // collecting tombstones in the lower levels, which we're expressly // trying to _stop_ doing by adding INIT. - auto merge321 = BucketBase::merge( + auto merge321 = LiveBucket::merge( bm, cfg.LEDGER_PROTOCOL_VERSION, merge32, level1, /*shadows=*/{}, /*keepTombstoneEntries=*/true, diff --git a/src/catchup/IndexBucketsWork.cpp b/src/catchup/IndexBucketsWork.cpp index 49b7a29fe4..7a3236681e 100644 --- a/src/catchup/IndexBucketsWork.cpp +++ b/src/catchup/IndexBucketsWork.cpp @@ -3,8 +3,8 @@ // of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 #include "IndexBucketsWork.h" -#include "bucket/BucketIndex.h" #include "bucket/BucketManager.h" +#include "bucket/DiskIndex.h" #include "bucket/LiveBucket.h" #include "util/Fs.h" #include "util/Logging.h" @@ -65,8 +65,8 @@ IndexBucketsWork::IndexWork::postWork() if (bm.getConfig().BUCKETLIST_DB_PERSIST_INDEX && fs::exists(indexFilename)) { - self->mIndex = BucketIndex::load(bm, indexFilename, - self->mBucket->getSize()); + self->mIndex = loadIndex(bm, indexFilename, + self->mBucket->getSize()); // If we could not load the index from the file, file is out of // date. Delete and create a new index. @@ -86,9 +86,9 @@ IndexBucketsWork::IndexWork::postWork() if (!self->mIndex) { // TODO: Fix this when archive BucketLists assume state - self->mIndex = BucketIndex::createIndex( - bm, self->mBucket->getFilename(), self->mBucket->getHash(), - ctx); + self->mIndex = + createIndex(bm, self->mBucket->getFilename(), + self->mBucket->getHash(), ctx); } app.postOnMainThread( diff --git a/src/catchup/IndexBucketsWork.h b/src/catchup/IndexBucketsWork.h index 08415387ee..12e23031a5 100644 --- a/src/catchup/IndexBucketsWork.h +++ b/src/catchup/IndexBucketsWork.h @@ -12,7 +12,7 @@ namespace stellar { class Bucket; -class BucketIndex; +class LiveBucketIndex; class BucketManager; class LiveBucket; @@ -21,7 +21,7 @@ class IndexBucketsWork : public Work class IndexWork : public BasicWork { std::shared_ptr mBucket; - std::unique_ptr mIndex; + std::unique_ptr mIndex; BasicWork::State mState{BasicWork::State::WORK_WAITING}; void postWork(); diff --git a/src/database/Database.cpp b/src/database/Database.cpp index 29738ba7a6..2ff393be0f 100644 --- a/src/database/Database.cpp +++ b/src/database/Database.cpp @@ -263,7 +263,7 @@ Database::maybeUpgradeToBucketListDB() { if (mApp.getPersistentState().getState(PersistentState::kDBBackend, getSession()) != - BucketIndex::DB_BACKEND_STATE) + LiveBucketIndex::DB_BACKEND_STATE) { CLOG_INFO(Database, "Upgrading to BucketListDB"); @@ -299,7 +299,7 @@ Database::maybeUpgradeToBucketListDB() getRawSession() << "DROP TABLE IF EXISTS ttl;"; mApp.getPersistentState().setState(PersistentState::kDBBackend, - BucketIndex::DB_BACKEND_STATE, + LiveBucketIndex::DB_BACKEND_STATE, getSession()); } } diff --git a/src/invariant/BucketListIsConsistentWithDatabase.cpp b/src/invariant/BucketListIsConsistentWithDatabase.cpp index d16a9bcdf3..1af6229948 100644 --- a/src/invariant/BucketListIsConsistentWithDatabase.cpp +++ b/src/invariant/BucketListIsConsistentWithDatabase.cpp @@ -122,7 +122,7 @@ BucketListIsConsistentWithDatabase::checkEntireBucketlist() { // Don't check entry types supported by BucketListDB, since they // won't exist in SQL - if (!BucketIndex::typeNotSupported(pair.first.type())) + if (!LiveBucketIndex::typeNotSupported(pair.first.type())) { continue; } @@ -160,7 +160,7 @@ BucketListIsConsistentWithDatabase::checkEntireBucketlist() if (mApp.getPersistentState().getState(PersistentState::kDBBackend, mApp.getDatabase().getSession()) != - BucketIndex::DB_BACKEND_STATE) + LiveBucketIndex::DB_BACKEND_STATE) { throw std::runtime_error( "Corrupt DB: BucketListDB flag " @@ -295,7 +295,8 @@ BucketListIsConsistentWithDatabase::checkOnBucketApply( } // Don't check DB against keys shadowed by earlier Buckets - if (BucketIndex::typeNotSupported(e.liveEntry().data.type()) && + if (LiveBucketIndex::typeNotSupported( + e.liveEntry().data.type()) && shadowedKeys.find(LedgerEntryKey(e.liveEntry())) == shadowedKeys.end()) { @@ -311,7 +312,7 @@ BucketListIsConsistentWithDatabase::checkOnBucketApply( { // Only check for OFFER keys that are not shadowed by an earlier // bucket - if (BucketIndex::typeNotSupported(e.deadEntry().type()) && + if (LiveBucketIndex::typeNotSupported(e.deadEntry().type()) && shadowedKeys.find(e.deadEntry()) == shadowedKeys.end()) { auto s = checkAgainstDatabase(ltx, e.deadEntry()); diff --git a/src/invariant/test/BucketListIsConsistentWithDatabaseTests.cpp b/src/invariant/test/BucketListIsConsistentWithDatabaseTests.cpp index 8da3e973ec..6a05b054b9 100644 --- a/src/invariant/test/BucketListIsConsistentWithDatabaseTests.cpp +++ b/src/invariant/test/BucketListIsConsistentWithDatabaseTests.cpp @@ -432,7 +432,7 @@ class ApplyBucketsWorkModifyEntry : public ApplyBucketsWork while (entry && entry.current() == mEntry) { releaseAssert( - BucketIndex::typeNotSupported(mEntry.data.type())); + LiveBucketIndex::typeNotSupported(mEntry.data.type())); modifyOfferEntry(entry.current()); mModified = true; diff --git a/src/ledger/LedgerManagerImpl.cpp b/src/ledger/LedgerManagerImpl.cpp index ece4d06d86..873bb0664a 100644 --- a/src/ledger/LedgerManagerImpl.cpp +++ b/src/ledger/LedgerManagerImpl.cpp @@ -1782,7 +1782,7 @@ LedgerManagerImpl::transferLedgerEntriesToBucketList( if (protocolVersionStartsFrom( initialLedgerVers, - BucketBase::FIRST_PROTOCOL_SUPPORTING_PERSISTENT_EVICTION)) + LiveBucket::FIRST_PROTOCOL_SUPPORTING_PERSISTENT_EVICTION)) { std::vector restoredKeys; auto const& restoredKeyMap = ltx.getRestoredHotArchiveKeys(); diff --git a/src/ledger/LedgerStateSnapshot.cpp b/src/ledger/LedgerStateSnapshot.cpp index b8c373742e..1f8a1cba93 100644 --- a/src/ledger/LedgerStateSnapshot.cpp +++ b/src/ledger/LedgerStateSnapshot.cpp @@ -25,7 +25,7 @@ LedgerEntryWrapper::LedgerEntryWrapper(LedgerTxnEntry&& entry) { } -LedgerEntryWrapper::LedgerEntryWrapper(std::shared_ptr entry) +LedgerEntryWrapper::LedgerEntryWrapper(std::shared_ptr entry) : mEntry(entry) { } diff --git a/src/ledger/LedgerStateSnapshot.h b/src/ledger/LedgerStateSnapshot.h index cc9fe382ec..a4e0653a1c 100644 --- a/src/ledger/LedgerStateSnapshot.h +++ b/src/ledger/LedgerStateSnapshot.h @@ -27,13 +27,13 @@ class LedgerEntryWrapper { // Either hold a reference or a pointer to the entry std::variant> + std::shared_ptr> mEntry; public: explicit LedgerEntryWrapper(ConstLedgerTxnEntry&& entry); explicit LedgerEntryWrapper(LedgerTxnEntry&& entry); - explicit LedgerEntryWrapper(std::shared_ptr entry); + explicit LedgerEntryWrapper(std::shared_ptr entry); LedgerEntry const& current() const; operator bool() const; }; diff --git a/src/ledger/LedgerTxn.cpp b/src/ledger/LedgerTxn.cpp index 7eee5b4627..19228103df 100644 --- a/src/ledger/LedgerTxn.cpp +++ b/src/ledger/LedgerTxn.cpp @@ -2721,7 +2721,7 @@ BulkLedgerEntryChangeAccumulator::accumulate(EntryIterator const& iter) // Don't accumulate entry types that are supported by BucketListDB auto type = iter.key().ledgerKey().type(); - if (!BucketIndex::typeNotSupported(type)) + if (!LiveBucketIndex::typeNotSupported(type)) { return false; } diff --git a/src/ledger/LedgerTypeUtils.cpp b/src/ledger/LedgerTypeUtils.cpp index b7cec7c68f..0fe657340d 100644 --- a/src/ledger/LedgerTypeUtils.cpp +++ b/src/ledger/LedgerTypeUtils.cpp @@ -5,8 +5,6 @@ #include "ledger/LedgerTypeUtils.h" #include "crypto/SHA.h" #include "util/GlobalChecks.h" -#include "util/UnorderedMap.h" -#include "util/UnorderedSet.h" #include "util/types.h" #include @@ -35,92 +33,4 @@ getTTLKey(LedgerKey const& e) k.ttl().keyHash = sha256(xdr::xdr_to_opaque(e)); return k; } - -LedgerEntryTypeAndDurability -bucketEntryToLedgerEntryAndDurabilityType(BucketEntry const& be) -{ - auto bet = be.type(); - LedgerEntryType let; - bool isTemp = false; - if (bet == INITENTRY || bet == LIVEENTRY) - { - auto le = be.liveEntry().data; - let = le.type(); - isTemp = isTemporaryEntry(le); - } - else if (bet == DEADENTRY) - { - auto lk = be.deadEntry(); - let = lk.type(); - isTemp = isTemporaryEntry(lk); - } - else - { - auto label = xdr::xdr_traits::enum_name(bet); - throw std::runtime_error( - fmt::format("Unsupported BucketEntryType {}", label)); - } - switch (let) - { - case ACCOUNT: - return LedgerEntryTypeAndDurability::ACCOUNT; - case TRUSTLINE: - return LedgerEntryTypeAndDurability::TRUSTLINE; - case OFFER: - return LedgerEntryTypeAndDurability::OFFER; - case DATA: - return LedgerEntryTypeAndDurability::DATA; - case CLAIMABLE_BALANCE: - return LedgerEntryTypeAndDurability::CLAIMABLE_BALANCE; - case LIQUIDITY_POOL: - return LedgerEntryTypeAndDurability::LIQUIDITY_POOL; - case CONTRACT_DATA: - return isTemp ? LedgerEntryTypeAndDurability::TEMPORARY_CONTRACT_DATA - : LedgerEntryTypeAndDurability::PERSISTENT_CONTRACT_DATA; - case CONTRACT_CODE: - return LedgerEntryTypeAndDurability::CONTRACT_CODE; - case CONFIG_SETTING: - return LedgerEntryTypeAndDurability::CONFIG_SETTING; - case TTL: - return LedgerEntryTypeAndDurability::TTL; - default: - auto label = xdr::xdr_traits::enum_name(let); - throw std::runtime_error( - fmt::format("Unknown LedgerEntryType {}", label)); - } -} - -std::string -toString(LedgerEntryTypeAndDurability const type) -{ - switch (type) - { - case LedgerEntryTypeAndDurability::ACCOUNT: - return "ACCOUNT"; - case LedgerEntryTypeAndDurability::TRUSTLINE: - return "TRUSTLINE"; - case LedgerEntryTypeAndDurability::OFFER: - return "OFFER"; - case LedgerEntryTypeAndDurability::DATA: - return "DATA"; - case LedgerEntryTypeAndDurability::CLAIMABLE_BALANCE: - return "CLAIMABLE_BALANCE"; - case LedgerEntryTypeAndDurability::LIQUIDITY_POOL: - return "LIQUIDITY_POOL"; - case LedgerEntryTypeAndDurability::TEMPORARY_CONTRACT_DATA: - return "TEMPORARY_CONTRACT_DATA"; - case LedgerEntryTypeAndDurability::PERSISTENT_CONTRACT_DATA: - return "PERSISTENT_CONTRACT_DATA"; - case LedgerEntryTypeAndDurability::CONTRACT_CODE: - return "CONTRACT_CODE"; - case LedgerEntryTypeAndDurability::CONFIG_SETTING: - return "CONFIG_SETTING"; - case LedgerEntryTypeAndDurability::TTL: - return "TTL"; - default: - throw std::runtime_error( - fmt::format(FMT_STRING("unknown LedgerEntryTypeAndDurability {:d}"), - static_cast(type))); - } -} }; \ No newline at end of file diff --git a/src/ledger/LedgerTypeUtils.h b/src/ledger/LedgerTypeUtils.h index 7cacc2ff30..ee6692fe5e 100644 --- a/src/ledger/LedgerTypeUtils.h +++ b/src/ledger/LedgerTypeUtils.h @@ -80,24 +80,4 @@ isPersistentEntry(T const& e) (e.type() == CONTRACT_DATA && e.contractData().durability == PERSISTENT); } - -enum class LedgerEntryTypeAndDurability : uint32_t -{ - ACCOUNT = 0, - TRUSTLINE = 1, - OFFER = 2, - DATA = 3, - CLAIMABLE_BALANCE = 4, - LIQUIDITY_POOL = 5, - TEMPORARY_CONTRACT_DATA = 6, - PERSISTENT_CONTRACT_DATA = 7, - CONTRACT_CODE = 8, - CONFIG_SETTING = 9, - TTL = 10, - NUM_TYPES = 11, -}; - -LedgerEntryTypeAndDurability -bucketEntryToLedgerEntryAndDurabilityType(BucketEntry const& be); -std::string toString(LedgerEntryTypeAndDurability let); } \ No newline at end of file diff --git a/src/main/AppConnector.h b/src/main/AppConnector.h index e01a940c10..756a768bba 100644 --- a/src/main/AppConnector.h +++ b/src/main/AppConnector.h @@ -1,6 +1,6 @@ #pragma once -#include "bucket/BucketSnapshotManager.h" +#include "bucket/BucketUtils.h" #include "main/Config.h" #include "medida/metrics_registry.h" diff --git a/src/main/Config.cpp b/src/main/Config.cpp index e06ea891ae..095cc49178 100644 --- a/src/main/Config.cpp +++ b/src/main/Config.cpp @@ -161,7 +161,7 @@ Config::Config() : NODE_SEED(SecretKey::random()) BACKGROUND_OVERLAY_PROCESSING = true; EXPERIMENTAL_PARALLEL_LEDGER_CLOSE = false; BUCKETLIST_DB_INDEX_PAGE_SIZE_EXPONENT = 14; // 2^14 == 16 kb - BUCKETLIST_DB_INDEX_CUTOFF = 20; // 20 mb + BUCKETLIST_DB_INDEX_CUTOFF = 250; // 250 mb BUCKETLIST_DB_PERSIST_INDEX = true; PUBLISH_TO_ARCHIVE_DELAY = std::chrono::seconds{0}; // automatic maintenance settings: diff --git a/src/test/TestUtils.h b/src/test/TestUtils.h index c54c6cf600..6de2fd91bc 100644 --- a/src/test/TestUtils.h +++ b/src/test/TestUtils.h @@ -53,7 +53,7 @@ testBucketMetadata(uint32_t protocolVersion) meta.ledgerVersion = protocolVersion; if (protocolVersionStartsFrom( protocolVersion, - BucketBase::FIRST_PROTOCOL_SUPPORTING_PERSISTENT_EVICTION)) + HotArchiveBucket::FIRST_PROTOCOL_SUPPORTING_PERSISTENT_EVICTION)) { meta.ext.v(1); meta.ext.bucketListType() = BucketListType::LIVE; diff --git a/src/transactions/FeeBumpTransactionFrame.cpp b/src/transactions/FeeBumpTransactionFrame.cpp index f8ad199508..13133648fa 100644 --- a/src/transactions/FeeBumpTransactionFrame.cpp +++ b/src/transactions/FeeBumpTransactionFrame.cpp @@ -11,6 +11,7 @@ #include "ledger/LedgerTxn.h" #include "ledger/LedgerTxnEntry.h" #include "ledger/LedgerTxnHeader.h" +#include "main/AppConnector.h" #include "main/Application.h" #include "transactions/MutableTransactionResult.h" #include "transactions/SignatureChecker.h" diff --git a/src/transactions/RestoreFootprintOpFrame.cpp b/src/transactions/RestoreFootprintOpFrame.cpp index e17fa9cec3..313d74b6a9 100644 --- a/src/transactions/RestoreFootprintOpFrame.cpp +++ b/src/transactions/RestoreFootprintOpFrame.cpp @@ -78,7 +78,7 @@ RestoreFootprintOpFrame::doApply( rustEntryRentChanges.reserve(footprint.readWrite.size()); for (auto const& lk : footprint.readWrite) { - std::shared_ptr hotArchiveEntry{nullptr}; + std::shared_ptr hotArchiveEntry{nullptr}; auto ttlKey = getTTLKey(lk); { // First check the live BucketList diff --git a/src/transactions/TransactionUtils.h b/src/transactions/TransactionUtils.h index 5c051fe1e0..2963fccf13 100644 --- a/src/transactions/TransactionUtils.h +++ b/src/transactions/TransactionUtils.h @@ -4,7 +4,6 @@ // under the Apache License, Version 2.0. See the COPYING file at the root // of this distribution or at http://www.apache.org/licenses/LICENSE-2.0 -#include "main/AppConnector.h" #include "util/NonCopyable.h" #include "util/ProtocolVersion.h" #include "xdr/Stellar-ledger-entries.h" diff --git a/src/transactions/test/InvokeHostFunctionTests.cpp b/src/transactions/test/InvokeHostFunctionTests.cpp index 58ed5998b8..ec5a98275c 100644 --- a/src/transactions/test/InvokeHostFunctionTests.cpp +++ b/src/transactions/test/InvokeHostFunctionTests.cpp @@ -2676,7 +2676,7 @@ TEST_CASE_VERSIONS("entry eviction", "[tx][soroban][archival]") if (protocolVersionStartsFrom( cfg.TESTING_UPGRADE_LEDGER_PROTOCOL_VERSION, - BucketBase::FIRST_PROTOCOL_SUPPORTING_PERSISTENT_EVICTION)) + LiveBucket::FIRST_PROTOCOL_SUPPORTING_PERSISTENT_EVICTION)) { LedgerTxn ltx(test.getApp().getLedgerTxnRoot()); REQUIRE(!ltx.load(persistentKey)); @@ -2699,7 +2699,7 @@ TEST_CASE_VERSIONS("entry eviction", "[tx][soroban][archival]") // Only support persistent eviction meta >= p23 if (protocolVersionStartsFrom( cfg.TESTING_UPGRADE_LEDGER_PROTOCOL_VERSION, - BucketBase:: + LiveBucket:: FIRST_PROTOCOL_SUPPORTING_PERSISTENT_EVICTION)) { // TLL and data key should both be in "deleted" @@ -2741,7 +2741,7 @@ TEST_CASE_VERSIONS("entry eviction", "[tx][soroban][archival]") if (protocolVersionStartsFrom( cfg.TESTING_UPGRADE_LEDGER_PROTOCOL_VERSION, - BucketBase:: + LiveBucket:: FIRST_PROTOCOL_SUPPORTING_PERSISTENT_EVICTION)) { REQUIRE(evicted); @@ -2793,7 +2793,7 @@ TEST_CASE_VERSIONS("entry eviction", "[tx][soroban][archival]") LedgerKey lk; if (protocolVersionStartsFrom( cfg.TESTING_UPGRADE_LEDGER_PROTOCOL_VERSION, - BucketBase:: + LiveBucket:: FIRST_PROTOCOL_SUPPORTING_PERSISTENT_EVICTION)) { REQUIRE(change.type() == diff --git a/src/util/BinaryFuseFilter.h b/src/util/BinaryFuseFilter.h index 30e22b522e..4f1e0fca0a 100644 --- a/src/util/BinaryFuseFilter.h +++ b/src/util/BinaryFuseFilter.h @@ -10,7 +10,6 @@ #include "xdr/Stellar-types.h" #include -#include namespace stellar { diff --git a/src/util/test/BinaryFuseTests.cpp b/src/util/test/BinaryFuseTests.cpp index d098746b25..6201215b5d 100644 --- a/src/util/test/BinaryFuseTests.cpp +++ b/src/util/test/BinaryFuseTests.cpp @@ -5,6 +5,7 @@ #include "crypto/ShortHash.h" #include "lib/catch.hpp" #include "util/BinaryFuseFilter.h" +#include "util/XDRCereal.h" #include "util/types.h" #include "xdr/Stellar-types.h" #include @@ -12,7 +13,6 @@ #include #include #include -#include using namespace stellar;