Skip to content

Commit 662bf64

Browse files
committed
Implemented async Item movement between tiers
1 parent aa9a5c1 commit 662bf64

File tree

7 files changed

+375
-6
lines changed

7 files changed

+375
-6
lines changed

cachelib/allocator/CacheAllocator-inl.h

Lines changed: 210 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -968,6 +968,25 @@ bool CacheAllocator<CacheTrait>::replaceInMMContainer(Item& oldItem,
968968
}
969969
}
970970

971+
template <typename CacheTrait>
972+
bool CacheAllocator<CacheTrait>::replaceInMMContainer(Item* oldItem,
973+
Item& newItem) {
974+
return replaceInMMContainer(*oldItem, newItem);
975+
}
976+
977+
template <typename CacheTrait>
978+
bool CacheAllocator<CacheTrait>::replaceInMMContainer(EvictionIterator& oldItemIt,
979+
Item& newItem) {
980+
auto& oldContainer = getMMContainer(*oldItemIt);
981+
auto& newContainer = getMMContainer(newItem);
982+
983+
// This function is used for eviction across tiers
984+
XDCHECK(&oldContainer != &newContainer);
985+
oldContainer.remove(oldItemIt);
986+
987+
return newContainer.add(newItem);
988+
}
989+
971990
template <typename CacheTrait>
972991
bool CacheAllocator<CacheTrait>::replaceChainedItemInMMContainer(
973992
Item& oldItem, Item& newItem) {
@@ -1102,6 +1121,156 @@ CacheAllocator<CacheTrait>::insertOrReplace(const ItemHandle& handle) {
11021121
return replaced;
11031122
}
11041123

1124+
/* Next two methods are used to asynchronously move Item between memory tiers.
1125+
*
1126+
* The thread, which moves Item, allocates new Item in the tier we are moving to
1127+
* and calls moveRegularItemOnEviction() method. This method does the following:
1128+
* 1. Create MoveCtx and put it to the movesMap.
1129+
* 2. Update the access container with the new item from the tier we are
1130+
* moving to. This Item has kIncomplete flag set.
1131+
* 3. Copy data from the old Item to the new one.
1132+
* 4. Unset the kIncomplete flag and Notify MoveCtx
1133+
*
1134+
* Concurrent threads which are getting handle to the same key:
1135+
* 1. When a handle is created it checks if the kIncomplete flag is set
1136+
* 2. If so, Handle implementation creates waitContext and adds it to the
1137+
* MoveCtx by calling addWaitContextForMovingItem() method.
1138+
* 3. Wait until the moving thread will complete its job.
1139+
*/
1140+
template <typename CacheTrait>
1141+
bool CacheAllocator<CacheTrait>::addWaitContextForMovingItem(
1142+
folly::StringPiece key, std::shared_ptr<WaitContext<ItemHandle>> waiter) {
1143+
auto shard = getShardForKey(key);
1144+
auto& movesMap = getMoveMapForShard(shard);
1145+
auto lock = getMoveLockForShard(shard);
1146+
auto it = movesMap.find(key);
1147+
if (it == movesMap.end()) {
1148+
return false;
1149+
}
1150+
auto ctx = it->second.get();
1151+
ctx->addWaiter(std::move(waiter));
1152+
return true;
1153+
}
1154+
1155+
template <typename CacheTrait>
1156+
template <typename ItemPtr>
1157+
typename CacheAllocator<CacheTrait>::ItemHandle
1158+
CacheAllocator<CacheTrait>::moveRegularItemOnEviction(
1159+
ItemPtr& oldItemPtr, ItemHandle& newItemHdl) {
1160+
// TODO: should we introduce new latency tracker. E.g. evictRegularLatency_
1161+
// ??? util::LatencyTracker tracker{stats_.evictRegularLatency_};
1162+
1163+
Item& oldItem = *oldItemPtr;
1164+
if (!oldItem.isAccessible() || oldItem.isExpired()) {
1165+
return {};
1166+
}
1167+
1168+
XDCHECK_EQ(newItemHdl->getSize(), oldItem.getSize());
1169+
XDCHECK_NE(getTierId(oldItem), getTierId(*newItemHdl));
1170+
1171+
// take care of the flags before we expose the item to be accessed. this
1172+
// will ensure that when another thread removes the item from RAM, we issue
1173+
// a delete accordingly. See D7859775 for an example
1174+
if (oldItem.isNvmClean()) {
1175+
newItemHdl->markNvmClean();
1176+
}
1177+
1178+
folly::StringPiece key(oldItem.getKey());
1179+
auto shard = getShardForKey(key);
1180+
auto& movesMap = getMoveMapForShard(shard);
1181+
MoveCtx* ctx(nullptr);
1182+
{
1183+
auto lock = getMoveLockForShard(shard);
1184+
auto res = movesMap.try_emplace(key, std::make_unique<MoveCtx>());
1185+
if (!res.second) {
1186+
return {};
1187+
}
1188+
ctx = res.first->second.get();
1189+
}
1190+
1191+
auto resHdl = ItemHandle{};
1192+
auto guard = folly::makeGuard([key, this, ctx, shard, &resHdl]() {
1193+
auto& movesMap = getMoveMapForShard(shard);
1194+
resHdl->unmarkIncomplete();
1195+
auto lock = getMoveLockForShard(shard);
1196+
ctx->setItemHandle(std::move(resHdl));
1197+
movesMap.erase(key);
1198+
});
1199+
1200+
// TODO: Possibly we can use markMoving() instead. But today
1201+
// moveOnSlabRelease logic assume that we mark as moving old Item
1202+
// and than do copy and replace old Item with the new one in access
1203+
// container. Furthermore, Item can be marked as Moving only
1204+
// if it is linked to MM container. In our case we mark the new Item
1205+
// and update access container before the new Item is ready (content is
1206+
// copied).
1207+
newItemHdl->markIncomplete();
1208+
1209+
// Inside the access container's lock, this checks if the old item is
1210+
// accessible and its refcount is zero. If the item is not accessible,
1211+
// there is no point to replace it since it had already been removed
1212+
// or in the process of being removed. If the item is in cache but the
1213+
// refcount is non-zero, it means user could be attempting to remove
1214+
// this item through an API such as remove(ItemHandle). In this case,
1215+
// it is unsafe to replace the old item with a new one, so we should
1216+
// also abort.
1217+
if (!accessContainer_->replaceIf(oldItem, *newItemHdl,
1218+
itemEvictionPredicate)) {
1219+
return {};
1220+
}
1221+
1222+
if (config_.moveCb) {
1223+
// Execute the move callback. We cannot make any guarantees about the
1224+
// consistency of the old item beyond this point, because the callback can
1225+
// do more than a simple memcpy() e.g. update external references. If there
1226+
// are any remaining handles to the old item, it is the caller's
1227+
// responsibility to invalidate them. The move can only fail after this
1228+
// statement if the old item has been removed or replaced, in which case it
1229+
// should be fine for it to be left in an inconsistent state.
1230+
config_.moveCb(oldItem, *newItemHdl, nullptr);
1231+
} else {
1232+
std::memcpy(newItemHdl->getWritableMemory(), oldItem.getMemory(),
1233+
oldItem.getSize());
1234+
}
1235+
1236+
// Inside the MM container's lock, this checks if the old item exists to
1237+
// make sure that no other thread removed it, and only then replaces it.
1238+
if (!replaceInMMContainer(oldItemPtr, *newItemHdl)) {
1239+
accessContainer_->remove(*newItemHdl);
1240+
return {};
1241+
}
1242+
1243+
// Replacing into the MM container was successful, but someone could have
1244+
// called insertOrReplace() or remove() before or after the
1245+
// replaceInMMContainer() operation, which would invalidate newItemHdl.
1246+
if (!newItemHdl->isAccessible()) {
1247+
removeFromMMContainer(*newItemHdl);
1248+
return {};
1249+
}
1250+
1251+
// no one can add or remove chained items at this point
1252+
if (oldItem.hasChainedItem()) {
1253+
// safe to acquire handle for a moving Item
1254+
auto oldHandle = acquire(&oldItem);
1255+
XDCHECK_EQ(1u, oldHandle->getRefCount()) << oldHandle->toString();
1256+
XDCHECK(!newItemHdl->hasChainedItem()) << newItemHdl->toString();
1257+
try {
1258+
auto l = chainedItemLocks_.lockExclusive(oldItem.getKey());
1259+
transferChainLocked(oldHandle, newItemHdl);
1260+
} catch (const std::exception& e) {
1261+
// this should never happen because we drained all the handles.
1262+
XLOGF(DFATAL, "{}", e.what());
1263+
throw;
1264+
}
1265+
1266+
XDCHECK(!oldItem.hasChainedItem());
1267+
XDCHECK(newItemHdl->hasChainedItem());
1268+
}
1269+
newItemHdl.unmarkNascent();
1270+
resHdl = std::move(newItemHdl); // guard will assign it to ctx under lock
1271+
return acquire(&oldItem);
1272+
}
1273+
11051274
template <typename CacheTrait>
11061275
bool CacheAllocator<CacheTrait>::moveRegularItem(Item& oldItem,
11071276
ItemHandle& newItemHdl) {
@@ -1356,10 +1525,47 @@ bool CacheAllocator<CacheTrait>::shouldWriteToNvmCacheExclusive(
13561525
return true;
13571526
}
13581527

1528+
template <typename CacheTrait>
1529+
template <typename ItemPtr>
1530+
typename CacheAllocator<CacheTrait>::ItemHandle
1531+
CacheAllocator<CacheTrait>::tryEvictToNextMemoryTier(
1532+
TierId tid, PoolId pid, ItemPtr& item) {
1533+
if(item->isExpired()) return acquire(item);
1534+
1535+
TierId nextTier = tid; // TODO - calculate this based on some admission policy
1536+
while (++nextTier < numTiers_) { // try to evict down to the next memory tiers
1537+
// allocateInternal might trigger another eviction
1538+
auto newItemHdl = allocateInternalTier(nextTier, pid,
1539+
item->getKey(),
1540+
item->getSize(),
1541+
item->getCreationTime(),
1542+
item->getExpiryTime());
1543+
1544+
if (newItemHdl) {
1545+
XDCHECK_EQ(newItemHdl->getSize(), item->getSize());
1546+
1547+
return moveRegularItemOnEviction(item, newItemHdl);
1548+
}
1549+
}
1550+
1551+
return {};
1552+
}
1553+
1554+
template <typename CacheTrait>
1555+
typename CacheAllocator<CacheTrait>::ItemHandle
1556+
CacheAllocator<CacheTrait>::tryEvictToNextMemoryTier(Item* item) {
1557+
auto tid = getTierId(*item);
1558+
auto pid = allocator_[tid]->getAllocInfo(item->getMemory()).poolId;
1559+
return tryEvictToNextMemoryTier(tid, pid, item);
1560+
}
1561+
13591562
template <typename CacheTrait>
13601563
typename CacheAllocator<CacheTrait>::ItemHandle
13611564
CacheAllocator<CacheTrait>::advanceIteratorAndTryEvictRegularItem(
13621565
TierId tid, PoolId pid, MMContainer& mmContainer, EvictionIterator& itr) {
1566+
auto evictHandle = tryEvictToNextMemoryTier(tid, pid, itr);
1567+
if(evictHandle) return evictHandle;
1568+
13631569
Item& item = *itr;
13641570

13651571
const bool evictToNvmCache = shouldWriteToNvmCache(item);
@@ -1378,7 +1584,7 @@ CacheAllocator<CacheTrait>::advanceIteratorAndTryEvictRegularItem(
13781584
// if we remove the item from both access containers and mm containers
13791585
// below, we will need a handle to ensure proper cleanup in case we end up
13801586
// not evicting this item
1381-
auto evictHandle = accessContainer_->removeIf(item, &itemEvictionPredicate);
1587+
evictHandle = accessContainer_->removeIf(item, &itemEvictionPredicate);
13821588

13831589
if (!evictHandle) {
13841590
++itr;
@@ -2715,6 +2921,9 @@ CacheAllocator<CacheTrait>::evictNormalItemForSlabRelease(Item& item) {
27152921
return ItemHandle{};
27162922
}
27172923

2924+
auto evictHandle = tryEvictToNextMemoryTier(&item);
2925+
if(evictHandle) return evictHandle;
2926+
27182927
auto predicate = [](const Item& it) { return it.getRefCount() == 0; };
27192928

27202929
const bool evictToNvmCache = shouldWriteToNvmCache(item);

0 commit comments

Comments
 (0)