@@ -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
0 commit comments