Skip to content

Commit 0f2fe81

Browse files
committed
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 c805e69 commit 0f2fe81

File tree

5 files changed

+84
-228
lines changed

5 files changed

+84
-228
lines changed

cachelib/allocator/CacheAllocator-inl.h

Lines changed: 70 additions & 190 deletions
Original file line numberDiff line numberDiff line change
@@ -415,7 +415,7 @@ CacheAllocator<CacheTrait>::allocateInternalTier(TierId tid,
415415
}
416416

417417
template <typename CacheTrait>
418-
typename CacheAllocator<CacheTrait>::ItemHandle
418+
typename CacheAllocator<CacheTrait>::WriteHandle
419419
CacheAllocator<CacheTrait>::allocateInternal(PoolId pid,
420420
typename Item::Key key,
421421
uint32_t size,
@@ -1186,14 +1186,13 @@ bool CacheAllocator<CacheTrait>::addWaitContextForMovingItem(
11861186
}
11871187

11881188
template <typename CacheTrait>
1189-
template <typename ItemPtr>
11901189
typename CacheAllocator<CacheTrait>::ItemHandle
11911190
CacheAllocator<CacheTrait>::moveRegularItemOnEviction(
1192-
ItemPtr& oldItemPtr, ItemHandle& newItemHdl) {
1191+
Item& oldItem, ItemHandle& newItemHdl) {
1192+
XDCHECK(oldItem.isMoving());
11931193
// TODO: should we introduce new latency tracker. E.g. evictRegularLatency_
11941194
// ??? util::LatencyTracker tracker{stats_.evictRegularLatency_};
11951195

1196-
Item& oldItem = *oldItemPtr;
11971196
if (!oldItem.isAccessible() || oldItem.isExpired()) {
11981197
return {};
11991198
}
@@ -1249,7 +1248,7 @@ CacheAllocator<CacheTrait>::moveRegularItemOnEviction(
12491248
// it is unsafe to replace the old item with a new one, so we should
12501249
// also abort.
12511250
if (!accessContainer_->replaceIf(oldItem, *newItemHdl,
1252-
itemEvictionPredicate)) {
1251+
itemMovingPredicate)) {
12531252
return {};
12541253
}
12551254

@@ -1269,7 +1268,7 @@ CacheAllocator<CacheTrait>::moveRegularItemOnEviction(
12691268

12701269
// Inside the MM container's lock, this checks if the old item exists to
12711270
// make sure that no other thread removed it, and only then replaces it.
1272-
if (!replaceInMMContainer(oldItemPtr, *newItemHdl)) {
1271+
if (!replaceInMMContainer(oldItem, *newItemHdl)) {
12731272
accessContainer_->remove(*newItemHdl);
12741273
return {};
12751274
}
@@ -1459,36 +1458,45 @@ CacheAllocator<CacheTrait>::findEviction(TierId tid, PoolId pid, ClassId cid) {
14591458
itr) {
14601459
++searchTries;
14611460

1462-
Item* candidate = itr.get();
1461+
Item* toRecycle = itr.get();
1462+
1463+
Item* candidate =
1464+
toRecycle->isChainedItem()
1465+
? &toRecycle->asChainedItem().getParentItem(compressor_)
1466+
: toRecycle;
1467+
1468+
// make sure no other thead is evicting the item
1469+
if (candidate->getRefCount() != 0 || !candidate->markMoving()) {
1470+
++itr;
1471+
continue;
1472+
}
1473+
1474+
itr.destroy();
1475+
14631476
// for chained items, the ownership of the parent can change. We try to
14641477
// evict what we think as parent and see if the eviction of parent
14651478
// recycles the child we intend to.
1466-
1467-
ItemHandle toReleaseHandle = tryEvictToNextMemoryTier(tid, pid, itr);
1468-
bool movedToNextTier = false;
1469-
if(toReleaseHandle) {
1470-
movedToNextTier = true;
1471-
} else {
1472-
toReleaseHandle =
1473-
itr->isChainedItem()
1474-
? advanceIteratorAndTryEvictChainedItem(tid, pid, itr)
1475-
: advanceIteratorAndTryEvictRegularItem(tid, pid, mmContainer, itr);
1476-
}
1479+
auto toReleaseHandle =
1480+
evictNormalItem(*candidate, true /* skipIfTokenInvalid */);
1481+
auto ref = candidate->unmarkMoving();
14771482

1478-
if (toReleaseHandle) {
1479-
if (toReleaseHandle->hasChainedItem()) {
1483+
if (toReleaseHandle || ref == 0u) {
1484+
if (candidate->hasChainedItem()) {
14801485
(*stats_.chainedItemEvictions)[pid][cid].inc();
14811486
} else {
14821487
(*stats_.regularItemEvictions)[pid][cid].inc();
14831488
}
1489+
} else {
1490+
if (candidate->hasChainedItem()) {
1491+
stats_.evictFailParentAC.inc();
1492+
} else {
1493+
stats_.evictFailAC.inc();
1494+
}
1495+
}
14841496

1485-
// Invalidate iterator since later on we may use this mmContainer
1486-
// again, which cannot be done unless we drop this iterator
1487-
itr.destroy();
1488-
1489-
// we must be the last handle and for chained items, this will be
1490-
// the parent.
1491-
XDCHECK(toReleaseHandle.get() == candidate || candidate->isChainedItem());
1497+
if (toReleaseHandle) {
1498+
XDCHECK(toReleaseHandle.get() == candidate);
1499+
XDCHECK(toRecycle == candidate || toRecycle->isChainedItem());
14921500
XDCHECK_EQ(1u, toReleaseHandle->getRefCount());
14931501

14941502
// We manually release the item here because we don't want to
@@ -1504,16 +1512,21 @@ CacheAllocator<CacheTrait>::findEviction(TierId tid, PoolId pid, ClassId cid) {
15041512
// recycle the candidate.
15051513
if (ReleaseRes::kRecycled ==
15061514
releaseBackToAllocator(itemToRelease, RemoveContext::kEviction,
1507-
/* isNascent */ movedToNextTier, candidate)) {
1508-
return candidate;
1515+
/* isNascent */ false, toRecycle)) {
1516+
return toRecycle;
1517+
}
1518+
} else if (ref == 0u) {
1519+
// it's safe to recycle the item here as there are no more
1520+
// references and the item could not been marked as moving
1521+
// by other thread since it's detached from MMContainer.
1522+
if (ReleaseRes::kRecycled ==
1523+
releaseBackToAllocator(*candidate, RemoveContext::kEviction,
1524+
/* isNascent */ false, toRecycle)) {
1525+
return toRecycle;
15091526
}
15101527
}
15111528

1512-
// If we destroyed the itr to possibly evict and failed, we restart
1513-
// from the beginning again
1514-
if (!itr) {
1515-
itr.resetToBegin();
1516-
}
1529+
itr.resetToBegin();
15171530
}
15181531
return nullptr;
15191532
}
@@ -1567,24 +1580,23 @@ bool CacheAllocator<CacheTrait>::shouldWriteToNvmCacheExclusive(
15671580
}
15681581

15691582
template <typename CacheTrait>
1570-
template <typename ItemPtr>
1571-
typename CacheAllocator<CacheTrait>::ItemHandle
1583+
typename CacheAllocator<CacheTrait>::WriteHandle
15721584
CacheAllocator<CacheTrait>::tryEvictToNextMemoryTier(
1573-
TierId tid, PoolId pid, ItemPtr& item) {
1574-
if(item->isChainedItem()) return {}; // TODO: We do not support ChainedItem yet
1575-
if(item->isExpired()) return acquire(item);
1585+
TierId tid, PoolId pid, Item& item) {
1586+
if(item.isChainedItem()) return {}; // TODO: We do not support ChainedItem yet
1587+
if(item.isExpired()) return acquire(&item);
15761588

15771589
TierId nextTier = tid; // TODO - calculate this based on some admission policy
15781590
while (++nextTier < numTiers_) { // try to evict down to the next memory tiers
15791591
// allocateInternal might trigger another eviction
15801592
auto newItemHdl = allocateInternalTier(nextTier, pid,
1581-
item->getKey(),
1582-
item->getSize(),
1583-
item->getCreationTime(),
1584-
item->getExpiryTime());
1593+
item.getKey(),
1594+
item.getSize(),
1595+
item.getCreationTime(),
1596+
item.getExpiryTime());
15851597

15861598
if (newItemHdl) {
1587-
XDCHECK_EQ(newItemHdl->getSize(), item->getSize());
1599+
XDCHECK_EQ(newItemHdl->getSize(), item.getSize());
15881600

15891601
return moveRegularItemOnEviction(item, newItemHdl);
15901602
}
@@ -1594,149 +1606,11 @@ CacheAllocator<CacheTrait>::tryEvictToNextMemoryTier(
15941606
}
15951607

15961608
template <typename CacheTrait>
1597-
typename CacheAllocator<CacheTrait>::ItemHandle
1598-
CacheAllocator<CacheTrait>::tryEvictToNextMemoryTier(Item* item) {
1599-
auto tid = getTierId(*item);
1600-
auto pid = allocator_[tid]->getAllocInfo(item->getMemory()).poolId;
1601-
return tryEvictToNextMemoryTier(tid, pid, item);
1602-
}
1603-
1604-
template <typename CacheTrait>
1605-
typename CacheAllocator<CacheTrait>::ItemHandle
1606-
CacheAllocator<CacheTrait>::advanceIteratorAndTryEvictRegularItem(
1607-
TierId tid, PoolId pid, MMContainer& mmContainer, EvictionIterator& itr) {
1608-
Item& item = *itr;
1609-
1610-
const bool evictToNvmCache = shouldWriteToNvmCache(item);
1611-
1612-
auto token = evictToNvmCache ? nvmCache_->createPutToken(item.getKey())
1613-
: typename NvmCacheT::PutToken{};
1614-
// record the in-flight eviciton. If not, we move on to next item to avoid
1615-
// stalling eviction.
1616-
if (evictToNvmCache && !token.isValid()) {
1617-
++itr;
1618-
stats_.evictFailConcurrentFill.inc();
1619-
return ItemHandle{};
1620-
}
1621-
1622-
// If there are other accessors, we should abort. Acquire a handle here since
1623-
// if we remove the item from both access containers and mm containers
1624-
// below, we will need a handle to ensure proper cleanup in case we end up
1625-
// not evicting this item
1626-
auto evictHandle = accessContainer_->removeIf(item, &itemEvictionPredicate);
1627-
1628-
if (!evictHandle) {
1629-
++itr;
1630-
stats_.evictFailAC.inc();
1631-
return evictHandle;
1632-
}
1633-
1634-
mmContainer.remove(itr);
1635-
XDCHECK_EQ(reinterpret_cast<uintptr_t>(evictHandle.get()),
1636-
reinterpret_cast<uintptr_t>(&item));
1637-
XDCHECK(!evictHandle->isInMMContainer());
1638-
XDCHECK(!evictHandle->isAccessible());
1639-
1640-
// If the item is now marked as moving, that means its corresponding slab is
1641-
// being released right now. So, we look for the next item that is eligible
1642-
// for eviction. It is safe to destroy the handle here since the moving bit
1643-
// is set. Iterator was already advance by the remove call above.
1644-
if (evictHandle->isMoving()) {
1645-
stats_.evictFailMove.inc();
1646-
return ItemHandle{};
1647-
}
1648-
1649-
// Invalidate iterator since later on if we are not evicting this
1650-
// item, we may need to rely on the handle we created above to ensure
1651-
// proper cleanup if the item's raw refcount has dropped to 0.
1652-
// And since this item may be a parent item that has some child items
1653-
// in this very same mmContainer, we need to make sure we drop this
1654-
// exclusive iterator so we can gain access to it when we're cleaning
1655-
// up the child items
1656-
itr.destroy();
1657-
1658-
// Ensure that there are no accessors after removing from the access
1659-
// container
1660-
XDCHECK(evictHandle->getRefCount() == 1);
1661-
1662-
if (evictToNvmCache && shouldWriteToNvmCacheExclusive(item)) {
1663-
XDCHECK(token.isValid());
1664-
nvmCache_->put(evictHandle, std::move(token));
1665-
}
1666-
return evictHandle;
1667-
}
1668-
1669-
template <typename CacheTrait>
1670-
typename CacheAllocator<CacheTrait>::ItemHandle
1671-
CacheAllocator<CacheTrait>::advanceIteratorAndTryEvictChainedItem(
1672-
TierId tid, PoolId pid, EvictionIterator& itr) {
1673-
XDCHECK(itr->isChainedItem());
1674-
1675-
ChainedItem* candidate = &itr->asChainedItem();
1676-
++itr;
1677-
1678-
// The parent could change at any point through transferChain. However, if
1679-
// that happens, we would realize that the releaseBackToAllocator return
1680-
// kNotRecycled and we would try another chained item, leading to transient
1681-
// failure.
1682-
auto& parent = candidate->getParentItem(compressor_);
1683-
1684-
const bool evictToNvmCache = shouldWriteToNvmCache(parent);
1685-
1686-
auto token = evictToNvmCache ? nvmCache_->createPutToken(parent.getKey())
1687-
: typename NvmCacheT::PutToken{};
1688-
1689-
// if token is invalid, return. iterator is already advanced.
1690-
if (evictToNvmCache && !token.isValid()) {
1691-
stats_.evictFailConcurrentFill.inc();
1692-
return ItemHandle{};
1693-
}
1694-
1695-
// check if the parent exists in the hashtable and refcount is drained.
1696-
auto parentHandle =
1697-
accessContainer_->removeIf(parent, &itemEvictionPredicate);
1698-
if (!parentHandle) {
1699-
stats_.evictFailParentAC.inc();
1700-
return parentHandle;
1701-
}
1702-
1703-
// Invalidate iterator since later on we may use the mmContainer
1704-
// associated with this iterator which cannot be done unless we
1705-
// drop this iterator
1706-
//
1707-
// This must be done once we know the parent is not nullptr.
1708-
// Since we can very well be the last holder of this parent item,
1709-
// which may have a chained item that is linked in this MM container.
1710-
itr.destroy();
1711-
1712-
// Ensure we have the correct parent and we're the only user of the
1713-
// parent, then free it from access container. Otherwise, we abort
1714-
XDCHECK_EQ(reinterpret_cast<uintptr_t>(&parent),
1715-
reinterpret_cast<uintptr_t>(parentHandle.get()));
1716-
XDCHECK_EQ(1u, parent.getRefCount());
1717-
1718-
removeFromMMContainer(*parentHandle);
1719-
1720-
XDCHECK(!parent.isInMMContainer());
1721-
XDCHECK(!parent.isAccessible());
1722-
1723-
// TODO: add multi-tier support (similar as for unchained items)
1724-
1725-
// We need to make sure the parent is not marked as moving
1726-
// and we're the only holder of the parent item. Safe to destroy the handle
1727-
// here since moving bit is set.
1728-
if (parentHandle->isMoving()) {
1729-
stats_.evictFailParentMove.inc();
1730-
return ItemHandle{};
1731-
}
1732-
1733-
if (evictToNvmCache && shouldWriteToNvmCacheExclusive(*parentHandle)) {
1734-
XDCHECK(token.isValid());
1735-
XDCHECK(parentHandle->hasChainedItem());
1736-
nvmCache_->put(parentHandle, std::move(token));
1737-
}
1738-
1739-
return parentHandle;
1609+
typename CacheAllocator<CacheTrait>::WriteHandle
1610+
CacheAllocator<CacheTrait>::tryEvictToNextMemoryTier(Item& item) {
1611+
auto tid = getTierId(item);
1612+
auto pid = allocator_[tid]->getAllocInfo(item.getMemory()).poolId;
1613+
return tryEvictToNextMemoryTier(tid, pid, item);
17401614
}
17411615

17421616
template <typename CacheTrait>
@@ -2936,7 +2810,7 @@ void CacheAllocator<CacheTrait>::evictForSlabRelease(
29362810
auto owningHandle =
29372811
item.isChainedItem()
29382812
? evictChainedItemForSlabRelease(item.asChainedItem())
2939-
: evictNormalItemForSlabRelease(item);
2813+
: evictNormalItem(item);
29402814

29412815
// we managed to evict the corresponding owner of the item and have the
29422816
// last handle for the owner.
@@ -2993,14 +2867,15 @@ void CacheAllocator<CacheTrait>::evictForSlabRelease(
29932867

29942868
template <typename CacheTrait>
29952869
typename CacheAllocator<CacheTrait>::ItemHandle
2996-
CacheAllocator<CacheTrait>::evictNormalItemForSlabRelease(Item& item) {
2870+
CacheAllocator<CacheTrait>::evictNormalItem(Item& item,
2871+
bool skipIfTokenInvalid) {
29972872
XDCHECK(item.isMoving());
29982873

29992874
if (item.isOnlyMoving()) {
30002875
return ItemHandle{};
30012876
}
30022877

3003-
auto evictHandle = tryEvictToNextMemoryTier(&item);
2878+
auto evictHandle = tryEvictToNextMemoryTier(item);
30042879
if(evictHandle) return evictHandle;
30052880

30062881
auto predicate = [](const Item& it) { return it.getRefCount() == 0; };
@@ -3009,6 +2884,11 @@ CacheAllocator<CacheTrait>::evictNormalItemForSlabRelease(Item& item) {
30092884
auto token = evictToNvmCache ? nvmCache_->createPutToken(item.getKey())
30102885
: typename NvmCacheT::PutToken{};
30112886

2887+
if (skipIfTokenInvalid && evictToNvmCache && !token.isValid()) {
2888+
stats_.evictFailConcurrentFill.inc();
2889+
return ItemHandle{};
2890+
}
2891+
30122892
// We remove the item from both access and mm containers. It doesn't matter
30132893
// if someone else calls remove on the item at this moment, the item cannot
30142894
// be freed as long as we have the moving bit set.

0 commit comments

Comments
 (0)