Skip to content

Commit 9ae9b2e

Browse files
committed
Implemented async Item movement between tiers
1 parent c8576f5 commit 9ae9b2e

File tree

7 files changed

+386
-6
lines changed

7 files changed

+386
-6
lines changed

cachelib/allocator/CacheAllocator-inl.h

Lines changed: 217 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ CacheAllocator<CacheTrait>::CacheAllocator(Config config)
4444
[this](Item* it) -> ItemHandle { return acquire(it); })),
4545
chainedItemLocks_(config_.chainedItemsLockPower,
4646
std::make_shared<MurmurHash2>()),
47+
movesMap_(kShards),
48+
moveLock_(kShards),
4749
cacheCreationTime_{util::getCurrentTimeSec()} {
4850

4951
if (numTiers_ > 1 || std::holds_alternative<FileShmSegmentOpts>(
@@ -130,6 +132,8 @@ CacheAllocator<CacheTrait>::CacheAllocator(SharedMemNewT, Config config)
130132
[this](Item* it) -> ItemHandle { return acquire(it); })),
131133
chainedItemLocks_(config_.chainedItemsLockPower,
132134
std::make_shared<MurmurHash2>()),
135+
movesMap_(kShards),
136+
moveLock_(kShards),
133137
cacheCreationTime_{util::getCurrentTimeSec()} {
134138
initCommon(false);
135139
shmManager_->removeShm(detail::kShmInfoName,
@@ -166,6 +170,8 @@ CacheAllocator<CacheTrait>::CacheAllocator(SharedMemAttachT, Config config)
166170
[this](Item* it) -> ItemHandle { return acquire(it); })),
167171
chainedItemLocks_(config_.chainedItemsLockPower,
168172
std::make_shared<MurmurHash2>()),
173+
movesMap_(kShards),
174+
moveLock_(kShards),
169175
cacheCreationTime_{*metadata_.cacheCreationTime_ref()} {
170176
/* TODO - per tier? */
171177
for (auto pid : *metadata_.compactCachePools_ref()) {
@@ -985,6 +991,25 @@ bool CacheAllocator<CacheTrait>::replaceInMMContainer(Item& oldItem,
985991
}
986992
}
987993

994+
template <typename CacheTrait>
995+
bool CacheAllocator<CacheTrait>::replaceInMMContainer(Item* oldItem,
996+
Item& newItem) {
997+
return replaceInMMContainer(*oldItem, newItem);
998+
}
999+
1000+
template <typename CacheTrait>
1001+
bool CacheAllocator<CacheTrait>::replaceInMMContainer(EvictionIterator& oldItemIt,
1002+
Item& newItem) {
1003+
auto& oldContainer = getMMContainer(*oldItemIt);
1004+
auto& newContainer = getMMContainer(newItem);
1005+
1006+
// This function is used for eviction across tiers
1007+
XDCHECK(&oldContainer != &newContainer);
1008+
oldContainer.remove(oldItemIt);
1009+
1010+
return newContainer.add(newItem);
1011+
}
1012+
9881013
template <typename CacheTrait>
9891014
bool CacheAllocator<CacheTrait>::replaceChainedItemInMMContainer(
9901015
Item& oldItem, Item& newItem) {
@@ -1129,6 +1154,157 @@ CacheAllocator<CacheTrait>::insertOrReplace(const ItemHandle& handle) {
11291154
return replaced;
11301155
}
11311156

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

1562+
template <typename CacheTrait>
1563+
template <typename ItemPtr>
1564+
typename CacheAllocator<CacheTrait>::ItemHandle
1565+
CacheAllocator<CacheTrait>::tryEvictToNextMemoryTier(
1566+
TierId tid, PoolId pid, ItemPtr& item) {
1567+
if(item->isExpired()) return acquire(item);
1568+
1569+
TierId nextTier = tid; // TODO - calculate this based on some admission policy
1570+
while (++nextTier < numTiers_) { // try to evict down to the next memory tiers
1571+
// allocateInternal might trigger another eviction
1572+
auto newItemHdl = allocateInternalTier(nextTier, pid,
1573+
item->getKey(),
1574+
item->getSize(),
1575+
item->getCreationTime(),
1576+
item->getExpiryTime());
1577+
1578+
if (newItemHdl) {
1579+
XDCHECK_EQ(newItemHdl->getSize(), item->getSize());
1580+
1581+
return moveRegularItemOnEviction(item, newItemHdl);
1582+
}
1583+
}
1584+
1585+
return {};
1586+
}
1587+
1588+
template <typename CacheTrait>
1589+
typename CacheAllocator<CacheTrait>::ItemHandle
1590+
CacheAllocator<CacheTrait>::tryEvictToNextMemoryTier(Item* item) {
1591+
auto tid = getTierId(*item);
1592+
auto pid = allocator_[tid]->getAllocInfo(item->getMemory()).poolId;
1593+
return tryEvictToNextMemoryTier(tid, pid, item);
1594+
}
1595+
13861596
template <typename CacheTrait>
13871597
typename CacheAllocator<CacheTrait>::ItemHandle
13881598
CacheAllocator<CacheTrait>::advanceIteratorAndTryEvictRegularItem(
13891599
TierId tid, PoolId pid, MMContainer& mmContainer, EvictionIterator& itr) {
1600+
auto evictHandle = tryEvictToNextMemoryTier(tid, pid, itr);
1601+
if(evictHandle) return evictHandle;
1602+
13901603
Item& item = *itr;
13911604

13921605
const bool evictToNvmCache = shouldWriteToNvmCache(item);
@@ -1405,7 +1618,7 @@ CacheAllocator<CacheTrait>::advanceIteratorAndTryEvictRegularItem(
14051618
// if we remove the item from both access containers and mm containers
14061619
// below, we will need a handle to ensure proper cleanup in case we end up
14071620
// not evicting this item
1408-
auto evictHandle = accessContainer_->removeIf(item, &itemEvictionPredicate);
1621+
evictHandle = accessContainer_->removeIf(item, &itemEvictionPredicate);
14091622

14101623
if (!evictHandle) {
14111624
++itr;
@@ -2782,6 +2995,9 @@ CacheAllocator<CacheTrait>::evictNormalItemForSlabRelease(Item& item) {
27822995
return ItemHandle{};
27832996
}
27842997

2998+
auto evictHandle = tryEvictToNextMemoryTier(&item);
2999+
if(evictHandle) return evictHandle;
3000+
27853001
auto predicate = [](const Item& it) { return it.getRefCount() == 0; };
27863002

27873003
const bool evictToNvmCache = shouldWriteToNvmCache(item);

0 commit comments

Comments
 (0)