Skip to content

refactor: improve unordered_lru_cache#6965

Open
PastaPastaPasta wants to merge 4 commits into
dashpay:developfrom
PastaPastaPasta:refac/return-by-opt-value-lru-cache
Open

refactor: improve unordered_lru_cache#6965
PastaPastaPasta wants to merge 4 commits into
dashpay:developfrom
PastaPastaPasta:refac/return-by-opt-value-lru-cache

Conversation

@PastaPastaPasta
Copy link
Copy Markdown
Member

What was done?

  • return optional, instead of ref, and bool
  • add (generated) unit tests
  • improve truncate_if_needed to use std::nth_element instead of std::sort for O(N) average complexity instead of O(N log N).

How Has This Been Tested?

Compiles

Breaking Changes

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have made corresponding changes to the documentation
  • I have assigned this pull request to a milestone (for repository code-owners and collaborators only)

…onal

Change unordered_lru_cache::get() from returning bool with an output
parameter to returning std::optional<Value> by value. This provides a
more modern API that is easier to use and less error-prone.

- Add <optional> header to unordered_lru_cache.h
- Update get() signature: bool get(key, Value&) -> std::optional<Value> get(key)
- Update all 21 call sites across 11 files to use the new API
- Maintain same copy semantics for performance (no performance impact)

Files updated:
- src/unordered_lru_cache.h
- src/llmq/quorums.cpp (5 call sites)
- src/llmq/utils.cpp (2 call sites)
- src/llmq/snapshot.cpp, signing.cpp, dkgsessionmgr.cpp, blockprocessor.cpp
- src/instantsend/db.cpp (3 call sites)
- src/evo/mnhftx.cpp, creditpool.cpp, cbtx.cpp
- src/masternode/meta.cpp
Add Boost.Test suite with 17 test cases covering:
- Construction and configuration (runtime/compile-time sizing)
- Basic API behavior (insert, get, emplace, exists, erase, clear)
- LRU and truncation semantics (threshold behavior, recency tracking)
- Edge cases (maxSize=1, various threshold combinations)

Also optimize truncate_if_needed() to use std::nth_element instead
of std::sort for O(N) average complexity instead of O(N log N).
@PastaPastaPasta PastaPastaPasta added this to the 23.1 milestone Nov 12, 2025
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Nov 12, 2025

⚠️ Potential Merge Conflicts Detected

This PR has potential conflicts with the following open PRs:

Please coordinate with the authors of these PRs to avoid merge conflicts.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Nov 12, 2025

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.88% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'refactor: improve unordered_lru_cache' directly addresses the main change: refactoring and improving the unordered_lru_cache implementation through API changes and performance optimizations.
Description check ✅ Passed The description clearly outlines what was done (return optional, add unit tests, improve truncate_if_needed), how it was tested (compiles), and includes a checklist, all directly related to the changeset.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/llmq/quorums.cpp (1)

586-591: Don’t drop already cached quorums at genesis boundary.

When vecResultQuorums.back()->m_quorum_base_block_index->pprev is nullptr we’ve hit the beginning of the chain. Returning {} here discards the quorums we already collected from the cache, so callers get an empty result even though valid quorums exist. Instead of bailing out with an empty vector, return the quorums we have (or otherwise stop scanning without erasing them).

-            if (vecResultQuorums.back()->m_quorum_base_block_index->pprev == nullptr) return {};
+            if (vecResultQuorums.back()->m_quorum_base_block_index->pprev == nullptr) {
+                return vecResultQuorums;
+            }
🧹 Nitpick comments (1)
src/unordered_lru_cache.h (1)

60-68: Consider direct return for efficiency.

Line 65 uses std::make_optional(it->second.first) which creates an extra copy. You could return the value directly:

-            return std::make_optional(it->second.first);
+            return it->second.first;

When returning from a function that returns std::optional<T>, the compiler will automatically construct the optional from the value, avoiding the intermediate copy.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 368eebb and 4b85c94.

