mirror of
https://github.com/XRPLF/clio.git
synced 2026-04-29 15:37:53 +00:00
Fixes #2012. Please review and commit clang-tidy fixes. Co-authored-by: godexsoft <385326+godexsoft@users.noreply.github.com>
222 lines
7.7 KiB
C++
222 lines
7.7 KiB
C++
//------------------------------------------------------------------------------
|
|
/*
|
|
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 <boost/asio/error.hpp>
|
|
#include <boost/asio/spawn.hpp>
|
|
#include <boost/asio/steady_timer.hpp>
|
|
#include <boost/signals2/connection.hpp>
|
|
#include <boost/signals2/signal.hpp>
|
|
#include <boost/signals2/variadic_signal.hpp>
|
|
|
|
#include <atomic>
|
|
#include <concepts>
|
|
#include <expected>
|
|
#include <functional>
|
|
#include <optional>
|
|
#include <shared_mutex>
|
|
#include <utility>
|
|
|
|
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 <typename ValueType, typename ErrorType>
|
|
requires(not std::same_as<ValueType, ErrorType>)
|
|
class BlockingCache {
|
|
public:
|
|
/**
|
|
* @brief Possible states of the cache
|
|
*/
|
|
enum class State { NoValue, Updating, HasValue };
|
|
|
|
private:
|
|
std::atomic<State> state_{State::NoValue};
|
|
util::Mutex<std::optional<ValueType>, std::shared_mutex> value_;
|
|
boost::signals2::signal<void(std::expected<ValueType, ErrorType>)> 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<std::expected<ValueType, ErrorType>(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<bool(ValueType const&)>;
|
|
|
|
/**
|
|
* @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<ValueType, ErrorType> 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<ValueType, ErrorType>
|
|
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<std::shared_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<ValueType, ErrorType> 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<ValueType, ErrorType>
|
|
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<ValueType, ErrorType> The result of the ongoing update
|
|
*
|
|
* This method blocks the current coroutine until the ongoing update signals completion.
|
|
*/
|
|
std::expected<ValueType, ErrorType>
|
|
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<std::expected<ValueType, ErrorType>> result;
|
|
boost::signals2::scoped_connection const slot =
|
|
updateFinished_.connect([yield, &timer, &result](std::expected<ValueType, ErrorType> 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
|