Skip to content

Commit 91a5bbc

Browse files
AlnisMmeta-codesync[bot]
authored andcommitted
DRAM LockGroupIterator
Summary: Lock once, iterate over all buckets belonging to the lock. Reviewed By: rlyerly Differential Revision: D100740472 fbshipit-source-id: acb3a5713f851c16e1ec07c639956ec0e1dd9a48
1 parent 99ec81e commit 91a5bbc

6 files changed

Lines changed: 621 additions & 0 deletions

File tree

cachelib/allocator/CacheAllocator.h

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -652,6 +652,34 @@ class CacheAllocator : public CacheBase {
652652

653653
AccessIterator end() { return accessContainer_->end(); }
654654

655+
// Alternative iterator that batches by hash table lock group instead of
656+
// per-bucket.
657+
//
658+
// Differences from AccessIterator:
659+
// - Fewer lock acquisitions: O(numLocks) vs O(numBuckets).
660+
// - Larger pinning window: each lock group covers numBuckets/numLocks
661+
// buckets. All items across those buckets are snapshotted as Handles
662+
// at once, blocking eviction until the caller advances past them.
663+
// AccessIterator snapshots one bucket at a time.
664+
using LockGroupAccessIterator = typename AccessContainer::LockGroupIterator;
665+
666+
LockGroupAccessIterator beginLockGroup() {
667+
return accessContainer_->beginLockGroup(
668+
[this](Item* it) { return tryAcquire(it); },
669+
[this](Key key) -> WriteHandle { return findInternal(key); });
670+
}
671+
672+
LockGroupAccessIterator beginLockGroup(util::Throttler::Config config) {
673+
return accessContainer_->beginLockGroup(
674+
[this](Item* it) { return tryAcquire(it); },
675+
[this](Key key) -> WriteHandle { return findInternal(key); },
676+
config);
677+
}
678+
679+
LockGroupAccessIterator endLockGroup() {
680+
return accessContainer_->endLockGroup();
681+
}
682+
655683
enum class RemoveRes : uint8_t {
656684
kSuccess,
657685
kNotFoundInRam,

cachelib/allocator/ChainedHashTable.h

Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -624,6 +624,110 @@ class ChainedHashTable {
624624
Iterator begin() { return Iterator(*this); }
625625
Iterator end() { return Iterator(*this, Iterator::EndIter); }
626626

627+
// Like Iterator, but batches by lock group instead of per-bucket.
628+
// Acquires each lock once and snapshots handles for all buckets under
629+
// that lock, reducing the total number of lock acquisitions from
630+
// O(numBuckets) to O(numLocks). Uses non-blocking handle creation
631+
// under the lock with retry outside, avoiding the deadlock where
632+
// blocking handleMaker_ waits for item moves while holding the lock.
633+
//
634+
// Trade-off: larger snapshot per lock group means more items are pinned
635+
// (not evictable) at once compared to the per-bucket Iterator.
636+
class LockGroupIterator {
637+
public:
638+
using TryHandleMakerFn =
639+
std::function<std::pair<Handle, TryAcquireResult>(T*)>;
640+
using FindByKeyFn = std::function<Handle(folly::StringPiece)>;
641+
642+
~LockGroupIterator() {
643+
XDCHECK_GT(container_->numIterators_.load(), 0u);
644+
--container_->numIterators_;
645+
}
646+
LockGroupIterator(const LockGroupIterator&) = delete;
647+
LockGroupIterator& operator=(const LockGroupIterator&) = delete;
648+
649+
LockGroupIterator(LockGroupIterator&&) noexcept;
650+
LockGroupIterator& operator=(LockGroupIterator&&) noexcept;
651+
enum EndIterT { EndIter };
652+
653+
LockGroupIterator& operator++();
654+
655+
T& operator*();
656+
T* operator->() { return &(*(*this)); }
657+
const T& operator*() const;
658+
const T* operator->() const { return &(*(*this)); }
659+
660+
bool operator==(const LockGroupIterator& other) const noexcept {
661+
return container_ == other.container_ && currLock_ == other.currLock_ &&
662+
cursor_ == other.cursor_;
663+
}
664+
665+
bool operator!=(const LockGroupIterator& other) const noexcept {
666+
return !(*this == other);
667+
}
668+
669+
const Handle& asHandle() { return curr(); }
670+
671+
void reset();
672+
673+
private:
674+
using C = Container<T, HookPtr, LockT>;
675+
676+
friend C;
677+
explicit LockGroupIterator(C& ht,
678+
TryHandleMakerFn tryHandleMaker,
679+
FindByKeyFn findByKey,
680+
folly::Optional<util::Throttler::Config>
681+
throttlerConfig = folly::none);
682+
683+
LockGroupIterator(C& ht, EndIterT);
684+
685+
mutable C* container_;
686+
TryHandleMakerFn tryHandleMaker_;
687+
FindByKeyFn findByKey_;
688+
689+
// current lock group that the iterator is pointing to.
690+
mutable size_t currLock_{0};
691+
// cursor into the current lock group's snapshot.
692+
mutable unsigned int cursor_{0};
693+
// snapshot of handles for all items in the current lock group.
694+
mutable std::vector<Handle> lockGroupElems_;
695+
696+
folly::Optional<util::Throttler> throttler_ = folly::none;
697+
698+
Handle& curr() const {
699+
if (cursor_ < lockGroupElems_.size()) {
700+
return lockGroupElems_[cursor_];
701+
}
702+
throw std::logic_error(
703+
"LockGroupIterator in invalid state with cursor_: " +
704+
folly::to<std::string>(cursor_) + ", currLock_: " +
705+
folly::to<std::string>(currLock_) + ", total locks: " +
706+
folly::to<std::string>(container_->config_.getNumLocks()));
707+
}
708+
709+
// Returns handles for all items in the given lock group. Uses
710+
// non-blocking tryHandleMaker_ under the lock, then retries moving
711+
// items via findByKey_ outside it.
712+
std::vector<Handle> getLockGroupElems(size_t lockIdx);
713+
};
714+
715+
LockGroupIterator beginLockGroup(
716+
typename LockGroupIterator::TryHandleMakerFn tryHandleMaker,
717+
typename LockGroupIterator::FindByKeyFn findByKey,
718+
folly::Optional<util::Throttler::Config> throttlerConfig);
719+
720+
LockGroupIterator beginLockGroup(
721+
typename LockGroupIterator::TryHandleMakerFn tryHandleMaker,
722+
typename LockGroupIterator::FindByKeyFn findByKey) {
723+
return LockGroupIterator(*this, std::move(tryHandleMaker),
724+
std::move(findByKey));
725+
}
726+
727+
LockGroupIterator endLockGroup() {
728+
return LockGroupIterator(*this, LockGroupIterator::EndIter);
729+
}
730+
627731
// Stats describing the distribution of items (keys) in the hash table
628732
struct DistributionStats {
629733
uint64_t numKeys{0};
@@ -1320,4 +1424,201 @@ void ChainedHashTable::Container<T, HookPtr, LockT>::Iterator::reset() {
13201424
}
13211425
XDCHECK_EQ(0u, curSor_);
13221426
}
1427+
1428+
template <typename T,
1429+
typename ChainedHashTable::Hook<T> T::* HookPtr,
1430+
typename LockT>
1431+
std::vector<typename ChainedHashTable::Container<T, HookPtr, LockT>::Handle>
1432+
ChainedHashTable::Container<T, HookPtr, LockT>::LockGroupIterator::
1433+
getLockGroupElems(size_t lockIdx) {
1434+
std::vector<Handle> elems;
1435+
std::vector<std::string> retryKeys;
1436+
1437+
{
1438+
auto guard = container_->locks_.lockShared(lockIdx);
1439+
const auto numBuckets = container_->config_.getNumBuckets();
1440+
1441+
container_->locks_.forEachBucketForLock(
1442+
lockIdx, numBuckets, [this, &elems, &retryKeys](size_t bucket) {
1443+
container_->ht_.forEachBucketElem(
1444+
bucket, [this, &elems, &retryKeys](T* elem) {
1445+
try {
1446+
auto [h, tryRes] = tryHandleMaker_(elem);
1447+
if (tryRes == TryAcquireResult::kSuccess) {
1448+
elems.emplace_back(std::move(h));
1449+
} else if (tryRes == TryAcquireResult::kMoving) {
1450+
// Can't retry under the lock — findByKey_ may block on the
1451+
// move which needs exclusive access to this same lock
1452+
// group. Save the key and retry after releasing the lock.
1453+
auto key = elem->getKey();
1454+
retryKeys.emplace_back(key.data(), key.size());
1455+
}
1456+
// kSkip: handle not acquirable, skip it.
1457+
} catch (const std::exception&) {
1458+
// if we are not able to acquire a handle, skip over them.
1459+
}
1460+
});
1461+
});
1462+
}
1463+
1464+
// Retry items that were being moved. Now that we don't hold any lock,
1465+
// findByKey_ can safely block waiting for the move to complete.
1466+
for (auto& key : retryKeys) {
1467+
try {
1468+
auto h = findByKey_(folly::StringPiece(key));
1469+
if (h) {
1470+
elems.emplace_back(std::move(h));
1471+
}
1472+
} catch (const std::exception&) {
1473+
// if we are not able to acquire a handle, skip over them.
1474+
}
1475+
}
1476+
1477+
return elems;
1478+
}
1479+
1480+
template <typename T,
1481+
typename ChainedHashTable::Hook<T> T::* HookPtr,
1482+
typename LockT>
1483+
typename ChainedHashTable::Container<T, HookPtr, LockT>::LockGroupIterator&
1484+
ChainedHashTable::Container<T, HookPtr, LockT>::LockGroupIterator::
1485+
operator++() {
1486+
if (throttler_) {
1487+
throttler_->throttle();
1488+
}
1489+
1490+
// Release the handle we're advancing past so it no longer pins the item.
1491+
// This unblocks evictions and slab rebalances on iterated-past items
1492+
// before we move to the next lock group.
1493+
if (cursor_ < lockGroupElems_.size()) {
1494+
lockGroupElems_[cursor_].reset();
1495+
}
1496+
1497+
++cursor_;
1498+
if (cursor_ < lockGroupElems_.size()) {
1499+
return *this;
1500+
}
1501+
1502+
++currLock_;
1503+
for (; currLock_ < container_->config_.getNumLocks(); ++currLock_) {
1504+
lockGroupElems_ = getLockGroupElems(currLock_);
1505+
if (!lockGroupElems_.empty()) {
1506+
cursor_ = 0;
1507+
return *this;
1508+
} else if (throttler_) {
1509+
throttler_->throttle();
1510+
}
1511+
}
1512+
1513+
// reached the end
1514+
lockGroupElems_.clear();
1515+
cursor_ = 0;
1516+
return *this;
1517+
}
1518+
1519+
template <typename T,
1520+
typename ChainedHashTable::Hook<T> T::* HookPtr,
1521+
typename LockT>
1522+
T& ChainedHashTable::Container<T, HookPtr, LockT>::LockGroupIterator::
1523+
operator*() {
1524+
return *curr();
1525+
}
1526+
1527+
template <typename T,
1528+
typename ChainedHashTable::Hook<T> T::* HookPtr,
1529+
typename LockT>
1530+
const T&
1531+
ChainedHashTable::Container<T, HookPtr, LockT>::LockGroupIterator::operator*()
1532+
const {
1533+
return *curr();
1534+
}
1535+
1536+
template <typename T,
1537+
typename ChainedHashTable::Hook<T> T::* HookPtr,
1538+
typename LockT>
1539+
ChainedHashTable::Container<T, HookPtr, LockT>::LockGroupIterator::
1540+
LockGroupIterator(Container<T, HookPtr, LockT>& container,
1541+
TryHandleMakerFn tryHandleMaker,
1542+
FindByKeyFn findByKey,
1543+
folly::Optional<util::Throttler::Config> throttlerConfig)
1544+
: container_(&container),
1545+
tryHandleMaker_(std::move(tryHandleMaker)),
1546+
findByKey_(std::move(findByKey)) {
1547+
if (throttlerConfig) {
1548+
throttler_.assign(util::Throttler(*throttlerConfig));
1549+
}
1550+
1551+
++container_->numIterators_;
1552+
1553+
reset();
1554+
}
1555+
1556+
template <typename T,
1557+
typename ChainedHashTable::Hook<T> T::* HookPtr,
1558+
typename LockT>
1559+
ChainedHashTable::Container<T, HookPtr, LockT>::LockGroupIterator::
1560+
LockGroupIterator(Container<T, HookPtr, LockT>& container, EndIterT)
1561+
: container_(&container), currLock_{container_->config_.getNumLocks()} {
1562+
++container_->numIterators_;
1563+
XDCHECK_EQ(0u, cursor_);
1564+
}
1565+
1566+
template <typename T,
1567+
typename ChainedHashTable::Hook<T> T::* HookPtr,
1568+
typename LockT>
1569+
ChainedHashTable::Container<T, HookPtr, LockT>::LockGroupIterator::
1570+
LockGroupIterator(LockGroupIterator&& other) noexcept
1571+
: container_{other.container_},
1572+
tryHandleMaker_(std::move(other.tryHandleMaker_)),
1573+
findByKey_(std::move(other.findByKey_)),
1574+
currLock_{other.currLock_},
1575+
cursor_{other.cursor_},
1576+
lockGroupElems_(std::move(other.lockGroupElems_)),
1577+
throttler_(std::move(other.throttler_)) {
1578+
++container_->numIterators_;
1579+
}
1580+
1581+
template <typename T,
1582+
typename ChainedHashTable::Hook<T> T::* HookPtr,
1583+
typename LockT>
1584+
typename ChainedHashTable::Container<T, HookPtr, LockT>::LockGroupIterator&
1585+
ChainedHashTable::Container<T, HookPtr, LockT>::LockGroupIterator::operator=(
1586+
LockGroupIterator&& other) noexcept {
1587+
if (this != &other) {
1588+
this->~LockGroupIterator();
1589+
new (this) LockGroupIterator(std::move(other));
1590+
}
1591+
return *this;
1592+
}
1593+
1594+
template <typename T,
1595+
typename ChainedHashTable::Hook<T> T::* HookPtr,
1596+
typename LockT>
1597+
typename ChainedHashTable::Container<T, HookPtr, LockT>::LockGroupIterator
1598+
ChainedHashTable::Container<T, HookPtr, LockT>::beginLockGroup(
1599+
typename LockGroupIterator::TryHandleMakerFn tryHandleMaker,
1600+
typename LockGroupIterator::FindByKeyFn findByKey,
1601+
folly::Optional<util::Throttler::Config> throttlerConfig) {
1602+
return LockGroupIterator(*this, std::move(tryHandleMaker),
1603+
std::move(findByKey), throttlerConfig);
1604+
}
1605+
1606+
template <typename T,
1607+
typename ChainedHashTable::Hook<T> T::* HookPtr,
1608+
typename LockT>
1609+
void ChainedHashTable::Container<T, HookPtr, LockT>::LockGroupIterator::
1610+
reset() {
1611+
cursor_ = 0;
1612+
currLock_ = 0;
1613+
lockGroupElems_ = getLockGroupElems(currLock_);
1614+
while (lockGroupElems_.empty() &&
1615+
++currLock_ < container_->config_.getNumLocks()) {
1616+
if (throttler_) {
1617+
throttler_->throttle();
1618+
}
1619+
lockGroupElems_ = getLockGroupElems(currLock_);
1620+
}
1621+
XDCHECK_EQ(0u, cursor_);
1622+
}
1623+
13231624
} // namespace facebook::cachelib

cachelib/allocator/tests/AllocatorTypeTest.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,10 @@ TYPED_TEST(BaseAllocatorTest, IterateAndRemoveWithIter) {
149149
this->testIterateAndRemoveWithIter();
150150
}
151151

152+
TYPED_TEST(BaseAllocatorTest, IterateLockGroup) {
153+
this->testIterateLockGroup();
154+
}
155+
152156
TYPED_TEST(BaseAllocatorTest, IterateWithEvictions) {
153157
this->testIterateWithEvictions();
154158
}

cachelib/allocator/tests/BaseAllocatorTest.h

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2081,6 +2081,35 @@ class BaseAllocatorTest : public AllocatorTest<AllocatorT> {
20812081
}
20822082
}
20832083

2084+
// Verify that LockGroupAccessIterator visits every accessible item exactly
2085+
// once. Exercises CacheAllocator::beginLockGroup() / endLockGroup() and the
2086+
// tryHandleMaker / findByKey wiring inside CacheAllocator.
2087+
void testIterateLockGroup() {
2088+
typename AllocatorT::Config config;
2089+
config.setCacheSize(10 * Slab::kSize);
2090+
AllocatorT alloc(config);
2091+
const size_t numBytes = alloc.getCacheMemoryStats().ramCacheSize;
2092+
std::set<uint32_t> allocSizes{1024};
2093+
auto poolId = alloc.addPool("foobar", numBytes, allocSizes);
2094+
2095+
const unsigned int numItems = 100;
2096+
const uint32_t itemSize = 100;
2097+
std::set<std::string> expectedKeys;
2098+
for (unsigned int i = 0; i < numItems; ++i) {
2099+
const std::string key = "key_" + folly::to<std::string>(i);
2100+
auto handle = util::allocateAccessible(alloc, poolId, key, itemSize);
2101+
ASSERT_NE(nullptr, handle);
2102+
expectedKeys.insert(key);
2103+
}
2104+
2105+
std::set<std::string> visitedKeys;
2106+
for (auto it = alloc.beginLockGroup(); it != alloc.endLockGroup(); ++it) {
2107+
const bool inserted = visitedKeys.insert(it->getKey().str()).second;
2108+
ASSERT_TRUE(inserted);
2109+
}
2110+
ASSERT_EQ(expectedKeys, visitedKeys);
2111+
}
2112+
20842113
void testIterateWithEvictions() {
20852114
std::set<std::string> evictedKeys;
20862115
auto removeCb =

0 commit comments

Comments
 (0)