fix: reuse visited lists across sub-pools#2435
Conversation
Signed-off-by: jac0626 <jac0626@users.noreply.github.com> Assisted-by: Codex:gpt-5
|
/label S-waiting-on-review |
Merge Protections🟢 All 2 merge protections satisfied — ready to merge. Show 2 satisfied protections🟢 Require kind label
🟢 Require version label
|
There was a problem hiding this comment.
Code Review
This pull request updates the object retrieval logic in ResourceObjectPool to prioritize reusing existing objects across all sub-pools before constructing new ones, and adds a unit test to verify this behavior. However, the current implementation introduces a critical concurrency issue: if sub-pools are locked by other threads, try_lock() fails, causing the outer while (true) loop to busy-spin and consume 100% CPU. It is recommended to replace this loop with a single-pass check over the sub-pools, falling back to constructing a new object if no existing objects can be safely retrieved.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| while (true) { | ||
| if (sub_pool_mutexes_[pool_id].try_lock()) { | ||
| prefer_pool_id_ = pool_id; | ||
| if (pool_[pool_id]->empty()) { | ||
| sub_pool_mutexes_[pool_id].unlock(); | ||
| auto obj = this->constructor_(); | ||
| obj->source_pool_id_ = pool_id; | ||
| return obj; | ||
| uint64_t empty_pool_count = 0; | ||
| for (uint64_t i = 0; i < kSubPoolCount; ++i) { | ||
| if (sub_pool_mutexes_[pool_id].try_lock()) { | ||
| if (pool_[pool_id]->empty()) { | ||
| sub_pool_mutexes_[pool_id].unlock(); | ||
| ++empty_pool_count; | ||
| } else { | ||
| std::shared_ptr<T> obj = pool_[pool_id]->front(); | ||
| pool_[pool_id]->pop_front(); | ||
| sub_pool_mutexes_[pool_id].unlock(); | ||
| prefer_pool_id_ = pool_id; | ||
| obj->source_pool_id_ = pool_id; | ||
| obj->Reset(); | ||
| return obj; | ||
| } | ||
| } | ||
| std::shared_ptr<T> obj = pool_[pool_id]->front(); | ||
| pool_[pool_id]->pop_front(); | ||
| sub_pool_mutexes_[pool_id].unlock(); | ||
| obj->source_pool_id_ = pool_id; | ||
| obj->Reset(); | ||
|
|
||
| ++pool_id; | ||
| pool_id %= kSubPoolCount; | ||
| } | ||
| if (empty_pool_count == kSubPoolCount) { | ||
| auto obj = this->constructor_(); | ||
| obj->source_pool_id_ = prefer_pool_id_; | ||
| return obj; | ||
| } | ||
| ++pool_id; | ||
| pool_id %= kSubPoolCount; | ||
| } |
There was a problem hiding this comment.
Potential Livelock / High CPU Usage Under Concurrency
In the current implementation, if any sub-pool is locked by another thread (which is highly likely under concurrent access), try_lock() will fail, and empty_pool_count will not reach kSubPoolCount. This causes the outer while (true) loop to busy-spin indefinitely or for a very long time, consuming 100% CPU.
Solution
Instead of a busy-spin while (true) loop, we can perform a single pass over all sub-pools. If we find an available object in any sub-pool we can lock, we reuse it. If we finish the pass and find no available objects (either because they are empty or because they are locked), we safely allocate a new object. This completely avoids busy-spinning while still satisfying the goal of reusing objects across sub-pools before allocating new ones.
for (uint64_t i = 0; i < kSubPoolCount; ++i) {
if (sub_pool_mutexes_[pool_id].try_lock()) {
if (!pool_[pool_id]->empty()) {
std::shared_ptr<T> obj = pool_[pool_id]->front();
pool_[pool_id]->pop_front();
sub_pool_mutexes_[pool_id].unlock();
prefer_pool_id_ = pool_id;
obj->source_pool_id_ = pool_id;
obj->Reset();
return obj;
}
sub_pool_mutexes_[pool_id].unlock();
}
++pool_id;
pool_id %= kSubPoolCount;
}
auto obj = this->constructor_();
obj->source_pool_id_ = prefer_pool_id_;
return obj;There was a problem hiding this comment.
Pull request overview
This PR improves ResourceObjectPool’s allocation policy so that when a thread’s preferred sub-pool is empty, the pool will first attempt to reuse an idle object from other sub-pools before allocating a new VisitedList. This reduces peak scratch-memory growth in HGraph search workloads without changing the public API/ABI.
Changes:
- Update
ResourceObjectPool::TakeOne()to scan all sub-pools for an available object before constructing a new one. - Add a regression test that fails under the old “allocate immediately when preferred sub-pool is empty” behavior and passes with cross-sub-pool reuse.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| src/utils/resource_object_pool.h | Implements cross-sub-pool reuse by scanning sub-pools before allocating a new resource object. |
| src/utils/visited_list_test.cpp | Adds a regression test validating that no additional VisitedList allocations occur when other sub-pools still have idle objects. |
Signed-off-by: jac0626 <jac0626@users.noreply.github.com> Assisted-by: Codex:gpt-5
Change Type
Linked Issue
What Changed
Test Evidence
Test details:
Compatibility Impact
Performance and Concurrency Impact
try_lockbehaviorDocumentation Impact
Risk and Rollback
Checklist
Drafted with AI assistant: Codex:gpt-5