cachelib/allocator/Handle.h (358 lines of code) (raw):

/* * Copyright (c) Facebook, Inc. and its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #pragma once #include <folly/Function.h> #include <folly/fibers/Baton.h> #include <folly/futures/Future.h> #include <folly/logging/xlog.h> #include <gtest/gtest.h> #include <iostream> #include <mutex> #include "cachelib/allocator/nvmcache/WaitContext.h" namespace facebook { namespace cachelib { namespace detail { // Bit mask for flags on cache handle enum class HandleFlags : uint8_t { // Indicates if a handle has been created but not inserted into the cache yet. // This is used to track if removeCB is invoked when the item is freed back. kNascent = 1 << 0, // Indicate if the item was expired kExpired = 1 << 1, // Indicate if we went to NvmCache to look for this item kWentToNvm = 1 << 2, }; template <typename T> struct WriteHandleImpl; // RAII class that manages cache item pointer lifetime. These handles // can only be created by a CacheAllocator and upon destruction the handle // takes care of releasing the item to the correct cache allocator instance. // Handle must be destroyed *before* the instance of the CacheAllocator // gets destroyed. template <typename T> struct ReadHandleImpl { using Item = T; using CacheT = typename T::CacheT; ReadHandleImpl() = default; /*implicit*/ ReadHandleImpl(std::nullptr_t) {} // reset the handle by releasing the item it holds. void reset() noexcept { waitContext_.reset(); if (it_ == nullptr) { return; } assert(alloc_ != nullptr); try { alloc_->release(it_, isNascent()); } catch (const std::exception& e) { XLOGF(CRITICAL, "Failed to release {:#10x} : {}", static_cast<void*>(it_), e.what()); } it_ = nullptr; } // Waits for item (if async op in progress) and then releases item's // ownership to the caller. Item* release() noexcept { auto ret = getInternal(); if (waitContext_) { waitContext_->releaseHandle(); waitContext_.reset(); } else { it_ = nullptr; } return ret; } ~ReadHandleImpl() noexcept { reset(); } ReadHandleImpl(const ReadHandleImpl&) = delete; ReadHandleImpl& operator=(const ReadHandleImpl&) = delete; FOLLY_ALWAYS_INLINE ReadHandleImpl(ReadHandleImpl&& other) noexcept : alloc_(other.alloc_), it_(other.releaseItem()), waitContext_(std::move(other.waitContext_)), flags_(other.getFlags()) {} FOLLY_ALWAYS_INLINE ReadHandleImpl& operator=( ReadHandleImpl&& other) noexcept { if (this != &other) { this->~ReadHandleImpl(); new (this) ReadHandleImpl(std::move(other)); } return *this; } // == and != operators for comparison with Item* friend bool operator==(const ReadHandleImpl& a, const Item* it) noexcept { return a.get() == it; } friend bool operator==(const Item* it, const ReadHandleImpl& a) noexcept { return a == it; } friend bool operator!=(const ReadHandleImpl& a, const Item* it) noexcept { return !(a == it); } friend bool operator!=(const Item* it, const ReadHandleImpl& a) noexcept { return !(a == it); } // == and != operators for comparison with nullptr friend bool operator==(const ReadHandleImpl& a, std::nullptr_t) noexcept { return a.get() == nullptr; } friend bool operator==(std::nullptr_t nullp, const ReadHandleImpl& a) noexcept { return a == nullp; } friend bool operator!=(const ReadHandleImpl& a, std::nullptr_t nullp) noexcept { return !(a == nullp); } friend bool operator!=(std::nullptr_t nullp, const ReadHandleImpl& a) noexcept { return !(a == nullp); } // == and != operator friend bool operator==(const ReadHandleImpl& a, const ReadHandleImpl& b) noexcept { return a.get() == b.get(); } friend bool operator!=(const ReadHandleImpl& a, const ReadHandleImpl& b) noexcept { return !(a == b); } // for use in bool contexts like `if (handle) { ... }` FOLLY_ALWAYS_INLINE explicit operator bool() const noexcept { return get() != nullptr; } // Accessors always return a const item. FOLLY_ALWAYS_INLINE const Item* operator->() const noexcept { return getInternal(); } FOLLY_ALWAYS_INLINE const Item& operator*() const noexcept { return *getInternal(); } FOLLY_ALWAYS_INLINE const Item* get() const noexcept { return getInternal(); } // Convert to semi future. folly::SemiFuture<ReadHandleImpl> toSemiFuture() && { if (isReady()) { return folly::makeSemiFuture(std::forward<ReadHandleImpl>(*this)); } folly::Promise<ReadHandleImpl> promise; auto semiFuture = promise.getSemiFuture(); auto cb = onReady([p = std::move(promise)](ReadHandleImpl handle) mutable { p.setValue(std::move(handle)); }); if (cb) { // Handle became ready after the initial isReady check. So we will run // the callback to set the promise inline. cb(std::move(*this)); return semiFuture; } else { return std::move(semiFuture).deferValue([](ReadHandleImpl handle) { if (handle) { // Increment one refcount on user thread since we transferred a handle // from a cachelib internal thread. handle.alloc_->adjustHandleCountForThread_private(1); } return handle; }); } } WriteHandleImpl<T> toWriteHandle() && { XDCHECK_NE(alloc_, nullptr); alloc_->invalidateNvm(*it_); return WriteHandleImpl<T>{std::move(*this)}; } using ReadyCallback = folly::Function<void(ReadHandleImpl)>; // Return true iff item handle is ready to use. // Empty handles are considered ready with it_ == nullptr. FOLLY_ALWAYS_INLINE bool isReady() const noexcept { return waitContext_ ? waitContext_->isReady() : true; } // Return true if this item has a wait context which means // it has missed in DRAM and went to nvm cache. bool wentToNvm() const noexcept { return getFlags() & static_cast<uint8_t>(HandleFlags::kWentToNvm); } // Return true if this handle couldn't be fulfilled because the item had // already expired. If item is present then the source of truth // lies with the actual item. bool wasExpired() const noexcept { return getFlags() & static_cast<uint8_t>(HandleFlags::kExpired); } // blocks until `isReady() == true`. void wait() const noexcept { if (isReady()) { return; } CHECK(waitContext_.get() != nullptr); waitContext_->wait(); } // Clones Item handle. returns an empty handle if it is null. // @return HandleImpl return a handle to this item // @throw std::overflow_error is the maximum item refcount is execeeded by // creating this item handle. ReadHandleImpl clone() const { ReadHandleImpl hdl{}; if (alloc_) { hdl = alloc_->acquire(getInternal()); } hdl.cloneFlags(*this); return hdl; } bool isWriteHandle() const { return false; } protected: // accessor. Calling get on handle with isReady() == false blocks the thread // until the handle is ready. FOLLY_ALWAYS_INLINE Item* getInternal() const noexcept { return waitContext_ ? waitContext_->get() : it_; } private: struct ItemWaitContext : public WaitContext<ReadHandleImpl> { explicit ItemWaitContext(CacheT& alloc) : alloc_(alloc) {} // @return managed item pointer // NOTE: get() blocks the thread until isReady is true Item* get() const noexcept { wait(); XDCHECK(isReady()); return it_.load(std::memory_order_acquire); } // Wait until we have the item void wait() const noexcept { if (isReady()) { return; } baton_.wait(); XDCHECK(isReady()); } uint8_t getFlags() const { return flags_; } // Assumes ownership of the item managed by hdl // and invokes the onReadyCallback_ // postcondition: `isReady() == true` // // NOTE: It's a bug to set a hdl that's already ready and can // terminate the application. This is only used internally within // cachelib and shouldn't be exposed outside of cachelib to applications. // // Active item handle count semantics // ---------------------------------- // // CacheLib keeps track of thread-local handle count in order to detect // if we have leaked handles on shutdown. The reason for thread-local is // to achieve optimal concurrency without forcing all threads to sync on // updating the same atomics. For async get (from nvm-cache), there are // three scenarios that we need to consider regarding the handle count. // 1. User calls "wait()" on a handle or relies on checking "isReady()". // 2. User adds a "onReadyCallback" via "onReady()". // 3. User converts handle to a "SemiFuture". // // For (2), user must increment the active handle count, if user's cb // is successfully enqueued. Something like the following. User can do // this at the beginning of their onReadyCallback. Now, beware that // user should NOT execute the onReadyCallback if the onReady() enqueue // failed, as that means the item is already ready, and there's no need // to adjust the refcount. // // TODO: T87126824. The behavior for (2) is far from ideal. CacheLib will // figure out a way to resolve this API and avoid the need for user // to explicitly adjust the handle count. // // For (1) and (3), do nothing. Cachelib automatically handles handle // counts internally. In particular, for (1), the current thread will // have "zero" outstanding handle for this handle user is accessing, // instead when the handle is destructed, we will "inc" the handle count // and then "dec" the handle count to achieve a net-zero change in count. // For (3), the for the "handle count" associated with the original // item-handle that had been converted to SemiFuture, we have done the // same as (1) which we will achieve a net-zero change at destruction. // In addition, we will be bumping the handle count by 1, when SemiFuture // is evaluated (via defer callback). This is because we have cloned // an item handle to be passed to the SemiFuture. void set(ReadHandleImpl hdl) override { XDCHECK(!isReady()); SCOPE_EXIT { hdl.release(); }; flags_ = hdl.getFlags(); auto it = hdl.getInternal(); it_.store(it, std::memory_order_release); // Handles are fulfilled by threads different from the owners. Adjust // the refcount tracking accordingly. use the local copy to not make // this an atomic load check. if (it) { alloc_.adjustHandleCountForThread_private(-1); } { std::lock_guard<std::mutex> l(mtx_); if (onReadyCallback_) { // We will construct another handle that will be transferred to // another thread. So we will decrement a count locally to be back // to 0 on this thread. In the user thread, they must increment by // 1. It is done automatically if the user converted their ItemHandle // to a SemiFuture via toSemiFuture(). auto readHandle = hdl.clone(); if (readHandle) { alloc_.adjustHandleCountForThread_private(-1); } onReadyCallback_(std::move(readHandle)); } } baton_.post(); } // @return true iff we have the item bool isReady() const noexcept { return it_.load(std::memory_order_acquire) != reinterpret_cast<const Item*>(kItemNotReady); } // Set the onReady callback // @param cb ReadyCallback // // @return callback function back if handle does not have waitContext_ // or if waitContext_ is ready. Caller is // expected to call this function // empty function if waitContext exists and is not ready // ReadyCallback onReady(ReadyCallback&& callBack) { std::lock_guard<std::mutex> l(mtx_); if (isReady()) { return std::move(callBack); } onReadyCallback_ = std::move(callBack); // callback consumed, return empty function return ReadyCallback(); } void releaseHandle() noexcept { // After @wait, callback is invoked. We don't have to worry about mutex. wait(); if (it_.exchange(nullptr, std::memory_order_release) != nullptr) { alloc_.adjustHandleCountForThread_private(1); } } ~ItemWaitContext() override { if (!isReady()) { XDCHECK(false); XLOG(CRITICAL, "Destorying an unresolved handle"); return; } auto it = it_.load(std::memory_order_acquire); if (it == nullptr) { return; } // If we have a wait context, we acquired the handle from another thread // that asynchronously created the handle. Fix up the thread local // refcount so that alloc_.release does not decrement it to negative. alloc_.adjustHandleCountForThread_private(1); try { alloc_.release(it, /* isNascent */ false); } catch (const std::exception& e) { XLOGF(CRITICAL, "Failed to release {:#10x} : {}", static_cast<void*>(it), e.what()); } } private: // we are waiting on Item* to be set to a value. One of the valid values is // nullptr. So choose something that we dont expect to indicate a ptr // state that is not valid. static constexpr uintptr_t kItemNotReady = 0x1221; mutable folly::fibers::Baton baton_; //< baton to wait on for the handle to // be "ready" std::mutex mtx_; //< mutex to set and get onReadyCallback_ ReadyCallback onReadyCallback_; //< callback invoked when "ready" std::atomic<Item*> it_{reinterpret_cast<Item*>(kItemNotReady)}; //< The item uint8_t flags_{}; //< flags associated with the handle generated by NvmCache CacheT& alloc_; //< allocator instance }; // Set the onReady callback which should be invoked once the item is ready. // If the item is ready, the callback is returned back to the user for // execution. // // If the callback is successfully enqueued, then within the callback, user // must increment per-thread handle count by 1. // cache->adjustHandleCountForThread_private(1); // This is needed because cachelib had previously moved a handle from an // internal thread to this callback, and cachelib internally removed a // 1. It is done automatically if the user converted their ItemHandle // to a SemiFuture via toSemiFuture(). For more details, refer to comments // around ItemWaitContext. // // @param callBack callback function // // @return an empty function if the callback was enqueued to be // executed when the handle becomes ready. // if the handle becomes/is ready, this returns the // original callback back to the caller to execute. // FOLLY_NODISCARD ReadyCallback onReady(ReadyCallback&& callBack) { return (waitContext_) ? waitContext_->onReady(std::move(callBack)) : std::move(callBack); } std::shared_ptr<ItemWaitContext> getItemWaitContext() const noexcept { return waitContext_; } // Internal book keeping to track handles that correspond to items that are // not present in cache. This state is mutated, but does not affect the user // visible meaning of the item handle(public API). Hence this is const. // markNascent is set when we know the handle is constructed for an item that // is not inserted into the cache yet. we unmark the nascent flag when we know // the item was successfully inserted into the cache by the caller. void markNascent() const { flags_ |= static_cast<uint8_t>(HandleFlags::kNascent); } void unmarkNascent() const { flags_ &= ~static_cast<uint8_t>(HandleFlags::kNascent); } bool isNascent() const { return flags_ & static_cast<uint8_t>(HandleFlags::kNascent); } void markExpired() { flags_ |= static_cast<uint8_t>(HandleFlags::kExpired); } void markWentToNvm() { flags_ |= static_cast<uint8_t>(HandleFlags::kWentToNvm); } uint8_t getFlags() const { return waitContext_ ? waitContext_->getFlags() : flags_; } void cloneFlags(const ReadHandleImpl& other) { flags_ = other.getFlags(); } Item* releaseItem() noexcept { return std::exchange(it_, nullptr); } // User of a handle can access cache via this accessor CacheT& getCache() const { XDCHECK(alloc_); return *alloc_; } // Handle which has the item already FOLLY_ALWAYS_INLINE ReadHandleImpl(Item* it, CacheT& alloc) noexcept : alloc_(&alloc), it_(it) {} // handle that has a wait context allocated. Used for async handles // In this case, the it_ will be filled in asynchronously and mulitple // ItemHandles can wait on the one underlying handle explicit ReadHandleImpl(CacheT& alloc) noexcept : alloc_(&alloc), it_(nullptr), waitContext_(std::make_shared<ItemWaitContext>(alloc)) {} // Only CacheAllocator and NvmCache can create non-default constructed handles friend CacheT; friend typename CacheT::NvmCacheT; // Object-cache's c++ allocator will need to create a zero refcount handle in // order to access CacheAllocator API. Search for this function for details. template <typename ItemHandle2, typename Item2, typename Cache2> friend ItemHandle2* objcacheInitializeZeroRefcountHandle(void* handleStorage, Item2* it, Cache2& alloc); // A handle is marked as nascent when it was not yet inserted into the cache. // However, user can override it by marking an item as "not nascent" even if // it's not inserted into the cache. Unmarking it means a not-yet-inserted // item will still be processed by RemoveCallback if user frees it. Today, // the only user who can do this is Cachelib's ObjectCache API to ensure the // correct RAII behavior for an object. template <typename ItemHandle2> friend void objcacheUnmarkNascent(const ItemHandle2& hdl); // Object-cache's c++ allocator needs to access CacheAllocator directly from // an item handle in order to access CacheAllocator APIs. template <typename ItemHandle2> friend typename ItemHandle2::CacheT& objcacheGetCache(const ItemHandle2& hdl); // instance of the cache this handle and item belong to. CacheT* alloc_ = nullptr; // pointer to item when the item does not have a wait context associated. Item* it_ = nullptr; // The waitContext allows an application to wait until the item is fetched. // This provides a future kind of interfaces (see ItemWaitContext for // details). std::shared_ptr<ItemWaitContext> waitContext_; mutable uint8_t flags_{}; // Only CacheAllocator and NvmCache can create non-default constructed handles friend CacheT; friend typename CacheT::NvmCacheT; // Following methods are only used in tests where we need to access private // methods in ItemHandle template <typename T1, typename T2> friend T1 createHandleWithWaitContextForTest(T2&); template <typename T1> friend std::shared_ptr<typename T1::ItemWaitContext> getWaitContextForTest( T1&); FRIEND_TEST(ItemHandleTest, WaitContext_readycb); FRIEND_TEST(ItemHandleTest, WaitContext_ready_immediate); FRIEND_TEST(ItemHandleTest, onReadyWithNoWaitContext); }; // WriteHandleImpl is a sub class of ReadHandleImpl to function as a mutable // handle. User is able to obtain a mutable item from a "write handle". template <typename T> struct WriteHandleImpl : public ReadHandleImpl<T> { using Item = T; using CacheT = typename T::CacheT; using ReadHandle = ReadHandleImpl<T>; using ReadHandle::ReadHandle; // inherit constructors // TODO(jiayueb): remove this constructor after we finish R/W handle // migration. In the end, WriteHandle should only be obtained via // CacheAllocator APIs like findToWrite(). explicit WriteHandleImpl(ReadHandle&& readHandle) : ReadHandle(std::move(readHandle)) {} // Accessors always return a non-const item. FOLLY_ALWAYS_INLINE Item* operator->() const noexcept { return ReadHandle::getInternal(); } FOLLY_ALWAYS_INLINE Item& operator*() const noexcept { return *ReadHandle::getInternal(); } FOLLY_ALWAYS_INLINE Item* get() const noexcept { return ReadHandle::getInternal(); } // Clones write handle. returns an empty handle if it is null. // @return WriteHandleImpl return a handle to this item // @throw std::overflow_error is the maximum item refcount is execeeded by // creating this item handle. WriteHandleImpl clone() const { return WriteHandleImpl{ReadHandle::clone()}; } bool isWriteHandle() const { return true; } // Friends // Only CacheAllocator and NvmCache can create non-default constructed handles friend CacheT; friend typename CacheT::NvmCacheT; // Object-cache's c++ allocator will need to create a zero refcount handle in // order to access CacheAllocator API. Search for this function for details. template <typename ItemHandle2, typename Item2, typename Cache2> friend ItemHandle2* objcacheInitializeZeroRefcountHandle(void* handleStorage, Item2* it, Cache2& alloc); // A handle is marked as nascent when it was not yet inserted into the cache. // However, user can override it by marking an item as "not nascent" even if // it's not inserted into the cache. Unmarking it means a not-yet-inserted // item will still be processed by RemoveCallback if user frees it. Today, // the only user who can do this is Cachelib's ObjectCache API to ensure the // correct RAII behavior for an object. template <typename ItemHandle2> friend void objcacheUnmarkNascent(const ItemHandle2& hdl); // Object-cache's c++ allocator needs to access CacheAllocator directly from // an item handle in order to access CacheAllocator APIs. template <typename ItemHandle2> friend typename ItemHandle2::CacheT& objcacheGetCache(const ItemHandle2& hdl); // Following methods are only used in tests where we need to access private // methods in ItemHandle template <typename T1, typename T2> friend T1 createHandleWithWaitContextForTest(T2&); template <typename T1> friend std::shared_ptr<typename T1::ItemWaitContext> getWaitContextForTest( T1&); FRIEND_TEST(ItemHandleTest, WaitContext_readycb); FRIEND_TEST(ItemHandleTest, WaitContext_ready_immediate); FRIEND_TEST(ItemHandleTest, onReadyWithNoWaitContext); }; template <typename T> std::ostream& operator<<(std::ostream& os, const ReadHandleImpl<T>& it) { if (it) { os << it->toString(); } else { os << "nullptr"; } return os; } } // namespace detail } // namespace cachelib } // namespace facebook