Skip to content

Commit 78eed16

Browse files
committed
Implemented async Item movement between tiers
1 parent cbd1e14 commit 78eed16

File tree

7 files changed

+196
-15
lines changed

7 files changed

+196
-15
lines changed

cachelib/allocator/CacheAllocator-inl.h

Lines changed: 57 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1104,6 +1104,22 @@ CacheAllocator<CacheTrait>::insertOrReplace(const ItemHandle& handle) {
11041104
return replaced;
11051105
}
11061106

1107+
template <typename CacheTrait>
1108+
bool CacheAllocator<CacheTrait>::addWaitContextForMovingItem(
1109+
folly::StringPiece key,
1110+
std::shared_ptr<WaitContext<ItemHandle>> waiter) {
1111+
auto shard = getShardForKey(key);
1112+
auto& movesMap = getMoveMapForShard(shard);
1113+
auto lock = getMoveLockForShard(shard);
1114+
auto it = movesMap.find(key);
1115+
if(it == movesMap.end()) {
1116+
return false;
1117+
}
1118+
auto ctx = it->second.get();
1119+
ctx->addWaiter(std::move(waiter));
1120+
return true;
1121+
}
1122+
11071123
template <typename CacheTrait>
11081124
bool CacheAllocator<CacheTrait>::moveRegularItemOnEviction(
11091125
Item& oldItem,
@@ -1125,22 +1141,35 @@ bool CacheAllocator<CacheTrait>::moveRegularItemOnEviction(
11251141
newItemHdl->markNvmClean();
11261142
}
11271143

1128-
if(config_.moveCb) {
1129-
// Execute the move callback. We cannot make any guarantees about the
1130-
// consistency of the old item beyond this point, because the callback can
1131-
// do more than a simple memcpy() e.g. update external references. If there
1132-
// are any remaining handles to the old item, it is the caller's
1133-
// responsibility to invalidate them. The move can only fail after this
1134-
// statement if the old item has been removed or replaced, in which case it
1135-
// should be fine for it to be left in an inconsistent state.
1136-
config_.moveCb(oldItem, *newItemHdl, nullptr);
1137-
} else {
1138-
std::memcpy(newItemHdl->getWritableMemory(), oldItem.getMemory(), oldItem.getSize());
1144+
folly::StringPiece key(oldItem.getKey());
1145+
auto shard = getShardForKey(key);
1146+
auto& movesMap = getMoveMapForShard(shard);
1147+
MoveCtx* ctx(nullptr);
1148+
{
1149+
auto lock = getMoveLockForShard(shard);
1150+
auto res = movesMap.try_emplace(key, std::make_unique<MoveCtx>());
1151+
if(!res.second) {
1152+
return false;
1153+
}
1154+
ctx = res.first->second.get();
11391155
}
11401156

1141-
// TODO: Possible data race. We copied Item's memory to the newItemHdl
1142-
// but have not updated accessContainer yet. Concurrent threads might get handle
1143-
// to the old Item.
1157+
auto resHdl = ItemHandle{};
1158+
auto guard = folly::makeGuard([key, this, ctx, shard, &resHdl]() {
1159+
auto& movesMap = getMoveMapForShard(shard);
1160+
resHdl->unmarkNotReady();
1161+
auto lock = getMoveLockForShard(shard);
1162+
ctx->setItemHandle(std::move(resHdl));
1163+
movesMap.erase(key);
1164+
});
1165+
1166+
// TODO: Possibly we can use markMoving() instead. But today
1167+
// moveOnSlabRelease logic assume that we mark as moving old Item
1168+
// and than do copy and replace old Item with the new one in access
1169+
// container. Furthermore, Item can be marked as Moving only
1170+
// if it is linked to MM container. In our case we mark the new Item
1171+
// and update access container before the new Item is ready (content is copied).
1172+
newItemHdl->markNotReady();
11441173

11451174
// Inside the access container's lock, this checks if the old item is
11461175
// accessible and its refcount is zero. If the item is not accessible,
@@ -1154,6 +1183,19 @@ bool CacheAllocator<CacheTrait>::moveRegularItemOnEviction(
11541183
return false;
11551184
}
11561185

1186+
if(config_.moveCb) {
1187+
// Execute the move callback. We cannot make any guarantees about the
1188+
// consistency of the old item beyond this point, because the callback can
1189+
// do more than a simple memcpy() e.g. update external references. If there
1190+
// are any remaining handles to the old item, it is the caller's
1191+
// responsibility to invalidate them. The move can only fail after this
1192+
// statement if the old item has been removed or replaced, in which case it
1193+
// should be fine for it to be left in an inconsistent state.
1194+
config_.moveCb(oldItem, *newItemHdl, nullptr);
1195+
} else {
1196+
std::memcpy(newItemHdl->getWritableMemory(), oldItem.getMemory(), oldItem.getSize());
1197+
}
1198+
11571199
// Inside the MM container's lock, this checks if the old item exists to
11581200
// make sure that no other thread removed it, and only then replaces it.
11591201
if (!replaceInMMContainer(oldItem, *newItemHdl)) {
@@ -1188,6 +1230,7 @@ bool CacheAllocator<CacheTrait>::moveRegularItemOnEviction(
11881230
XDCHECK(newItemHdl->hasChainedItem());
11891231
}
11901232
newItemHdl.unmarkNascent();
1233+
resHdl = newItemHdl.clone(); // guard will assign it to ctx under lock
11911234
return true;
11921235
}
11931236

cachelib/allocator/CacheAllocator.h

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
#include <folly/ScopeGuard.h>
2222
#include <folly/logging/xlog.h>
2323
#include <folly/synchronization/SanitizeThread.h>
24+
#include <folly/hash/Hash.h>
25+
#include <folly/container/F14Map.h>
2426

2527
#include <functional>
2628
#include <memory>
@@ -1745,6 +1747,79 @@ class CacheAllocator : public CacheBase {
17451747
return 0; // TODO
17461748
}
17471749

1750+
bool addWaitContextForMovingItem(folly::StringPiece key,
1751+
std::shared_ptr<WaitContext<ItemHandle>> waiter);
1752+
1753+
class MoveCtx {
1754+
public:
1755+
MoveCtx() {}
1756+
1757+
~MoveCtx() {
1758+
// prevent any further enqueue to waiters
1759+
// Note: we don't need to hold locks since no one can enqueue
1760+
// after this point.
1761+
wakeUpWaiters();
1762+
}
1763+
1764+
// record the item handle. Upon destruction we will wake up the waiters
1765+
// and pass a clone of the handle to the callBack. By default we pass
1766+
// a null handle
1767+
void setItemHandle(ItemHandle _it) { it = std::move(_it); }
1768+
1769+
// enqueue a waiter into the waiter list
1770+
// @param waiter WaitContext
1771+
void addWaiter(std::shared_ptr<WaitContext<ItemHandle>> waiter) {
1772+
XDCHECK(waiter);
1773+
waiters.push_back(std::move(waiter));
1774+
}
1775+
1776+
private:
1777+
// notify all pending waiters that are waiting for the fetch.
1778+
void wakeUpWaiters() {
1779+
bool refcountOverflowed = false;
1780+
for (auto& w : waiters) {
1781+
// If refcount overflowed earlier, then we will return miss to
1782+
// all subsequent waitors.
1783+
if (refcountOverflowed) {
1784+
w->set(ItemHandle{});
1785+
continue;
1786+
}
1787+
1788+
try {
1789+
w->set(it.clone());
1790+
} catch (const exception::RefcountOverflow&) {
1791+
// We'll return a miss to the user's pending read,
1792+
// so we should enqueue a delete via NvmCache.
1793+
//TODO: cache.remove(it);
1794+
refcountOverflowed = true;
1795+
}
1796+
}
1797+
}
1798+
1799+
ItemHandle it; // will be set when Context is being filled
1800+
std::vector<std::shared_ptr<WaitContext<ItemHandle>>> waiters; // list of
1801+
// waiters
1802+
};
1803+
using MoveMap = folly::F14ValueMap<folly::StringPiece, std::unique_ptr<MoveCtx>, folly::HeterogeneousAccessHash<folly::StringPiece>>;
1804+
1805+
static size_t getShardForKey(folly::StringPiece key) {
1806+
return folly::Hash()(key) % kShards;
1807+
}
1808+
1809+
MoveMap& getMoveMapForShard(size_t shard) { return movesMap_[shard].movesMap_; }
1810+
1811+
MoveMap& getMoveMap(folly::StringPiece key) {
1812+
return getMoveMapForShard(getShardForKey(key));
1813+
}
1814+
1815+
std::unique_lock<std::mutex> getMoveLockForShard(size_t shard) {
1816+
return std::unique_lock<std::mutex>(moveLock_[shard].moveLock_);
1817+
}
1818+
1819+
std::unique_lock<std::mutex> getMoveLock(folly::StringPiece key) {
1820+
return getMoveLockForShard(getShardForKey(key));
1821+
}
1822+
17481823
// Whether the memory allocator for this cache allocator was created on shared
17491824
// memory. The hash table, chained item hash table etc is also created on
17501825
// shared memory except for temporary shared memory mode when they're created
@@ -1862,6 +1937,18 @@ class CacheAllocator : public CacheBase {
18621937
// indicates if the shutdown of cache is in progress or not
18631938
std::atomic<bool> shutDownInProgress_{false};
18641939

1940+
static constexpr size_t kShards = 8192; // TODO: need to define right value
1941+
1942+
// a map of all pending moves
1943+
struct {
1944+
alignas(folly::hardware_destructive_interference_size) MoveMap movesMap_;
1945+
} movesMap_[kShards];
1946+
1947+
// a map of move locks for each shard
1948+
struct {
1949+
alignas(folly::hardware_destructive_interference_size) std::mutex moveLock_;
1950+
} moveLock_[kShards];
1951+
18651952
// END private members
18661953

18671954
// Make this friend to give access to acquire and release

cachelib/allocator/CacheItem-inl.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,21 @@ bool CacheItem<CacheTrait>::isNvmEvicted() const noexcept {
264264
return ref_.isNvmEvicted();
265265
}
266266

267+
template <typename CacheTrait>
268+
void CacheItem<CacheTrait>::markNotReady() noexcept {
269+
ref_.markNotReady();
270+
}
271+
272+
template <typename CacheTrait>
273+
void CacheItem<CacheTrait>::unmarkNotReady() noexcept {
274+
ref_.unmarkNotReady();
275+
}
276+
277+
template <typename CacheTrait>
278+
bool CacheItem<CacheTrait>::isNotReady() const noexcept {
279+
return ref_.isNotReady();
280+
}
281+
267282
template <typename CacheTrait>
268283
void CacheItem<CacheTrait>::markIsChainedItem() noexcept {
269284
XDCHECK(!hasChainedItem());

cachelib/allocator/CacheItem.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,14 @@ class CACHELIB_PACKED_ATTR CacheItem {
240240
void unmarkNvmEvicted() noexcept;
241241
bool isNvmEvicted() const noexcept;
242242

243+
/**
244+
* Marks that the item is migrating between memory tiers and
245+
* not ready for access now. Accessing thread should wait.
246+
*/
247+
void markNotReady() noexcept;
248+
void unmarkNotReady() noexcept;
249+
bool isNotReady() const noexcept;
250+
243251
/**
244252
* Function to set the timestamp for when to expire an item
245253
* Employs a best-effort approach to update the expiryTime. Item's expiry

cachelib/allocator/Handle.h

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -464,7 +464,14 @@ struct HandleImpl {
464464

465465
// Handle which has the item already
466466
FOLLY_ALWAYS_INLINE HandleImpl(Item* it, CacheT& alloc) noexcept
467-
: alloc_(&alloc), it_(it) {}
467+
: alloc_(&alloc), it_(it) {
468+
if(it_ && it_->isNotReady()) {
469+
waitContext_ = std::make_shared<ItemWaitContext>(alloc);
470+
if(!alloc_->addWaitContextForMovingItem(it->getKey(), waitContext_)) {
471+
waitContext_.reset();
472+
}
473+
}
474+
}
468475

469476
// handle that has a wait context allocated. Used for async handles
470477
// In this case, the it_ will be filled in asynchronously and mulitple

cachelib/allocator/Refcount.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,10 @@ class FOLLY_PACK_ATTR RefcountWithFlags {
116116
// unevictable in the past.
117117
kUnevictable_NOOP,
118118

119+
// Item is accecible but content is not ready yet. Used by eviction
120+
// when Item is moved between memory tiers.
121+
kNotReady,
122+
119123
// Unused. This is just to indciate the maximum number of flags
120124
kFlagMax,
121125
};
@@ -329,6 +333,14 @@ class FOLLY_PACK_ATTR RefcountWithFlags {
329333
void unmarkNvmEvicted() noexcept { return unSetFlag<kNvmEvicted>(); }
330334
bool isNvmEvicted() const noexcept { return isFlagSet<kNvmEvicted>(); }
331335

336+
/**
337+
* Marks that the item is migrating between memory tiers and
338+
* not ready for access now. Accessing thread should wait.
339+
*/
340+
void markNotReady() noexcept { return setFlag<kNotReady>(); }
341+
void unmarkNotReady() noexcept { return unSetFlag<kNotReady>(); }
342+
bool isNotReady() const noexcept { return isFlagSet<kNotReady>(); }
343+
332344
// Whether or not an item is completely drained of access
333345
// Refcount is 0 and the item is not linked, accessible, nor moving
334346
bool isDrained() const noexcept { return getRefWithAccessAndAdmin() == 0; }

cachelib/allocator/tests/ItemHandleTest.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ struct TestItem {
3939
using ChainedItem = int;
4040

4141
void reset() {}
42+
43+
folly::StringPiece getKey() const { return folly::StringPiece(); }
44+
45+
bool isNotReady() const { return false; }
4246
};
4347

4448
struct TestNvmCache;
@@ -79,6 +83,11 @@ struct TestAllocator {
7983

8084
void adjustHandleCountForThread_private(int i) { tlRef_.tlStats() += i; }
8185

86+
bool addWaitContextForMovingItem(folly::StringPiece key,
87+
std::shared_ptr<WaitContext<TestItemHandle>> waiter) {
88+
return false;
89+
}
90+
8291
util::FastStats<int> tlRef_;
8392
};
8493
} // namespace

0 commit comments

Comments
 (0)