//------------------------------------------------------------------------------ /* This file is part of clio: https://github.com/XRPLF/clio Copyright (c) 2025, the clio developers. Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ //============================================================================== #pragma once #include "util/Assert.hpp" #include "util/Mutex.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include namespace util { /** * @brief A thread-safe cache that blocks getting operations until the cache is updated * * @tparam ValueType The type of value to be cached * @tparam ErrorType The type of error that can occur during updates */ template requires(not std::same_as) class BlockingCache { public: /** * @brief Possible states of the cache */ enum class State { NoValue, Updating, HasValue }; private: std::atomic state_{State::NoValue}; util::Mutex, std::shared_mutex> value_; boost::signals2::signal)> updateFinished_; public: /** * @brief Default constructor - creates an empty cache */ BlockingCache() = default; /** * @brief Construct a cache with an initial value * @param initialValue The value to initialize the cache with */ explicit BlockingCache(ValueType initialValue) : state_{State::HasValue}, value_(std::move(initialValue)) { } BlockingCache(BlockingCache&&) = delete; BlockingCache(BlockingCache const&) = delete; BlockingCache& operator=(BlockingCache&&) = delete; BlockingCache& operator=(BlockingCache const&) = delete; /** * @brief Function type for cache update operations * @details Called when the cache needs to be populated or refreshed */ using Updater = std::function(boost::asio::yield_context)>; /** * @brief Function type to verify if a value should be cached * @details Returns true if the value should be stored in the cache */ using Verifier = std::function; /** * @brief Asynchronously get a value from the cache, updating if necessary * * @param yield The asio yield context for coroutine suspension * @param updater Function to generate a new value if needed * @param verifier Function to validate whether a value should be cached * @return std::expected The cached value or an error * * Depending on the current cache state, this will either: * - Return the cached value if it's already present * - Wait for an ongoing update to complete * - Trigger a new update if the cache is empty */ [[nodiscard]] std::expected asyncGet(boost::asio::yield_context yield, Updater updater, Verifier verifier) { switch (state_) { case State::Updating: { return wait(yield, std::move(updater), std::move(verifier)); } case State::HasValue: { auto const value = value_.template lock(); ASSERT(value->has_value(), "Value should be presented when the cache is full"); return value->value(); } case State::NoValue: { return update(yield, std::move(updater), std::move(verifier)); } }; std::unreachable(); } /** * @brief Force an update of the cache value * * @param yield The ASIO yield context for coroutine suspension * @param updater Function to generate a new value * @param verifier Function to validate whether a value should be cached * @return std::expected The new value or an error * * Initiates a cache update operation regardless of current state. * If another update is already in progress, waits for it to complete. */ [[nodiscard]] std::expected update(boost::asio::yield_context yield, Updater updater, Verifier verifier) { if (state_ == State::Updating) { return asyncGet(yield, std::move(updater), std::move(verifier)); } state_ = State::Updating; auto const result = updater(yield); auto const shouldBeCached = result.has_value() and verifier(result.value()); if (shouldBeCached) { value_.lock().get() = result.value(); state_ = State::HasValue; } else { state_ = State::NoValue; value_.lock().get() = std::nullopt; } updateFinished_(result); return result; } /** * @brief Invalidates the currently cached value if present * * Clears the cache and sets its state to Empty. * Has no effect if the cache is already empty or being updated. */ void invalidate() { if (state_ == State::HasValue) { state_ = State::NoValue; value_.lock().get() = std::nullopt; } } /** * @brief Returns the current state of the cache * @return Current cache state (Empty, Updating, or Full) */ [[nodiscard]] State state() const { return state_; } private: /** * @brief Wait for an ongoing update to complete * * @param yield The ASIO yield context for coroutine suspension * @param updater Function to generate a new value if needed * @param verifier Function to validate whether a value should be cached * @return std::expected The result of the ongoing update * * This method blocks the current coroutine until the ongoing update signals completion. */ std::expected wait(boost::asio::yield_context yield, Updater updater, Verifier verifier) { boost::asio::steady_timer timer{yield.get_executor(), boost::asio::steady_timer::duration::max()}; boost::system::error_code errorCode; std::optional> result; boost::signals2::scoped_connection const slot = updateFinished_.connect([yield, &timer, &result](std::expected value) { boost::asio::spawn(yield, [&timer, &result, value = std::move(value)](auto&&) { result = std::move(value); timer.cancel(); }); }); if (state_ == State::Updating) { timer.async_wait(yield[errorCode]); ASSERT(result.has_value(), "There should be some value after waiting"); return std::move(result).value(); } return asyncGet(yield, std::move(updater), std::move(verifier)); } }; } // namespace util