Skip to content

Commit e59b1fe

Browse files
igchorvinser52
authored andcommitted
Shorten critical section in findEviction
Remove the item from mmContainer and drop the lock before attempting eviction. Use moving bit for synchronization in findEviction moving bit is used to give exclusive right to evict the item to a particular thread. Originially, there was an assumption that whoever marked the item as moving will try to free it until he succeeds. Since we don't want to do that in findEviction (potentially can take a long time) we need to make sure that unmarking is safe. This patch checks the flags after unmarking (atomically) and if ref is zero it also recyles the item. This is needed as there might be some concurrent thread releasing the item (and decrementing ref count). If moving bit is set, that thread would not free the memory back to allocator, resulting in memory leak on unmarkMoving().
1 parent c8dce0c commit e59b1fe

File tree

5 files changed

+80
-224
lines changed

5 files changed

+80
-224
lines changed

cachelib/allocator/CacheAllocator-inl.h

Lines changed: 66 additions & 185 deletions
Original file line numberDiff line numberDiff line change
@@ -1216,14 +1216,13 @@ bool CacheAllocator<CacheTrait>::addWaitContextForMovingItem(
12161216
}
12171217

12181218
template <typename CacheTrait>
1219-
template <typename ItemPtr>
12201219
typename CacheAllocator<CacheTrait>::WriteHandle
12211220
CacheAllocator<CacheTrait>::moveRegularItemOnEviction(
1222-
ItemPtr& oldItemPtr, WriteHandle& newItemHdl) {
1221+
Item& oldItem, WriteHandle& newItemHdl) {
1222+
XDCHECK(oldItem.isMoving());
12231223
// TODO: should we introduce new latency tracker. E.g. evictRegularLatency_
12241224
// ??? util::LatencyTracker tracker{stats_.evictRegularLatency_};
12251225

1226-
Item& oldItem = *oldItemPtr;
12271226
if (!oldItem.isAccessible() || oldItem.isExpired()) {
12281227
return {};
12291228
}
@@ -1279,7 +1278,7 @@ CacheAllocator<CacheTrait>::moveRegularItemOnEviction(
12791278
// it is unsafe to replace the old item with a new one, so we should
12801279
// also abort.
12811280
if (!accessContainer_->replaceIf(oldItem, *newItemHdl,
1282-
itemEvictionPredicate)) {
1281+
itemMovingPredicate)) {
12831282
return {};
12841283
}
12851284

@@ -1299,7 +1298,7 @@ CacheAllocator<CacheTrait>::moveRegularItemOnEviction(
12991298

13001299
// Inside the MM container's lock, this checks if the old item exists to
13011300
// make sure that no other thread removed it, and only then replaces it.
1302-
if (!replaceInMMContainer(oldItemPtr, *newItemHdl)) {
1301+
if (!replaceInMMContainer(oldItem, *newItemHdl)) {
13031302
accessContainer_->remove(*newItemHdl);
13041303
return {};
13051304
}
@@ -1490,42 +1489,52 @@ CacheAllocator<CacheTrait>::findEviction(TierId tid, PoolId pid, ClassId cid) {
14901489
++searchTries;
14911490
(*stats_.evictionAttempts)[pid][cid].inc();
14921491

1493-
Item* candidate = itr.get();
1492+
Item* toRecycle = itr.get();
1493+
1494+
Item* candidate =
1495+
toRecycle->isChainedItem()
1496+
? &toRecycle->asChainedItem().getParentItem(compressor_)
1497+
: toRecycle;
1498+
1499+
// make sure no other thead is evicting the item
1500+
if (candidate->getRefCount() != 0 || !candidate->markMoving()) {
1501+
++itr;
1502+
continue;
1503+
}
1504+
1505+
itr.destroy();
1506+
14941507
// for chained items, the ownership of the parent can change. We try to
14951508
// evict what we think as parent and see if the eviction of parent
14961509
// recycles the child we intend to.
1497-
1498-
ItemHandle toReleaseHandle = tryEvictToNextMemoryTier(tid, pid, itr);
1499-
bool movedToNextTier = false;
1500-
if(toReleaseHandle) {
1501-
movedToNextTier = true;
1502-
} else {
1503-
toReleaseHandle =
1504-
itr->isChainedItem()
1505-
? advanceIteratorAndTryEvictChainedItem(tid, pid, itr)
1506-
: advanceIteratorAndTryEvictRegularItem(tid, pid, mmContainer, itr);
1507-
}
1510+
auto toReleaseHandle =
1511+
evictNormalItem(*candidate, true /* skipIfTokenInvalid */);
1512+
auto ref = candidate->unmarkMoving();
15081513

1509-
if (toReleaseHandle) {
1510-
if (toReleaseHandle->hasChainedItem()) {
1514+
if (toReleaseHandle || ref == 0u) {
1515+
if (candidate->hasChainedItem()) {
15111516
(*stats_.chainedItemEvictions)[pid][cid].inc();
15121517
} else {
15131518
(*stats_.regularItemEvictions)[pid][cid].inc();
15141519
}
1520+
} else {
1521+
if (candidate->hasChainedItem()) {
1522+
stats_.evictFailParentAC.inc();
1523+
} else {
1524+
stats_.evictFailAC.inc();
1525+
}
1526+
}
15151527

1528+
if (toReleaseHandle) {
15161529
if (auto eventTracker = getEventTracker()) {
15171530
eventTracker->record(
15181531
AllocatorApiEvent::DRAM_EVICT, toReleaseHandle->getKey(),
15191532
AllocatorApiResult::EVICTED, toReleaseHandle->getSize(),
15201533
toReleaseHandle->getConfiguredTTL().count());
15211534
}
1522-
// Invalidate iterator since later on we may use this mmContainer
1523-
// again, which cannot be done unless we drop this iterator
1524-
itr.destroy();
15251535

1526-
// we must be the last handle and for chained items, this will be
1527-
// the parent.
1528-
XDCHECK(toReleaseHandle.get() == candidate || candidate->isChainedItem());
1536+
XDCHECK(toReleaseHandle.get() == candidate);
1537+
XDCHECK(toRecycle == candidate || toRecycle->isChainedItem());
15291538
XDCHECK_EQ(1u, toReleaseHandle->getRefCount());
15301539

15311540
// We manually release the item here because we don't want to
@@ -1541,16 +1550,21 @@ CacheAllocator<CacheTrait>::findEviction(TierId tid, PoolId pid, ClassId cid) {
15411550
// recycle the candidate.
15421551
if (ReleaseRes::kRecycled ==
15431552
releaseBackToAllocator(itemToRelease, RemoveContext::kEviction,
1544-
/* isNascent */ movedToNextTier, candidate)) {
1545-
return candidate;
1553+
/* isNascent */ false, toRecycle)) {
1554+
return toRecycle;
1555+
}
1556+
} else if (ref == 0u) {
1557+
// it's safe to recycle the item here as there are no more
1558+
// references and the item could not been marked as moving
1559+
// by other thread since it's detached from MMContainer.
1560+
if (ReleaseRes::kRecycled ==
1561+
releaseBackToAllocator(*candidate, RemoveContext::kEviction,
1562+
/* isNascent */ false, toRecycle)) {
1563+
return toRecycle;
15461564
}
15471565
}
15481566

1549-
// If we destroyed the itr to possibly evict and failed, we restart
1550-
// from the beginning again
1551-
if (!itr) {
1552-
itr.resetToBegin();
1553-
}
1567+
itr.resetToBegin();
15541568
}
15551569
return nullptr;
15561570
}
@@ -1604,24 +1618,23 @@ bool CacheAllocator<CacheTrait>::shouldWriteToNvmCacheExclusive(
16041618
}
16051619

16061620
template <typename CacheTrait>
1607-
template <typename ItemPtr>
16081621
typename CacheAllocator<CacheTrait>::WriteHandle
16091622
CacheAllocator<CacheTrait>::tryEvictToNextMemoryTier(
1610-
TierId tid, PoolId pid, ItemPtr& item) {
1611-
if(item->isChainedItem()) return {}; // TODO: We do not support ChainedItem yet
1612-
if(item->isExpired()) return acquire(item);
1623+
TierId tid, PoolId pid, Item& item) {
1624+
if(item.isChainedItem()) return {}; // TODO: We do not support ChainedItem yet
1625+
if(item.isExpired()) return acquire(&item);
16131626

16141627
TierId nextTier = tid; // TODO - calculate this based on some admission policy
16151628
while (++nextTier < getNumTiers()) { // try to evict down to the next memory tiers
16161629
// allocateInternal might trigger another eviction
16171630
auto newItemHdl = allocateInternalTier(nextTier, pid,
1618-
item->getKey(),
1619-
item->getSize(),
1620-
item->getCreationTime(),
1621-
item->getExpiryTime());
1631+
item.getKey(),
1632+
item.getSize(),
1633+
item.getCreationTime(),
1634+
item.getExpiryTime());
16221635

16231636
if (newItemHdl) {
1624-
XDCHECK_EQ(newItemHdl->getSize(), item->getSize());
1637+
XDCHECK_EQ(newItemHdl->getSize(), item.getSize());
16251638

16261639
return moveRegularItemOnEviction(item, newItemHdl);
16271640
}
@@ -1632,150 +1645,12 @@ CacheAllocator<CacheTrait>::tryEvictToNextMemoryTier(
16321645

16331646
template <typename CacheTrait>
16341647
typename CacheAllocator<CacheTrait>::WriteHandle
1635-
CacheAllocator<CacheTrait>::tryEvictToNextMemoryTier(Item* item) {
1636-
auto tid = getTierId(*item);
1637-
auto pid = allocator_[tid]->getAllocInfo(item->getMemory()).poolId;
1648+
CacheAllocator<CacheTrait>::tryEvictToNextMemoryTier(Item& item) {
1649+
auto tid = getTierId(item);
1650+
auto pid = allocator_[tid]->getAllocInfo(item.getMemory()).poolId;
16381651
return tryEvictToNextMemoryTier(tid, pid, item);
16391652
}
16401653

1641-
template <typename CacheTrait>
1642-
typename CacheAllocator<CacheTrait>::WriteHandle
1643-
CacheAllocator<CacheTrait>::advanceIteratorAndTryEvictRegularItem(
1644-
TierId tid, PoolId pid, MMContainer& mmContainer, EvictionIterator& itr) {
1645-
Item& item = *itr;
1646-
1647-
const bool evictToNvmCache = shouldWriteToNvmCache(item);
1648-
1649-
auto token = evictToNvmCache ? nvmCache_->createPutToken(item.getKey())
1650-
: typename NvmCacheT::PutToken{};
1651-
// record the in-flight eviciton. If not, we move on to next item to avoid
1652-
// stalling eviction.
1653-
if (evictToNvmCache && !token.isValid()) {
1654-
++itr;
1655-
stats_.evictFailConcurrentFill.inc();
1656-
return WriteHandle{};
1657-
}
1658-
1659-
// If there are other accessors, we should abort. Acquire a handle here since
1660-
// if we remove the item from both access containers and mm containers
1661-
// below, we will need a handle to ensure proper cleanup in case we end up
1662-
// not evicting this item
1663-
auto evictHandle = accessContainer_->removeIf(item, &itemEvictionPredicate);
1664-
1665-
if (!evictHandle) {
1666-
++itr;
1667-
stats_.evictFailAC.inc();
1668-
return evictHandle;
1669-
}
1670-
1671-
mmContainer.remove(itr);
1672-
XDCHECK_EQ(reinterpret_cast<uintptr_t>(evictHandle.get()),
1673-
reinterpret_cast<uintptr_t>(&item));
1674-
XDCHECK(!evictHandle->isInMMContainer());
1675-
XDCHECK(!evictHandle->isAccessible());
1676-
1677-
// If the item is now marked as moving, that means its corresponding slab is
1678-
// being released right now. So, we look for the next item that is eligible
1679-
// for eviction. It is safe to destroy the handle here since the moving bit
1680-
// is set. Iterator was already advance by the remove call above.
1681-
if (evictHandle->isMoving()) {
1682-
stats_.evictFailMove.inc();
1683-
return WriteHandle{};
1684-
}
1685-
1686-
// Invalidate iterator since later on if we are not evicting this
1687-
// item, we may need to rely on the handle we created above to ensure
1688-
// proper cleanup if the item's raw refcount has dropped to 0.
1689-
// And since this item may be a parent item that has some child items
1690-
// in this very same mmContainer, we need to make sure we drop this
1691-
// exclusive iterator so we can gain access to it when we're cleaning
1692-
// up the child items
1693-
itr.destroy();
1694-
1695-
// Ensure that there are no accessors after removing from the access
1696-
// container
1697-
XDCHECK(evictHandle->getRefCount() == 1);
1698-
1699-
if (evictToNvmCache && shouldWriteToNvmCacheExclusive(item)) {
1700-
XDCHECK(token.isValid());
1701-
nvmCache_->put(evictHandle, std::move(token));
1702-
}
1703-
return evictHandle;
1704-
}
1705-
1706-
template <typename CacheTrait>
1707-
typename CacheAllocator<CacheTrait>::WriteHandle
1708-
CacheAllocator<CacheTrait>::advanceIteratorAndTryEvictChainedItem(
1709-
TierId tid, PoolId pid, EvictionIterator& itr) {
1710-
XDCHECK(itr->isChainedItem());
1711-
1712-
ChainedItem* candidate = &itr->asChainedItem();
1713-
++itr;
1714-
1715-
// The parent could change at any point through transferChain. However, if
1716-
// that happens, we would realize that the releaseBackToAllocator return
1717-
// kNotRecycled and we would try another chained item, leading to transient
1718-
// failure.
1719-
auto& parent = candidate->getParentItem(compressor_);
1720-
1721-
const bool evictToNvmCache = shouldWriteToNvmCache(parent);
1722-
1723-
auto token = evictToNvmCache ? nvmCache_->createPutToken(parent.getKey())
1724-
: typename NvmCacheT::PutToken{};
1725-
1726-
// if token is invalid, return. iterator is already advanced.
1727-
if (evictToNvmCache && !token.isValid()) {
1728-
stats_.evictFailConcurrentFill.inc();
1729-
return WriteHandle{};
1730-
}
1731-
1732-
// check if the parent exists in the hashtable and refcount is drained.
1733-
auto parentHandle =
1734-
accessContainer_->removeIf(parent, &itemEvictionPredicate);
1735-
if (!parentHandle) {
1736-
stats_.evictFailParentAC.inc();
1737-
return parentHandle;
1738-
}
1739-
1740-
// Invalidate iterator since later on we may use the mmContainer
1741-
// associated with this iterator which cannot be done unless we
1742-
// drop this iterator
1743-
//
1744-
// This must be done once we know the parent is not nullptr.
1745-
// Since we can very well be the last holder of this parent item,
1746-
// which may have a chained item that is linked in this MM container.
1747-
itr.destroy();
1748-
1749-
// Ensure we have the correct parent and we're the only user of the
1750-
// parent, then free it from access container. Otherwise, we abort
1751-
XDCHECK_EQ(reinterpret_cast<uintptr_t>(&parent),
1752-
reinterpret_cast<uintptr_t>(parentHandle.get()));
1753-
XDCHECK_EQ(1u, parent.getRefCount());
1754-
1755-
removeFromMMContainer(*parentHandle);
1756-
1757-
XDCHECK(!parent.isInMMContainer());
1758-
XDCHECK(!parent.isAccessible());
1759-
1760-
// TODO: add multi-tier support (similar as for unchained items)
1761-
1762-
// We need to make sure the parent is not marked as moving
1763-
// and we're the only holder of the parent item. Safe to destroy the handle
1764-
// here since moving bit is set.
1765-
if (parentHandle->isMoving()) {
1766-
stats_.evictFailParentMove.inc();
1767-
return WriteHandle{};
1768-
}
1769-
1770-
if (evictToNvmCache && shouldWriteToNvmCacheExclusive(*parentHandle)) {
1771-
XDCHECK(token.isValid());
1772-
XDCHECK(parentHandle->hasChainedItem());
1773-
nvmCache_->put(parentHandle, std::move(token));
1774-
}
1775-
1776-
return parentHandle;
1777-
}
1778-
17791654
template <typename CacheTrait>
17801655
typename CacheAllocator<CacheTrait>::RemoveRes
17811656
CacheAllocator<CacheTrait>::remove(typename Item::Key key) {
@@ -3043,7 +2918,7 @@ void CacheAllocator<CacheTrait>::evictForSlabRelease(
30432918
auto owningHandle =
30442919
item.isChainedItem()
30452920
? evictChainedItemForSlabRelease(item.asChainedItem())
3046-
: evictNormalItemForSlabRelease(item);
2921+
: evictNormalItem(item);
30472922

30482923
// we managed to evict the corresponding owner of the item and have the
30492924
// last handle for the owner.
@@ -3100,14 +2975,15 @@ void CacheAllocator<CacheTrait>::evictForSlabRelease(
31002975

31012976
template <typename CacheTrait>
31022977
typename CacheAllocator<CacheTrait>::WriteHandle
3103-
CacheAllocator<CacheTrait>::evictNormalItemForSlabRelease(Item& item) {
2978+
CacheAllocator<CacheTrait>::evictNormalItem(Item& item,
2979+
bool skipIfTokenInvalid) {
31042980
XDCHECK(item.isMoving());
31052981

31062982
if (item.isOnlyMoving()) {
31072983
return WriteHandle{};
31082984
}
31092985

3110-
auto evictHandle = tryEvictToNextMemoryTier(&item);
2986+
auto evictHandle = tryEvictToNextMemoryTier(item);
31112987
if(evictHandle) return evictHandle;
31122988

31132989
auto predicate = [](const Item& it) { return it.getRefCount() == 0; };
@@ -3116,6 +2992,11 @@ CacheAllocator<CacheTrait>::evictNormalItemForSlabRelease(Item& item) {
31162992
auto token = evictToNvmCache ? nvmCache_->createPutToken(item.getKey())
31172993
: typename NvmCacheT::PutToken{};
31182994

2995+
if (skipIfTokenInvalid && evictToNvmCache && !token.isValid()) {
2996+
stats_.evictFailConcurrentFill.inc();
2997+
return WriteHandle{};
2998+
}
2999+
31193000
// We remove the item from both access and mm containers. It doesn't matter
31203001
// if someone else calls remove on the item at this moment, the item cannot
31213002
// be freed as long as we have the moving bit set.

0 commit comments

Comments
 (0)