📒 Files selected for processing (14)
  • src/Makefile.test.include (1 hunks)
  • src/evo/cbtx.cpp (1 hunks)
  • src/evo/creditpool.cpp (2 hunks)
  • src/evo/mnhftx.cpp (1 hunks)
  • src/instantsend/db.cpp (3 hunks)
  • src/llmq/blockprocessor.cpp (1 hunks)
  • src/llmq/dkgsessionmgr.cpp (1 hunks)
  • src/llmq/quorums.cpp (5 hunks)
  • src/llmq/signing.cpp (3 hunks)
  • src/llmq/snapshot.cpp (1 hunks)
  • src/llmq/utils.cpp (2 hunks)
  • src/masternode/meta.cpp (1 hunks)
  • src/test/unordered_lru_cache_tests.cpp (1 hunks)
  • src/unordered_lru_cache.h (3 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
src/**/*.{cpp,h,cc,cxx,hpp}

📄 CodeRabbit inference engine (CLAUDE.md)

src/**/*.{cpp,h,cc,cxx,hpp}: Dash Core C++ codebase must be written in C++20 and require at least Clang 16 or GCC 11.1
Dash uses unordered_lru_cache for efficient caching with LRU eviction

Files:

  • src/masternode/meta.cpp
  • src/evo/cbtx.cpp
  • src/evo/creditpool.cpp
  • src/llmq/signing.cpp
  • src/llmq/utils.cpp
  • src/llmq/snapshot.cpp
  • src/unordered_lru_cache.h
  • src/test/unordered_lru_cache_tests.cpp
  • src/llmq/blockprocessor.cpp
  • src/llmq/quorums.cpp
  • src/llmq/dkgsessionmgr.cpp
  • src/evo/mnhftx.cpp
  • src/instantsend/db.cpp
src/{masternode,evo}/**/*.{cpp,h,cc,cxx,hpp}

📄 CodeRabbit inference engine (CLAUDE.md)

Masternode lists must use immutable data structures (Immer library) for thread safety

Files:

  • src/masternode/meta.cpp
  • src/evo/cbtx.cpp
  • src/evo/creditpool.cpp
  • src/evo/mnhftx.cpp
src/{test,wallet/test,qt/test}/**/*.{cpp,h,cc,cxx,hpp}

📄 CodeRabbit inference engine (CLAUDE.md)

Unit tests for C++ code should be placed in src/test/, src/wallet/test/, or src/qt/test/ and use Boost::Test or Qt 5 for GUI tests

Files:

  • src/test/unordered_lru_cache_tests.cpp
🧠 Learnings (7)
📓 Common learnings
Learnt from: kwvg
Repo: dashpay/dash PR: 6543
File: src/wallet/receive.cpp:240-251
Timestamp: 2025-02-06T14:34:30.466Z
Learning: Pull request #6543 is focused on move-only changes and refactoring, specifically backporting from Bitcoin. Behavior changes should be proposed in separate PRs.
📚 Learning: 2025-07-20T18:42:49.794Z
Learnt from: CR
Repo: dashpay/dash PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-07-20T18:42:49.794Z
Learning: Applies to src/{test,wallet/test,qt/test}/**/*.{cpp,h,cc,cxx,hpp} : Unit tests for C++ code should be placed in src/test/, src/wallet/test/, or src/qt/test/ and use Boost::Test or Qt 5 for GUI tests

Applied to files:

  • src/Makefile.test.include
  • src/test/unordered_lru_cache_tests.cpp
📚 Learning: 2025-08-11T17:16:36.654Z
Learnt from: PastaPastaPasta
Repo: dashpay/dash PR: 6804
File: src/qt/proposalwizard.cpp:40-42
Timestamp: 2025-08-11T17:16:36.654Z
Learning: In the Dash repository, when a PR adds new files that are not from Bitcoin backports, these files must be added to the list in test/util/data/non-backported.txt. This applies to newly created files like qt/proposalwizard.{h,cpp} and forms/proposalwizard.ui. Limited exemptions may exist for subtrees and similar cases.

Applied to files:

  • src/Makefile.test.include
📚 Learning: 2025-10-21T11:09:34.688Z
Learnt from: kwvg
Repo: dashpay/dash PR: 6849
File: src/governance/governance.cpp:1339-1343
Timestamp: 2025-10-21T11:09:34.688Z
Learning: In the Dash Core codebase, `CacheMap` (defined in src/cachemap.h) is internally thread-safe and uses its own `mutable CCriticalSection cs` to protect access to its members. Methods like `GetSize()`, `Insert()`, `Get()`, `HasKey()`, etc., can be called without holding external locks.

Applied to files:

  • src/evo/cbtx.cpp
  • src/evo/creditpool.cpp
  • src/test/unordered_lru_cache_tests.cpp
  • src/instantsend/db.cpp
📚 Learning: 2025-10-02T18:29:54.756Z
Learnt from: kwvg
Repo: dashpay/dash PR: 6840
File: src/net_processing.cpp:2882-2886
Timestamp: 2025-10-02T18:29:54.756Z
Learning: Across net_processing.cpp, once LLMQContext (m_llmq_ctx) is asserted non-null, its subcomponents (e.g., isman, qdkgsman, quorum_block_processor) are treated as initialized and used without extra null checks.

Applied to files:

  • src/evo/cbtx.cpp
  • src/llmq/quorums.cpp
  • src/llmq/dkgsessionmgr.cpp
📚 Learning: 2025-08-19T14:57:31.801Z
Learnt from: knst
Repo: dashpay/dash PR: 6692
File: src/llmq/blockprocessor.cpp:217-224
Timestamp: 2025-08-19T14:57:31.801Z
Learning: In PR #6692, knst acknowledged a null pointer dereference issue in ProcessBlock() method where LookupBlockIndex may return nullptr but is passed to gsl::not_null, and created follow-up PR #6789 to address it, consistent with avoiding scope creep in performance-focused PRs.

Applied to files:

  • src/evo/creditpool.cpp
  • src/llmq/blockprocessor.cpp
📚 Learning: 2025-07-20T18:42:49.794Z
Learnt from: CR
Repo: dashpay/dash PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-07-20T18:42:49.794Z
Learning: Applies to src/**/*.{cpp,h,cc,cxx,hpp} : Dash uses unordered_lru_cache for efficient caching with LRU eviction

Applied to files:

  • src/test/unordered_lru_cache_tests.cpp
🧬 Code graph analysis (2)
src/unordered_lru_cache.h (1)
src/script/descriptor.cpp (1)
  • nullopt (656-656)
src/llmq/dkgsessionmgr.cpp (2)
src/evo/mnhftx.h (1)
  • quorumHash (39-39)
src/llmq/commitment.h (1)
  • quorumIndex (54-54)
🪛 GitHub Actions: Clang Diff Format Check
src/llmq/utils.cpp

[error] 251-257: Clang format differences detected. The step 'git diff -U0 origin/develop -- $(git ls-files -- $(cat test/util/data/non-backported.txt)) | ./contrib/devtools/clang-format-diff.py -p1 > diff_output.txt' reported differences and exit code 1. Please run clang-format or apply the diffs to fix formatting issues.

🔇 Additional comments (10)
src/unordered_lru_cache.h (1)

105-112: LGTM! Excellent performance improvement.

The optimization from std::sort to std::nth_element is well-implemented. After partitioning, the first keep elements are guaranteed to be the most recently accessed entries, and erasing entries beyond that point is correct. This reduces the average time complexity from O(N log N) to O(N) for the eviction process.

src/instantsend/db.cpp (3)

274-278: LGTM! Clean optional-based cache lookup.

The early return pattern for cache hits is clean and efficient. The cache is consulted only when use_cache is true, and the cached value is returned immediately if present.


298-306: LGTM! Correct cache integration.

The cache lookup correctly returns early if a cached value is found. The DB read fallback and cache insertion are properly sequenced.


318-326: LGTM! Proper two-level cache lookup.

The function correctly uses the outpoint cache to retrieve the islock hash, then delegates to GetInstantSendLockByHashInternal for the full lock retrieval. The DB fallback and cache insertion logic are correct.

src/test/unordered_lru_cache_tests.cpp (1)

1-401: Excellent comprehensive test coverage!

The test suite thoroughly validates the unordered_lru_cache implementation across multiple dimensions:

  • Construction and configuration scenarios
  • Basic operations (insert, get, emplace, exists, erase, clear)
  • LRU eviction behavior and recency tracking
  • Edge cases (maxsize=1, threshold variations, multiple truncation cycles)
  • Value semantics (verifying returned values are copies)

The tests properly verify the new optional-based API and ensure correct LRU semantics.

src/Makefile.test.include (1)

199-199: LGTM! Test file properly added to build.

The new test file is correctly added to the BITCOIN_TESTS list in alphabetical order.

src/masternode/meta.cpp (1)

142-145: LGTM! Clean optional-based cache lookup.

The cache integration correctly uses the new optional API, returning the cached value if present or std::nullopt otherwise.

src/llmq/blockprocessor.cpp (1)

516-527: LGTM! Correct cache integration with proper locking.

The optional-based cache lookup is correct. The cache check is properly guarded by the lock, and the DB read fallback occurs outside the lock to minimize lock contention. The cache insertion before returning is correct.

src/llmq/snapshot.cpp (1)

355-357: LGTM! Clean optional-based cache integration.

The cache lookup correctly uses the new optional API with an early return on cache hit, followed by DB fallback and cache insertion.

src/evo/mnhftx.cpp (1)

328-330: LGTM! Correct optional-based cache lookup.

The cache integration properly uses the new optional API with an early return on cache hit. The multiple DB fallback paths with cache insertions are correctly implemented.

Copy link
Copy Markdown
Collaborator

@knst knst left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR is not ready yet IMO, I stopped reviewing in the middle

Also conflicts to #6963

Comment thread src/evo/creditpool.cpp
LOCK(cache_mutex);
if (creditPoolCache.get(block_hash, pool)) {
return pool;
if (auto cached = creditPoolCache.get(block_hash)) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should reduce scope of pool here

Comment thread src/evo/creditpool.cpp
static_cast<size_t>(Params().CreditPoolPeriodBlocks()) * 2};
if (LOCK(cache_mutex); block_data_cache.get(block_index->GetBlockHash(), blockData)) {
return blockData;
if (LOCK(cache_mutex); auto cached = block_data_cache.get(block_index->GetBlockHash())) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit(perf): should reduce scope of blockData here to avoid empty object creation

Comment thread src/instantsend/db.cpp Outdated
}
}

