Clio  develop
The XRP Ledger API server.
Loading...
Searching...
No Matches
BlockingCache.hpp
1#pragma once
2
3#include "util/Assert.hpp"
4#include "util/Mutex.hpp"
5#include "util/Spawn.hpp"
6
7#include <boost/asio/error.hpp>
8#include <boost/asio/spawn.hpp>
9#include <boost/asio/steady_timer.hpp>
10#include <boost/signals2/connection.hpp>
11#include <boost/signals2/signal.hpp>
12#include <boost/signals2/variadic_signal.hpp>
13
14#include <atomic>
15#include <concepts>
16#include <expected>
17#include <functional>
18#include <optional>
19#include <shared_mutex>
20#include <utility>
21
22namespace util {
23
30template <typename ValueType, typename ErrorType>
31 requires(not std::same_as<ValueType, ErrorType>)
33public:
37 enum class State { NoValue, Updating, HasValue };
38
39private:
40 std::atomic<State> state_{State::NoValue};
41 util::Mutex<std::optional<ValueType>, std::shared_mutex> value_;
42 boost::signals2::signal<void(std::expected<ValueType, ErrorType>)> updateFinished_;
43
44public:
48 BlockingCache() = default;
49
54 explicit BlockingCache(ValueType initialValue)
55 : state_{State::HasValue}, value_(std::move(initialValue))
56 {
57 }
58
59 BlockingCache(BlockingCache&&) = delete;
60 BlockingCache(BlockingCache const&) = delete;
62 operator=(BlockingCache&&) = delete;
64 operator=(BlockingCache const&) = delete;
65
70 using Updater = std::function<std::expected<ValueType, ErrorType>(boost::asio::yield_context)>;
71
76 using Verifier = std::function<bool(ValueType const&)>;
77
91 [[nodiscard]] std::expected<ValueType, ErrorType>
92 asyncGet(boost::asio::yield_context yield, Updater updater, Verifier verifier)
93 {
94 switch (state_) {
95 case State::Updating: {
96 return wait(yield, std::move(updater), std::move(verifier));
97 }
98 case State::HasValue: {
99 auto const value = value_.template lock<std::shared_lock>();
100 ASSERT(value->has_value(), "Value should be presented when the cache is full");
101 return value->value();
102 }
103 case State::NoValue: {
104 return update(yield, std::move(updater), std::move(verifier));
105 }
106 };
107 std::unreachable();
108 }
109
121 [[nodiscard]] std::expected<ValueType, ErrorType>
122 update(boost::asio::yield_context yield, Updater updater, Verifier verifier)
123 {
124 if (state_ == State::Updating) {
125 return asyncGet(yield, std::move(updater), std::move(verifier));
126 }
127 state_ = State::Updating;
128
129 auto const result = updater(yield);
130 auto const shouldBeCached = result.has_value() and verifier(result.value());
131
132 if (shouldBeCached) {
133 value_.lock().get() = result.value();
134 state_ = State::HasValue;
135 } else {
136 state_ = State::NoValue;
137 value_.lock().get() = std::nullopt;
138 }
139
140 updateFinished_(result);
141 return result;
142 }
143
150 void
152 {
153 if (state_ == State::HasValue) {
154 state_ = State::NoValue;
155 value_.lock().get() = std::nullopt;
156 }
157 }
158
163 [[nodiscard]] State
164 state() const
165 {
166 return state_;
167 }
168
169private:
180 std::expected<ValueType, ErrorType>
181 wait(boost::asio::yield_context yield, Updater updater, Verifier verifier)
182 {
183 struct SharedContext {
184 SharedContext(boost::asio::yield_context y)
185 : timer(y.get_executor(), boost::asio::steady_timer::duration::max())
186 {
187 }
188
189 boost::asio::steady_timer timer;
190 std::optional<std::expected<ValueType, ErrorType>> result;
191 };
192
193 auto sharedContext = std::make_shared<SharedContext>(yield);
194 boost::system::error_code errorCode;
195
196 boost::signals2::scoped_connection const slot =
197 updateFinished_.connect([yield,
198 sharedContext](std::expected<ValueType, ErrorType> value) {
200 yield,
201 [sharedContext = std::move(sharedContext), value = std::move(value)](auto&&) {
202 sharedContext->result = std::move(value);
203 sharedContext->timer.cancel();
204 }
205 );
206 });
207
208 if (state_ == State::Updating) {
209 sharedContext->timer.async_wait(yield[errorCode]);
210 ASSERT(sharedContext->result.has_value(), "There should be some value after waiting");
211 return std::move(sharedContext->result).value();
212 }
213 return asyncGet(yield, std::move(updater), std::move(verifier));
214 }
215};
216
217} // namespace util
A thread-safe cache that blocks getting operations until the cache is updated.
Definition BlockingCache.hpp:32
void invalidate()
Invalidates the currently cached value if present.
Definition BlockingCache.hpp:151
BlockingCache(ValueType initialValue)
Construct a cache with an initial value.
Definition BlockingCache.hpp:54
std::expected< ValueType, ErrorType > asyncGet(boost::asio::yield_context yield, Updater updater, Verifier verifier)
Asynchronously get a value from the cache, updating if necessary.
Definition BlockingCache.hpp:92
State state() const
Returns the current state of the cache.
Definition BlockingCache.hpp:164
State
Possible states of the cache.
Definition BlockingCache.hpp:37
std::function< std::expected< ValueType, ErrorType >(boost::asio::yield_context)> Updater
Function type for cache update operations.
Definition BlockingCache.hpp:70
BlockingCache()=default
Default constructor - creates an empty cache.
std::expected< ValueType, ErrorType > update(boost::asio::yield_context yield, Updater updater, Verifier verifier)
Force an update of the cache value.
Definition BlockingCache.hpp:122
std::function< bool(ValueType const &)> Verifier
Function type to verify if a value should be cached.
Definition BlockingCache.hpp:76
A container for data that is protected by a mutex. Inspired by Mutex in Rust.
Definition Mutex.hpp:82
This namespace contains various utilities.
Definition AccountUtils.hpp:11
void spawn(Ctx &&ctx, F &&func)
Spawns a coroutine using boost::asio::spawn.
Definition Spawn.hpp:53