Skip to content

Commit e87508c

Browse files
Rob Lyerlymeta-codesync[bot]
authored andcommitted
Implement FlashCacheComponent's allocate & insert APIs
Summary: ^ BlockCache isn't currently exposed to the outside world, so it always expects to have the value ready to write at allocation time. Break up inserting into BlockCache according to the interface with an allocate (make space for writing) separate from inserting (make visible in cache). This isn't too much work since Regions already support refcounting, so we can create an AllocatedHandle and then release the region when the item is inserted (or discarded). Note: clean up writeEntry to return void since it always returns an ok status. Reviewed By: AlnisM Differential Revision: D88516565 fbshipit-source-id: cf83e4e36b6192e22c33924b381083f673945fd6
1 parent 0d22ba2 commit e87508c

File tree

7 files changed

+363
-88
lines changed

7 files changed

+363
-88
lines changed

cachelib/interface/CacheComponent.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ class CacheComponent {
7373
* Returns an AllocatedHandle to the old item if it was replaced. The input
7474
* AllocatedHandle is no longer usable (moved out).
7575
*
76+
* Note: it may be too expensive for some implementations to return the old
77+
* item; they'll just return std::nullopt.
78+
*
7679
* @param handle AllocatedHandle returned by allocate()
7780
* @return an empty optional if the item was inserted, an AllocatedHandle to
7881
* the replaced item if it was replaced (no longer findable replaced) or an
@@ -110,6 +113,15 @@ class CacheComponent {
110113
virtual folly::coro::Task<UnitResult> remove(ReadHandle&& handle) = 0;
111114

112115
protected:
116+
/**
117+
* Mark an item as inserted into cache.
118+
* @param handle handle to the cache item
119+
* @param inserted whether the item was inserted into cache
120+
*/
121+
FOLLY_ALWAYS_INLINE void setInserted(Handle& handle, bool inserted) {
122+
handle.inserted_ = inserted;
123+
}
124+
113125
/**
114126
* Release a handle (make unusable) without adjusting refcounts. Useful when
115127
* CacheComponent takes an rvalue ref to a handle that needs to be destroyed.

cachelib/interface/Result.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ struct Error {
2626
INVALID_CONFIG,
2727

2828
// Allocation errors
29+
ALLOCATE_FAILED,
2930
NO_SPACE,
3031

3132
// Insertion errors

cachelib/interface/components/FlashCacheComponent.cpp

Lines changed: 162 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,89 @@ using namespace facebook::cachelib::navy;
2020

2121
namespace facebook::cachelib::interface {
2222

23+
/**
24+
* In-memory buffer for data cached in flash. Reference counting via
25+
* RegionDescriptors is used to ensure the backing Region is still resident &
26+
* valid while working on this cache item. The buffer's layout is:
27+
*
28+
* ---------------------------------------------------------------------------
29+
* | creation & expiry time || value || (empty) | key | entry descriptor |
30+
* ---------------------------------------------------------------------------
31+
*/
32+
class FlashCacheItem : public CacheItem {
33+
public:
34+
using EntryDesc = BlockCache::EntryDesc;
35+
36+
static constexpr size_t kMetadataSize = 2 * sizeof(uint32_t);
37+
38+
// Allocation constructor
39+
// Note: valueSize *must* include space for creation & expiration time
40+
FlashCacheItem(const HashedKey& hashedKey,
41+
uint32_t valueSize,
42+
std::tuple<RegionDescriptor, size_t, RelAddress>&& allocation,
43+
const uint32_t creationTime,
44+
const uint32_t ttlSecs)
45+
: desc_(std::move(std::get<0>(allocation))),
46+
relAddr_(std::move(std::get<2>(allocation))),
47+
buffer_(std::get<1>(allocation)) {
48+
XDCHECK_GE(valueSize, 2 * sizeof(uint32_t))
49+
<< "valueSize doesn't include space for metadata";
50+
BlockCache::writeEntryDescAndKey(buffer_, hashedKey, valueSize);
51+
uint32_t* metadata = reinterpret_cast<uint32_t*>(buffer_.data());
52+
metadata[0] = creationTime;
53+
metadata[1] = ttlSecs > 0 ? creationTime + ttlSecs : 0;
54+
}
55+
56+
RegionDescriptor& getRegionDescriptor() noexcept { return desc_; }
57+
RelAddress& getRelAddress() noexcept { return relAddr_; }
58+
Buffer& getBuffer() noexcept { return buffer_; }
59+
EntryDesc* getEntryDescriptor() const noexcept {
60+
auto entryEnd = const_cast<uint8_t*>(buffer_.data()) + buffer_.size();
61+
return reinterpret_cast<EntryDesc*>(entryEnd - sizeof(EntryDesc));
62+
}
63+
64+
// ------------------------------ Interface ------------------------------ //
65+
66+
uint32_t getCreationTime() const noexcept override {
67+
return reinterpret_cast<const uint32_t*>(buffer_.data())[0];
68+
}
69+
uint32_t getExpiryTime() const noexcept override {
70+
return reinterpret_cast<const uint32_t*>(buffer_.data())[1];
71+
}
72+
// Ref-counting for FlashCache is different than RAMCache - multiple
73+
// RAMCacheItems all refer to a single cache item in memory whereas each
74+
// FlashCacheItem is separate (even if it references the same cache item in
75+
// Region memory).
76+
//
77+
// The only thing we care about is Region refcounting via RegionDescriptor. We
78+
// don't need to increment on create (creating the RegionDescriptor does that)
79+
// and we should always release the cache item to release the descriptor.
80+
void incrementRefCount() noexcept override {}
81+
bool decrementRefCount() noexcept override {
82+
// get cache to release the descriptor (need access to RegionManager)
83+
return true;
84+
}
85+
Key getKey() const noexcept override {
86+
auto* desc = getEntryDescriptor();
87+
return Key{reinterpret_cast<const char*>(desc) - desc->keySize,
88+
desc->keySize};
89+
}
90+
void* getMemory() const noexcept override {
91+
return const_cast<uint8_t*>(buffer_.data() + kMetadataSize);
92+
}
93+
uint32_t getMemorySize() const noexcept override {
94+
return getEntryDescriptor()->valueSize - kMetadataSize;
95+
}
96+
uint32_t getTotalSize() const noexcept override { return buffer_.size(); }
97+
98+
private:
99+
// Used for region lifecycle management & indexing
100+
RegionDescriptor desc_;
101+
RelAddress relAddr_;
102+
// Data is buffered in RAM
103+
Buffer buffer_;
104+
};
105+
23106
/* static */ Result<FlashCacheComponent> FlashCacheComponent::create(
24107
std::string name, navy::BlockCache::Config&& config) noexcept {
25108
try {
@@ -34,21 +117,83 @@ const std::string& FlashCacheComponent::getName() const noexcept {
34117
}
35118

36119
folly::coro::Task<Result<AllocatedHandle>> FlashCacheComponent::allocate(
37-
Key /* key */,
38-
uint32_t /* size */,
39-
uint32_t /* creationTime */,
40-
uint32_t /* ttlSecs */) {
41-
co_return makeError(Error::Code::UNIMPLEMENTED, "not yet implemented");
120+
Key key, uint32_t size, uint32_t creationTime, uint32_t ttlSecs) {
121+
if (key.empty()) {
122+
co_return makeError(Error::Code::INVALID_ARGUMENTS, "empty key");
123+
}
124+
125+
HashedKey hashedKey(key);
126+
uint32_t valueSize = size + FlashCacheItem::kMetadataSize;
127+
auto ret = co_await onWorkerThread(
128+
[=, this]() { return cache_->allocateForInsert(hashedKey, valueSize); },
129+
[this](auto&& result) {
130+
if (result.hasValue()) {
131+
// Successfully allocated Region memory but coroutine was cancelled -
132+
// close the descriptor to release refcount on the Region
133+
auto& [desc, slotSize, _] = result.value();
134+
cache_->addHole(slotSize);
135+
cache_->regionManager_.close(std::move(desc));
136+
}
137+
});
138+
if (ret.hasError()) {
139+
co_return makeError(Error::Code::ALLOCATE_FAILED,
140+
"could not allocate space in a region");
141+
}
142+
// Note: we can't checksum until the user has finished writing their data,
143+
// defer until they call insert()
144+
auto* item = new FlashCacheItem(hashedKey, valueSize, std::move(ret).value(),
145+
creationTime, ttlSecs);
146+
co_return AllocatedHandle(*this, *item);
147+
}
148+
149+
folly::coro::Task<UnitResult> FlashCacheComponent::insertImpl(
150+
AllocatedHandle&& handle, bool allowReplace) {
151+
if (!handle) {
152+
co_return makeError(Error::Code::INVALID_ARGUMENTS,
153+
"empty AllocatedHandle");
154+
}
155+
156+
auto& fccItem = reinterpret_cast<FlashCacheItem&>(*handle);
157+
auto* desc = fccItem.getEntryDescriptor();
158+
auto logicalSizeWritten = desc->keySize + desc->valueSize;
159+
auto totalSize = fccItem.getTotalSize();
160+
auto keyHash = desc->keyHash;
161+
162+
// Note: we don't need to run this on a worker thread because everything here
163+
// is synchronous - only allocation is async
164+
165+
if (cache_->checksumData_) {
166+
desc->cs = checksum(fccItem.getBuffer().view().slice(0, desc->valueSize));
167+
}
168+
cache_->regionManager_.write(fccItem.getRelAddress(),
169+
std::move(fccItem.getBuffer()));
170+
cache_->logicalWrittenCount_.add(logicalSizeWritten);
171+
auto inserted = cache_->updateIndex(keyHash, totalSize,
172+
fccItem.getRelAddress(), allowReplace);
173+
if (inserted) {
174+
setInserted(handle, true);
175+
auto _ = std::move(handle);
176+
co_return folly::unit;
177+
} else {
178+
// Note: do hole accounting in release()
179+
co_return makeError(Error::Code::ALREADY_INSERTED, "key already inserted");
180+
}
42181
}
43182

44183
folly::coro::Task<UnitResult> FlashCacheComponent::insert(
45-
AllocatedHandle&& /* handle */) {
46-
co_return makeError(Error::Code::UNIMPLEMENTED, "not yet implemented");
184+
AllocatedHandle&& handle) {
185+
co_return co_await insertImpl(std::move(handle), /* allowReplace */ false);
47186
}
48187

49188
folly::coro::Task<Result<std::optional<AllocatedHandle>>>
50-
FlashCacheComponent::insertOrReplace(AllocatedHandle&& /* handle */) {
51-
co_return makeError(Error::Code::UNIMPLEMENTED, "not yet implemented");
189+
FlashCacheComponent::insertOrReplace(AllocatedHandle&& handle) {
190+
auto inserted =
191+
co_await insertImpl(std::move(handle), /* allowReplace */ true);
192+
XDCHECK(inserted) << "insertOrReplace should never fail";
193+
// Note: returning the old data requires a flash read, don't expose for now
194+
co_return std::move(inserted).then([](auto&&) {
195+
return Result<std::optional<AllocatedHandle>>(std::nullopt);
196+
});
52197
}
53198

54199
folly::coro::Task<Result<std::optional<ReadHandle>>> FlashCacheComponent::find(
@@ -75,8 +220,14 @@ FlashCacheComponent::FlashCacheComponent(std::string&& name,
75220
: name_(std::move(name)),
76221
cache_(std::make_unique<navy::BlockCache>(std::move(config))) {}
77222

78-
folly::coro::Task<void> FlashCacheComponent::release(CacheItem& /* item */,
79-
bool /* inserted */) {
223+
folly::coro::Task<void> FlashCacheComponent::release(CacheItem& item,
224+
bool inserted) {
225+
auto& fccItem = reinterpret_cast<FlashCacheItem&>(item);
226+
if (!inserted) {
227+
cache_->addHole(fccItem.getTotalSize());
228+
}
229+
cache_->regionManager_.close(std::move(fccItem.getRegionDescriptor()));
230+
delete &item;
80231
co_return;
81232
}
82233

cachelib/interface/components/FlashCacheComponent.h

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
#pragma once
1818

1919
#include "cachelib/interface/CacheComponent.h"
20+
#include "cachelib/interface/utils/CoroFiberAdapter.h"
2021
#include "cachelib/navy/block_cache/BlockCache.h"
2122

2223
namespace facebook::cachelib::interface {
@@ -66,6 +67,24 @@ class FlashCacheComponent : public CacheComponent {
6667

6768
FlashCacheComponent(std::string&& name, navy::BlockCache::Config&& config);
6869

70+
// Runs func() on a RegionManager worker fiber. Should not be called from an
71+
// existing region manager worker fiber!
72+
template <typename FuncT,
73+
typename CleanupFuncT = std::function<void()>,
74+
typename ReturnT = std::invoke_result_t<FuncT>>
75+
folly::coro::Task<ReturnT> onWorkerThread(FuncT&& func,
76+
CleanupFuncT&& cleanup = {}) {
77+
XDCHECK(!cache_->regionManager_.isOnWorker())
78+
<< "Calling public APIs from a worker thread is unsupported";
79+
co_return co_await utils::onWorkerThread(
80+
cache_->regionManager_.getNextWorker(),
81+
std::forward<FuncT>(func),
82+
std::forward<CleanupFuncT>(cleanup));
83+
}
84+
85+
folly::coro::Task<UnitResult> insertImpl(AllocatedHandle&& handle,
86+
bool allowReplace);
87+
6988
// ------------------------------ Interface ------------------------------ //
7089

7190
folly::coro::Task<void> release(CacheItem& item, bool inserted) override;

0 commit comments

Comments
 (0)