InstantSendLockPtr ret;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-    InstantSendLockPtr ret;
-    ret = std::make_shared<InstantSendLock>();
+     InstantSendLockPtr ret{std::make_shared<InstantSendLock>()};

Comment thread src/llmq/quorums.cpp
if (!cached.has_value()) {
return errorHandler("Quorum not found", 0); // Don't bump score because we asked for it
}
pQuorum = *cached;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use std::optional's feature value_or

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why / how?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why?

I gave a 2nd look, it just shared_ptr; so, copy is quite fast, no allocation, one atomic is relatively cheap.

Though, it could be:

-        CQuorumPtr pQuorum;
-        {
-            LOCK(cs_map_quorums);
-            auto cached = mapQuorumsCache[request.GetLLMQType()].get(request.GetQuorumHash());
-            if (!cached.has_value()) {
-                return errorHandler("Quorum not found", 0); // Don't bump score because we asked for it
-            }
-            pQuorum = *cached;
+        CQuorumPtr pQuorum = WITH_LOCK(cs_map_quorums, return mapQuorumsCache[request.GetLLMQType()].get(request.GetQuorumHash())).value_or(nullptr);
+        if (pQuorum == nullptr) {
+            return errorHandler("Quorum not found", 0); // Don't bump score because we asked for it
         }

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe compiler may optimize away this copy anyway as the obj was not initialized prior.

Comment thread src/evo/cbtx.cpp Outdated

std::pair<uint256, int> qc_hash;
if (!qc_hashes_cached[llmqType].get(block_hash, qc_hash)) {
if (auto cached = qc_hashes_cached[llmqType].get(block_hash)) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this particular case is not good anymore; here's 1 extra constructor of uint256 is called now.

once - default constructor.
After that one more in get()
and 3rd one is 'qc_hash=*cached'

Used to be only 2 constructors:

  1. default for qc_hash
  2. assignment in get()

std::optional supposed to decrease its amount, not increase!

Consider use std::optional's feature value_or

Comment thread src/llmq/quorums.cpp
bool fCacheExists = cache.get(pindexStore->GetBlockHash(), vecResultQuorums);
if (fCacheExists) {
if (auto cached = cache.get(pindexStore->GetBlockHash())) {
vecResultQuorums = *cached;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider using std::optional's feature value_or

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why? / how?

Copy link
Copy Markdown
Collaborator

@knst knst Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indeed, this place is not feasible; not easy to do due to if (...) and extra code inside

Consider vecResultQuorums = std::move(*cached)

@coderabbitai, check that std::move is working with std::optional's values?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uint256 is trivially copyable; there's no gain to move really

@PastaPastaPasta PastaPastaPasta requested a review from knst November 12, 2025 15:30
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/evo/cbtx.cpp (1)

95-97: Construct the cached pair in-place.
Switching to std::optional::emplace avoids creating an extra temporary std::pair<uint256, int> when we fill a miss, trimming one more uint256 construction at virtually no cost to readability.

-                qc_hash = {::SerializeHash(pqc), rotation_enabled ? pqc.quorumIndex : 0};
+                qc_hash.emplace(::SerializeHash(pqc), rotation_enabled ? pqc.quorumIndex : 0);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4b85c94 and aafef95.

📒 Files selected for processing (4)
  • src/evo/cbtx.cpp (1 hunks)
  • src/evo/creditpool.cpp (3 hunks)
  • src/instantsend/db.cpp (3 hunks)
  • src/test/unordered_lru_cache_tests.cpp (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • src/test/unordered_lru_cache_tests.cpp
🧰 Additional context used
📓 Path-based instructions (2)
src/**/*.{cpp,h,cc,cxx,hpp}

📄 CodeRabbit inference engine (CLAUDE.md)

src/**/*.{cpp,h,cc,cxx,hpp}: Dash Core C++ codebase must be written in C++20 and require at least Clang 16 or GCC 11.1
Dash uses unordered_lru_cache for efficient caching with LRU eviction

Files:

  • src/evo/cbtx.cpp
  • src/evo/creditpool.cpp
  • src/instantsend/db.cpp
src/{masternode,evo}/**/*.{cpp,h,cc,cxx,hpp}

📄 CodeRabbit inference engine (CLAUDE.md)

Masternode lists must use immutable data structures (Immer library) for thread safety

Files:

  • src/evo/cbtx.cpp
  • src/evo/creditpool.cpp
🧠 Learnings (4)
📓 Common learnings
Learnt from: kwvg
Repo: dashpay/dash PR: 6543
File: src/wallet/receive.cpp:240-251
Timestamp: 2025-02-06T14:34:30.466Z
Learning: Pull request #6543 is focused on move-only changes and refactoring, specifically backporting from Bitcoin. Behavior changes should be proposed in separate PRs.
📚 Learning: 2025-08-19T14:57:31.801Z
Learnt from: knst
Repo: dashpay/dash PR: 6692
File: src/llmq/blockprocessor.cpp:217-224
Timestamp: 2025-08-19T14:57:31.801Z
Learning: In PR #6692, knst acknowledged a null pointer dereference issue in ProcessBlock() method where LookupBlockIndex may return nullptr but is passed to gsl::not_null, and created follow-up PR #6789 to address it, consistent with avoiding scope creep in performance-focused PRs.

Applied to files:

  • src/evo/creditpool.cpp
📚 Learning: 2025-10-21T11:09:34.688Z
Learnt from: kwvg
Repo: dashpay/dash PR: 6849
File: src/governance/governance.cpp:1339-1343
Timestamp: 2025-10-21T11:09:34.688Z
Learning: In the Dash Core codebase, `CacheMap` (defined in src/cachemap.h) is internally thread-safe and uses its own `mutable CCriticalSection cs` to protect access to its members. Methods like `GetSize()`, `Insert()`, `Get()`, `HasKey()`, etc., can be called without holding external locks.

Applied to files:

  • src/instantsend/db.cpp
📚 Learning: 2025-09-09T21:36:11.833Z
Learnt from: kwvg
Repo: dashpay/dash PR: 6837
File: src/stats/rawsender.cpp:0-0
Timestamp: 2025-09-09T21:36:11.833Z
Learning: In RawSender class (src/stats/rawsender.cpp), cs_net is the appropriate mutex for protecting socket access (m_sock) and network operations, not additional custom locks. The implementation correctly uses cs_net with GUARDED_BY annotations and EXCLUSIVE_LOCKS_REQUIRED to synchronize socket access between SendDirectly() and ReconnectThread().

Applied to files:

  • src/instantsend/db.cpp
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (10)
  • GitHub Check: linux64_multiprocess-build / Build source
  • GitHub Check: mac-build / Build source
  • GitHub Check: linux64_tsan-build / Build source
  • GitHub Check: linux64_sqlite-build / Build source
  • GitHub Check: linux64_fuzz-build / Build source
  • GitHub Check: linux64-build / Build source
  • GitHub Check: linux64_ubsan-build / Build source
  • GitHub Check: linux64_nowallet-build / Build source
  • GitHub Check: arm-linux-build / Build source
  • GitHub Check: win64-build / Build source
🔇 Additional comments (1)
src/instantsend/db.cpp (1)

274-291: Good call on caching null hits.
Persisting the miss in islockCache means we stop hammering LevelDB for the same absent hash, while write paths still refresh the entry once an IS-lock arrives.

Copy link
Copy Markdown

@UdjinM6 UdjinM6 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM overall, one test feels incomplete though

Comment on lines +112 to +113
// Erasing non-existent key is a no-op
c.erase(999);
Copy link
Copy Markdown

@UdjinM6 UdjinM6 Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should probably insert another element e.g. (2, 20) at the start of this test and then check that it wasn't removed neither by erase(1) above nor by erase(999) here

@github-actions
Copy link
Copy Markdown

This pull request has conflicts, please rebase.

@UdjinM6 UdjinM6 removed this from the 23.1 milestone Feb 15, 2026
@thepastaclaw
Copy link
Copy Markdown

thepastaclaw commented Mar 30, 2026

✅ Review complete (commit aafef95)

Copy link
Copy Markdown

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Good refactor — std::optional return is cleaner than out-parameter, and std::nth_element is the right choice for O(N) truncation. All callers are correctly adapted. One UB issue: when truncateThreshold < maxSize and cache size is between the two, nth_element is called with nth == end(), which is undefined behavior.

Reviewed commit: aafef95

🔴 1 blocking

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `src/unordered_lru_cache.h`:
- [BLOCKING] lines 106-108: `std::nth_element` called with `nth == end()` — undefined behavior
  `truncate_if_needed()` runs when `cacheMap.size() > truncateThreshold`, and `keep` is computed as `std::min(maxSize, vec.size())`. When a cache is constructed with `truncateThreshold < maxSize` and its size is in `(truncateThreshold, maxSize]`, then `keep == vec.size()`, making the call `std::nth_element(vec.begin(), vec.end(), vec.end(), ...)`. The C++ standard requires `nth` to be in `[first, last)` — passing `end()` is undefined behavior.

The existing test `edge_case_threshold_less_than_maxsize` (constructing `unordered_lru_cache<int, int, IntHasher> c(10, 5)` and inserting 6 elements) exercises exactly this path but won't reliably catch the UB unless running under an STL debug mode.

The previous `std::sort` implementation handled this case safely since sorting an entire range is always valid.

Comment thread src/unordered_lru_cache.h
Comment on lines +106 to 108
std::nth_element(vec.begin(), vec.begin() + keep, vec.end(), [](const Iterator& it1, const Iterator& it2) {
return it1->second.second > it2->second.second;
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Blocking: std::nth_element called with nth == end() — undefined behavior

truncate_if_needed() runs when cacheMap.size() > truncateThreshold, and keep is computed as std::min(maxSize, vec.size()). When a cache is constructed with truncateThreshold < maxSize and its size is in (truncateThreshold, maxSize], then keep == vec.size(), making the call std::nth_element(vec.begin(), vec.end(), vec.end(), ...). The C++ standard requires nth to be in [first, last) — passing end() is undefined behavior.

The existing test edge_case_threshold_less_than_maxsize (constructing unordered_lru_cache<int, int, IntHasher> c(10, 5) and inserting 6 elements) exercises exactly this path but won't reliably catch the UB unless running under an STL debug mode.

The previous std::sort implementation handled this case safely since sorting an entire range is always valid.

💡 Suggested change
Suggested change
std::nth_element(vec.begin(), vec.begin() + keep, vec.end(), [](const Iterator& it1, const Iterator& it2) {
return it1->second.second > it2->second.second;
});
const size_t keep = std::min(maxSize, vec.size());
if (keep >= vec.size()) return;
std::nth_element(vec.begin(), vec.begin() + keep, vec.end(), [](const Iterator& it1, const Iterator& it2) {

source: ['codex']

🤖 Fix this with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `src/unordered_lru_cache.h`:
- [BLOCKING] lines 106-108: `std::nth_element` called with `nth == end()` — undefined behavior
  `truncate_if_needed()` runs when `cacheMap.size() > truncateThreshold`, and `keep` is computed as `std::min(maxSize, vec.size())`. When a cache is constructed with `truncateThreshold < maxSize` and its size is in `(truncateThreshold, maxSize]`, then `keep == vec.size()`, making the call `std::nth_element(vec.begin(), vec.end(), vec.end(), ...)`. The C++ standard requires `nth` to be in `[first, last)` — passing `end()` is undefined behavior.

The existing test `edge_case_threshold_less_than_maxsize` (constructing `unordered_lru_cache<int, int, IntHasher> c(10, 5)` and inserting 6 elements) exercises exactly this path but won't reliably catch the UB unless running under an STL debug mode.

The previous `std::sort` implementation handled this case safely since sorting an entire range is always valid.

Copy link
Copy Markdown

@thepastaclaw thepastaclaw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Clean, well-scoped refactor of unordered_lru_cache. Codex's blocking UB claim about nth_element with nth==last is a false positive — the C++17/20 standard explicitly allows this and specifies it has no effect, and the new test_threshold_less_than_maxsize test exercises the path successfully. Two minor cosmetic nitpicks remain.

Reviewed commit: aafef95

💬 2 nitpick(s)

Comment thread src/unordered_lru_cache.h
@@ -102,11 +102,12 @@ class unordered_lru_cache
vec.emplace_back(it);
}
// sort by last access time (descending order)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 Nitpick: Stale comment: nth_element partitions, it does not sort

The comment // sort by last access time (descending order) no longer reflects the operation after the switch from std::sort to std::nth_element. nth_element partitions and provides no ordering guarantee within either side, so the comment is misleading. The next comment (// Erase all but the first \keep` most recently accessed entries`) already conveys what is being done; consider removing or rewriting the stale one.

💡 Suggested change
Suggested change
// sort by last access time (descending order)
// partition so the `keep` most recently accessed entries land in [begin, begin + keep)

source: ['claude']

Comment on lines +520 to 522
bool fExists;

fExists = m_evoDb.Exists(std::make_pair(DB_MINED_COMMITMENT, std::make_pair(llmqType, quorumHash)));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💬 Nitpick: Combine split declaration and assignment of fExists

The declaration bool fExists; on line 520 is followed by a blank line and then assigned on line 522. This split was meaningful when fExists was an out-parameter to the old get() API, but after the refactor it can be a single initialized declaration, matching the pattern used in other call sites updated by this PR (e.g. signing.cpp).

💡 Suggested change
Suggested change
bool fExists;
fExists = m_evoDb.Exists(std::make_pair(DB_MINED_COMMITMENT, std::make_pair(llmqType, quorumHash)));
bool fExists = m_evoDb.Exists(std::make_pair(DB_MINED_COMMITMENT, std::make_pair(llmqType, quorumHash)));

source: ['claude']

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants