Skip to content

Commit 0253683

Browse files
committed
Refactor findEviction function using markForEviction bit
and and enable combinedLocking support. Shorten MMContainer critical section by removing from Access Container after MMContainer lock is droped. Only markForEviction is executed under the MMContainer critical section now. If markForEviction succeeds, the item is guaranteed to be evicted.
1 parent c44e7f9 commit 0253683

File tree

5 files changed

+118
-205
lines changed

5 files changed

+118
-205
lines changed

cachelib/allocator/CacheAllocator-inl.h

+102-183
Original file line numberDiff line numberDiff line change
@@ -1240,6 +1240,19 @@ bool CacheAllocator<CacheTrait>::moveChainedItem(ChainedItem& oldItem,
12401240
return true;
12411241
}
12421242

1243+
template <typename CacheTrait>
1244+
void CacheAllocator<CacheTrait>::unlinkItemForEviction(Item& it) {
1245+
XDCHECK(it.isMarkedForEviction());
1246+
XDCHECK(it.getRefCount() == 0);
1247+
accessContainer_->remove(it);
1248+
removeFromMMContainer(it);
1249+
1250+
// Since we managed to mark the item for eviction we must be the only
1251+
// owner of the item.
1252+
const auto ref = it.unmarkForEviction();
1253+
XDCHECK(ref == 0u);
1254+
}
1255+
12431256
template <typename CacheTrait>
12441257
typename CacheAllocator<CacheTrait>::Item*
12451258
CacheAllocator<CacheTrait>::findEviction(PoolId pid, ClassId cid) {
@@ -1248,76 +1261,102 @@ CacheAllocator<CacheTrait>::findEviction(PoolId pid, ClassId cid) {
12481261
// Keep searching for a candidate until we were able to evict it
12491262
// or until the search limit has been exhausted
12501263
unsigned int searchTries = 0;
1251-
auto itr = mmContainer.getEvictionIterator();
1252-
while ((config_.evictionSearchTries == 0 ||
1253-
config_.evictionSearchTries > searchTries) &&
1254-
itr) {
1255-
++searchTries;
1256-
(*stats_.evictionAttempts)[pid][cid].inc();
1257-
1258-
Item* toRecycle = itr.get();
1259-
1260-
Item* candidate =
1261-
toRecycle->isChainedItem()
1262-
? &toRecycle->asChainedItem().getParentItem(compressor_)
1263-
: toRecycle;
1264-
1265-
// make sure no other thead is evicting the item
1266-
if (candidate->getRefCount() != 0 || !candidate->markMoving()) {
1267-
++itr;
1268-
continue;
1269-
}
1270-
1271-
// for chained items, the ownership of the parent can change. We try to
1272-
// evict what we think as parent and see if the eviction of parent
1273-
// recycles the child we intend to.
1274-
bool evictionSuccessful = false;
1275-
{
1276-
auto toReleaseHandle =
1277-
itr->isChainedItem()
1278-
? advanceIteratorAndTryEvictChainedItem(itr)
1279-
: advanceIteratorAndTryEvictRegularItem(mmContainer, itr);
1280-
evictionSuccessful = toReleaseHandle != nullptr;
1281-
// destroy toReleaseHandle. The item won't be released to allocator
1282-
// since we marked for eviction.
1283-
}
1284-
1285-
const auto ref = candidate->unmarkMoving();
1286-
if (ref == 0u) {
1287-
// Invalidate iterator since later on we may use this mmContainer
1288-
// again, which cannot be done unless we drop this iterator
1289-
itr.destroy();
1290-
1291-
// recycle the item. it's safe to do so, even if toReleaseHandle was
1292-
// NULL. If `ref` == 0 then it means that we are the last holder of
1293-
// that item.
1294-
if (candidate->hasChainedItem()) {
1295-
(*stats_.chainedItemEvictions)[pid][cid].inc();
1296-
} else {
1297-
(*stats_.regularItemEvictions)[pid][cid].inc();
1264+
while (config_.evictionSearchTries == 0 ||
1265+
config_.evictionSearchTries > searchTries) {
1266+
Item* toRecycle = nullptr;
1267+
Item* candidate = nullptr;
1268+
typename NvmCacheT::PutToken token;
1269+
1270+
mmContainer.withEvictionIterator([this, pid, cid, &candidate, &toRecycle,
1271+
&searchTries, &mmContainer,
1272+
&token](auto&& itr) {
1273+
if (!itr) {
1274+
++searchTries;
1275+
(*stats_.evictionAttempts)[pid][cid].inc();
1276+
return;
12981277
}
12991278

1300-
if (auto eventTracker = getEventTracker()) {
1301-
eventTracker->record(AllocatorApiEvent::DRAM_EVICT, candidate->getKey(),
1302-
AllocatorApiResult::EVICTED, candidate->getSize(),
1303-
candidate->getConfiguredTTL().count());
1304-
}
1279+
while ((config_.evictionSearchTries == 0 ||
1280+
config_.evictionSearchTries > searchTries) &&
1281+
itr) {
1282+
++searchTries;
1283+
(*stats_.evictionAttempts)[pid][cid].inc();
1284+
1285+
auto* toRecycle_ = itr.get();
1286+
auto* candidate_ =
1287+
toRecycle_->isChainedItem()
1288+
? &toRecycle_->asChainedItem().getParentItem(compressor_)
1289+
: toRecycle_;
1290+
1291+
const bool evictToNvmCache = shouldWriteToNvmCache(*candidate_);
1292+
if (evictToNvmCache)
1293+
token = nvmCache_->createPutToken(candidate_->getKey());
1294+
1295+
if (evictToNvmCache && !token.isValid()) {
1296+
stats_.evictFailConcurrentFill.inc();
1297+
} else if (candidate_->markForEviction()) {
1298+
XDCHECK(candidate_->isMarkedForEviction());
1299+
// markForEviction to make sure no other thead is evicting the item
1300+
// nor holding a handle to that item
1301+
toRecycle = toRecycle_;
1302+
candidate = candidate_;
1303+
1304+
// Check if parent changed for chained items - if yes, we cannot
1305+
// remove the child from the mmContainer as we will not be evicting
1306+
// it. We could abort right here, but we need to cleanup in case
1307+
// unmarkForEviction() returns 0 - so just go through normal path.
1308+
if (!toRecycle_->isChainedItem() ||
1309+
&toRecycle->asChainedItem().getParentItem(compressor_) ==
1310+
candidate)
1311+
mmContainer.remove(itr);
1312+
return;
1313+
} else {
1314+
if (candidate_->hasChainedItem()) {
1315+
stats_.evictFailParentAC.inc();
1316+
} else {
1317+
stats_.evictFailAC.inc();
1318+
}
1319+
}
13051320

1306-
// check if by releasing the item we intend to, we actually
1307-
// recycle the candidate.
1308-
if (ReleaseRes::kRecycled ==
1309-
releaseBackToAllocator(*candidate, RemoveContext::kEviction,
1310-
/* isNascent */ false, toRecycle)) {
1311-
return toRecycle;
1321+
++itr;
1322+
XDCHECK(toRecycle == nullptr);
1323+
XDCHECK(candidate == nullptr);
13121324
}
1325+
});
1326+
1327+
if (!toRecycle)
1328+
continue;
1329+
1330+
XDCHECK(toRecycle);
1331+
XDCHECK(candidate);
1332+
1333+
unlinkItemForEviction(*candidate);
1334+
1335+
if (token.isValid() && shouldWriteToNvmCacheExclusive(*candidate)) {
1336+
nvmCache_->put(*candidate, std::move(token));
1337+
}
1338+
1339+
// recycle the item. it's safe to do so, even if toReleaseHandle was
1340+
// NULL. If `ref` == 0 then it means that we are the last holder of
1341+
// that item.
1342+
if (candidate->hasChainedItem()) {
1343+
(*stats_.chainedItemEvictions)[pid][cid].inc();
13131344
} else {
1314-
XDCHECK(!evictionSuccessful);
1345+
(*stats_.regularItemEvictions)[pid][cid].inc();
1346+
}
1347+
1348+
if (auto eventTracker = getEventTracker()) {
1349+
eventTracker->record(AllocatorApiEvent::DRAM_EVICT, candidate->getKey(),
1350+
AllocatorApiResult::EVICTED, candidate->getSize(),
1351+
candidate->getConfiguredTTL().count());
13151352
}
13161353

1317-
// If we destroyed the itr to possibly evict and failed, we restart
1318-
// from the beginning again
1319-
if (!itr) {
1320-
itr.resetToBegin();
1354+
// check if by releasing the item we intend to, we actually
1355+
// recycle the candidate.
1356+
auto ret = releaseBackToAllocator(*candidate, RemoveContext::kEviction,
1357+
/* isNascent */ false, toRecycle);
1358+
if (ret == ReleaseRes::kRecycled) {
1359+
return toRecycle;
13211360
}
13221361
}
13231362
return nullptr;
@@ -2660,126 +2699,6 @@ void CacheAllocator<CacheTrait>::evictForSlabRelease(
26602699
}
26612700
}
26622701

2663-
template <typename CacheTrait>
2664-
typename CacheAllocator<CacheTrait>::WriteHandle
2665-
CacheAllocator<CacheTrait>::advanceIteratorAndTryEvictRegularItem(
2666-
MMContainer& mmContainer, EvictionIterator& itr) {
2667-
// we should flush this to nvmcache if it is not already present in nvmcache
2668-
// and the item is not expired.
2669-
Item& item = *itr;
2670-
const bool evictToNvmCache = shouldWriteToNvmCache(item);
2671-
2672-
auto token = evictToNvmCache ? nvmCache_->createPutToken(item.getKey())
2673-
: typename NvmCacheT::PutToken{};
2674-
2675-
// record the in-flight eviciton. If not, we move on to next item to avoid
2676-
// stalling eviction.
2677-
if (evictToNvmCache && !token.isValid()) {
2678-
++itr;
2679-
stats_.evictFailConcurrentFill.inc();
2680-
return WriteHandle{};
2681-
}
2682-
2683-
// If there are other accessors, we should abort. Acquire a handle here since
2684-
// if we remove the item from both access containers and mm containers
2685-
// below, we will need a handle to ensure proper cleanup in case we end up
2686-
// not evicting this item
2687-
auto evictHandle = accessContainer_->removeIf(item, &itemExclusivePredicate);
2688-
if (!evictHandle) {
2689-
++itr;
2690-
stats_.evictFailAC.inc();
2691-
return evictHandle;
2692-
}
2693-
2694-
mmContainer.remove(itr);
2695-
XDCHECK_EQ(reinterpret_cast<uintptr_t>(evictHandle.get()),
2696-
reinterpret_cast<uintptr_t>(&item));
2697-
XDCHECK(!evictHandle->isInMMContainer());
2698-
XDCHECK(!evictHandle->isAccessible());
2699-
2700-
// Invalidate iterator since later on if we are not evicting this
2701-
// item, we may need to rely on the handle we created above to ensure
2702-
// proper cleanup if the item's raw refcount has dropped to 0.
2703-
// And since this item may be a parent item that has some child items
2704-
// in this very same mmContainer, we need to make sure we drop this
2705-
// exclusive iterator so we can gain access to it when we're cleaning
2706-
// up the child items
2707-
itr.destroy();
2708-
2709-
// Ensure that there are no accessors after removing from the access
2710-
// container
2711-
XDCHECK(evictHandle->getRefCount() == 1);
2712-
2713-
if (evictToNvmCache && shouldWriteToNvmCacheExclusive(item)) {
2714-
XDCHECK(token.isValid());
2715-
nvmCache_->put(*evictHandle, std::move(token));
2716-
}
2717-
return evictHandle;
2718-
}
2719-
2720-
template <typename CacheTrait>
2721-
typename CacheAllocator<CacheTrait>::WriteHandle
2722-
CacheAllocator<CacheTrait>::advanceIteratorAndTryEvictChainedItem(
2723-
EvictionIterator& itr) {
2724-
XDCHECK(itr->isChainedItem());
2725-
2726-
ChainedItem* candidate = &itr->asChainedItem();
2727-
++itr;
2728-
2729-
// The parent could change at any point through transferChain. However, if
2730-
// that happens, we would realize that the releaseBackToAllocator return
2731-
// kNotRecycled and we would try another chained item, leading to transient
2732-
// failure.
2733-
auto& parent = candidate->getParentItem(compressor_);
2734-
2735-
const bool evictToNvmCache = shouldWriteToNvmCache(parent);
2736-
2737-
auto token = evictToNvmCache ? nvmCache_->createPutToken(parent.getKey())
2738-
: typename NvmCacheT::PutToken{};
2739-
2740-
// if token is invalid, return. iterator is already advanced.
2741-
if (evictToNvmCache && !token.isValid()) {
2742-
stats_.evictFailConcurrentFill.inc();
2743-
return WriteHandle{};
2744-
}
2745-
2746-
// check if the parent exists in the hashtable and refcount is drained.
2747-
auto parentHandle =
2748-
accessContainer_->removeIf(parent, &itemExclusivePredicate);
2749-
if (!parentHandle) {
2750-
stats_.evictFailParentAC.inc();
2751-
return parentHandle;
2752-
}
2753-
2754-
// Invalidate iterator since later on we may use the mmContainer
2755-
// associated with this iterator which cannot be done unless we
2756-
// drop this iterator
2757-
//
2758-
// This must be done once we know the parent is not nullptr.
2759-
// Since we can very well be the last holder of this parent item,
2760-
// which may have a chained item that is linked in this MM container.
2761-
itr.destroy();
2762-
2763-
// Ensure we have the correct parent and we're the only user of the
2764-
// parent, then free it from access container. Otherwise, we abort
2765-
XDCHECK_EQ(reinterpret_cast<uintptr_t>(&parent),
2766-
reinterpret_cast<uintptr_t>(parentHandle.get()));
2767-
XDCHECK_EQ(1u, parent.getRefCount());
2768-
2769-
removeFromMMContainer(*parentHandle);
2770-
2771-
XDCHECK(!parent.isInMMContainer());
2772-
XDCHECK(!parent.isAccessible());
2773-
2774-
if (evictToNvmCache && shouldWriteToNvmCacheExclusive(*parentHandle)) {
2775-
XDCHECK(token.isValid());
2776-
XDCHECK(parentHandle->hasChainedItem());
2777-
nvmCache_->put(*parentHandle, std::move(token));
2778-
}
2779-
2780-
return parentHandle;
2781-
}
2782-
27832702
template <typename CacheTrait>
27842703
typename CacheAllocator<CacheTrait>::WriteHandle
27852704
CacheAllocator<CacheTrait>::evictNormalItemForSlabRelease(Item& item) {

cachelib/allocator/CacheAllocator.h

+6-19
Original file line numberDiff line numberDiff line change
@@ -1652,6 +1652,12 @@ class CacheAllocator : public CacheBase {
16521652
bool removeFromNvm = true,
16531653
bool recordApiEvent = true);
16541654

1655+
// Must be called by the thread which called markForEviction and
1656+
// succeeded. After this call, the item is unlinked from Access and
1657+
// MM Containers. The item is no longer marked as exclusive and it's
1658+
// ref count is 0 - it's available for recycling.
1659+
void unlinkItemForEviction(Item& it);
1660+
16551661
// Implementation to find a suitable eviction from the container. The
16561662
// two parameters together identify a single container.
16571663
//
@@ -1662,25 +1668,6 @@ class CacheAllocator : public CacheBase {
16621668

16631669
using EvictionIterator = typename MMContainer::LockedIterator;
16641670

1665-
// Advance the current iterator and try to evict a regular item
1666-
//
1667-
// @param mmContainer the container to look for evictions.
1668-
// @param itr iterator holding the item
1669-
//
1670-
// @return valid handle to regular item on success. This will be the last
1671-
// handle to the item. On failure an empty handle.
1672-
WriteHandle advanceIteratorAndTryEvictRegularItem(MMContainer& mmContainer,
1673-
EvictionIterator& itr);
1674-
1675-
// Advance the current iterator and try to evict a chained item
1676-
// Iterator may also be reset during the course of this function
1677-
//
1678-
// @param itr iterator holding the item
1679-
//
1680-
// @return valid handle to the parent item on success. This will be the last
1681-
// handle to the item
1682-
WriteHandle advanceIteratorAndTryEvictChainedItem(EvictionIterator& itr);
1683-
16841671
// Deserializer CacheAllocatorMetadata and verify the version
16851672
//
16861673
// @param deserializer Deserializer object

cachelib/cachebench/runner/CacheStressor.h

+5-2
Original file line numberDiff line numberDiff line change
@@ -299,8 +299,11 @@ class CacheStressor : public Stressor {
299299
SCOPE_EXIT { throttleFn(); };
300300
// detect refcount leaks when run in debug mode.
301301
#ifndef NDEBUG
302-
auto checkCnt = [](int cnt) {
303-
if (cnt != 0) {
302+
auto checkCnt = [useCombinedLockForIterators =
303+
config_.useCombinedLockForIterators](int cnt) {
304+
// if useCombinedLockForIterators is set handle count can be modified
305+
// by a different thread
306+
if (!useCombinedLockForIterators && cnt != 0) {
304307
throw std::runtime_error(folly::sformat("Refcount leak {}", cnt));
305308
}
306309
};

cachelib/cachebench/util/Config.cpp

+3-1
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ StressorConfig::StressorConfig(const folly::dynamic& configJson) {
6767

6868
JSONSetVal(configJson, checkNvmCacheWarmUp);
6969

70+
JSONSetVal(configJson, useCombinedLockForIterators);
71+
7072
if (configJson.count("poolDistributions")) {
7173
for (auto& it : configJson["poolDistributions"]) {
7274
poolDistributions.push_back(DistributionConfig(it, configPath));
@@ -88,7 +90,7 @@ StressorConfig::StressorConfig(const folly::dynamic& configJson) {
8890
// If you added new fields to the configuration, update the JSONSetVal
8991
// to make them available for the json configs and increment the size
9092
// below
91-
checkCorrectSize<StressorConfig, 496>();
93+
checkCorrectSize<StressorConfig, 504>();
9294
}
9395

9496
bool StressorConfig::usesChainedItems() const {

cachelib/cachebench/util/Config.h

+2
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,8 @@ struct StressorConfig : public JSONConfig {
267267
// By default, timestamps are in milliseconds.
268268
uint64_t timestampFactor{1000};
269269

270+
bool useCombinedLockForIterators{false};
271+
270272
// admission policy for cache.
271273
std::shared_ptr<StressorAdmPolicy> admPolicy{};
272274

0 commit comments

Comments
 (0)