@@ -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+
9881013template <typename CacheTrait>
9891014bool 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+
11321308template <typename CacheTrait>
11331309bool 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+
13861596template <typename CacheTrait>
13871597typename CacheAllocator<CacheTrait>::ItemHandle
13881598CacheAllocator<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