userver: userver/cache/caching_component_base.hpp Source File
Loading...
Searching...
No Matches
caching_component_base.hpp
Go to the documentation of this file.
1#pragma once
2
3/// @file userver/cache/caching_component_base.hpp
4/// @brief @copybrief components::CachingComponentBase
5
6#include <memory>
7#include <string>
8#include <utility>
9
10#include <fmt/format.h>
11
12#include <userver/cache/cache_update_trait.hpp>
13#include <userver/cache/exceptions.hpp>
14#include <userver/compiler/demangle.hpp>
15#include <userver/components/component_fwd.hpp>
16#include <userver/components/loggable_component_base.hpp>
17#include <userver/concurrent/async_event_channel.hpp>
18#include <userver/dump/helpers.hpp>
19#include <userver/dump/meta.hpp>
20#include <userver/dump/operations.hpp>
21#include <userver/engine/async.hpp>
22#include <userver/rcu/rcu.hpp>
23#include <userver/utils/assert.hpp>
24#include <userver/utils/impl/wait_token_storage.hpp>
25#include <userver/utils/meta.hpp>
26#include <userver/utils/shared_readable_ptr.hpp>
27#include <userver/yaml_config/schema.hpp>
28
29USERVER_NAMESPACE_BEGIN
30
31namespace components {
32
33// clang-format off
34
35/// @ingroup userver_components userver_base_classes
36///
37/// @brief Base class for caching components
38///
39/// Provides facilities for creating periodically updated caches.
40/// You need to override cache::CacheUpdateTrait::Update
41/// then call cache::CacheUpdateTrait::StartPeriodicUpdates after setup
42/// and cache::CacheUpdateTrait::StopPeriodicUpdates before teardown.
43/// You can also override cache::CachingComponentBase::PreAssignCheck and set
44/// has-pre-assign-check: true in the static config to enable check.
45///
46/// Caching components must be configured in service config (see options below)
47/// and may be reconfigured dynamically via components::DynamicConfig.
48///
49/// @ref scripts/docs/en/userver/caches.md provide a more detailed introduction.
50///
51/// ## Dynamic config
52/// * @ref USERVER_CACHES
53/// * @ref USERVER_DUMPS
54///
55/// ## Static options:
56/// Name | Description | Default value
57/// ---- | ----------- | -------------
58/// update-types | specifies whether incremental and/or full updates will be used | see below
59/// update-interval | (*required*) interval between Update invocations | --
60/// update-jitter | max. amount of time by which update-interval may be adjusted for requests dispersal | update_interval / 10
61/// full-update-interval | interval between full updates | --
62/// full-update-jitter | max. amount of time by which full-update-interval may be adjusted for requests dispersal | full-update-interval / 10
63/// updates-enabled | if false, cache updates are disabled (except for the first one if !first-update-fail-ok) | true
64/// first-update-fail-ok | whether first update failure is non-fatal | false
65/// task-processor | the name of the TaskProcessor for running DoWork | main-task-processor
66/// config-settings | enables dynamic reconfiguration with CacheConfigSet | true
67/// exception-interval | Used instead of `update-interval` in case of exception | update_interval
68/// additional-cleanup-interval | how often to run background RCU garbage collector | 10 seconds
69/// is-strong-period | whether to include Update execution time in update-interval | false
70/// testsuite-force-periodic-update | override testsuite-periodic-update-enabled in TestsuiteSupport component config | --
71/// failed-updates-before-expiration | the number of consecutive failed updates for data expiration | --
72/// has-pre-assign-check | enables the check before changing the value in the cache, by default it is the check that the new value is not empty | false
73/// alert-on-failing-to-update-times | fire an alert if the cache update failed specified amount of times in a row. If zero - alerts are disabled. Value from dynamic config takes priority over static | 0
74/// dump.* | Manages cache behavior after dump load | -
75/// dump.first-update-mode | Behavior of update after successful load from dump. See info on modes below | skip
76/// dump.first-update-type | Update type after successful load from dump (`full`, `incremental` or `incremental-then-async-full`) | full
77/// ### Update types
78/// * `full-and-incremental`: both `update-interval` and `full-update-interval`
79/// must be specified. Updates with UpdateType::kIncremental will be triggered
80/// each `update-interval` (adjusted by jitter) unless `full-update-interval`
81/// has passed and UpdateType::kFull is triggered.
82/// * `only-full`: only `update-interval` must be specified. UpdateType::kFull
83/// will be triggered each `update-interval` (adjusted by jitter).
84/// * `only-incremental`: only `update-interval` must be specified. UpdateType::kFull is triggered
85/// on the first update, afterwards UpdateType::kIncremental will be triggered
86/// each `update-interval` (adjusted by jitter).
87///
88/// ### Avoiding memory leaks
89/// If you don't implement the deletion of objects that are deleted from the data source and don't use full updates,
90/// you may get an effective memory leak, because garbage objects will pile up in the cached data.
91///
92/// Calculation example:
93/// * size of database: 1000 objects
94/// * removal rate: 30 objects per minute (0.5 objects per second)
95///
96/// Let's say we allow 20% extra garbage objects in cache in addition to the actual objects from the database. In this case we need:
97///
98/// full-update-interval = (size-of-database * 20% / removal-rate) = 400s
99///
100/// ### `first-update-mode` modes
101/// Mode | Description
102/// ------------- | -----------
103/// `skip` | after successful load from dump, do nothing
104/// `required` | make a synchronous update of type `first-update-type`, stop the service on failure
105/// `best-effort` | make a synchronous update of type `first-update-type`, keep working and use data from dump on failure
106///
107/// ### testsuite-force-periodic-update
108/// use it to enable periodic cache update for a component in testsuite environment
109/// where testsuite-periodic-update-enabled from TestsuiteSupport config is false
110///
111/// By default, update types are guessed based on update intervals presence.
112/// If both `update-interval` and `full-update-interval` are present,
113/// `full-and-incremental` types is assumed. Otherwise `only-full` is used.
114///
115/// @see `dump::Dumper` for more info on persistent cache dumps and
116/// corresponding config options.
117///
118/// @see @ref scripts/docs/en/userver/caches.md. pytest_userver.client.Client.invalidate_caches()
119/// for a function to force cache update from testsuite.
120
121// clang-format on
122
123template <typename T>
124// NOLINTNEXTLINE(fuchsia-multiple-inheritance)
126 protected cache::CacheUpdateTrait {
127 public:
128 CachingComponentBase(const ComponentConfig& config, const ComponentContext&);
129 ~CachingComponentBase() override;
130
131 using cache::CacheUpdateTrait::Name;
132
133 using DataType = T;
134
135 /// @return cache contents. May be nullptr if and only if MayReturnNull()
136 /// returns true.
137 utils::SharedReadablePtr<T> Get() const;
138
139 /// @return cache contents. May be nullptr regardless of MayReturnNull().
140 utils::SharedReadablePtr<T> GetUnsafe() const;
141
142 /// Subscribes to cache updates using a member function. Also immediately
143 /// invokes the function with the current cache contents.
144 template <class Class>
145 concurrent::AsyncEventSubscriberScope UpdateAndListen(
146 Class* obj, std::string name,
147 void (Class::*func)(const std::shared_ptr<const T>&));
148
149 concurrent::AsyncEventChannel<const std::shared_ptr<const T>&>&
150 GetEventChannel();
151
152 static yaml_config::Schema GetStaticConfigSchema();
153
154 protected:
155 /// Sets the new value of cache. As a result the Get() member function starts
156 /// returning the value passed into this function after the Update() finishes.
157 ///
158 /// @warning Do not forget to update cache::UpdateStatisticsScope, otherwise
159 /// the behavior is undefined.
160 void Set(std::unique_ptr<const T> value_ptr);
161
162 /// @overload
163 void Set(T&& value);
164
165 /// @overload Set()
166 template <typename... Args>
167 void Emplace(Args&&... args);
168
169 /// Clears the content of the cache by string a default constructed T.
170 void Clear();
171
172 /// Whether Get() is expected to return nullptr.
173 /// If MayReturnNull() returns false, Get() throws an exception instead of
174 /// returning nullptr.
175 virtual bool MayReturnNull() const;
176
177 /// @{
178 /// Override to use custom serialization for cache dumps
179 virtual void WriteContents(dump::Writer& writer, const T& contents) const;
180
181 virtual std::unique_ptr<const T> ReadContents(dump::Reader& reader) const;
182 /// @}
183
184 /// @brief If the option has-pre-assign-check is set true in static config,
185 /// this function is called before assigning the new value to the cache
186 /// @note old_value_ptr and new_value_ptr can be nullptr.
187 virtual void PreAssignCheck(const T* old_value_ptr,
188 const T* new_value_ptr) const;
189
190 private:
191 void OnAllComponentsLoaded() final;
192
193 void Cleanup() final;
194
195 void MarkAsExpired() final;
196
197 void GetAndWrite(dump::Writer& writer) const final;
198 void ReadAndSet(dump::Reader& reader) final;
199
200 rcu::Variable<std::shared_ptr<const T>> cache_;
201 concurrent::AsyncEventChannel<const std::shared_ptr<const T>&> event_channel_;
202 utils::impl::WaitTokenStorage wait_token_storage_;
203};
204
205template <typename T>
206CachingComponentBase<T>::CachingComponentBase(const ComponentConfig& config,
207 const ComponentContext& context)
208 : LoggableComponentBase(config, context),
209 cache::CacheUpdateTrait(config, context),
210 event_channel_(components::GetCurrentComponentName(config),
211 [this](auto& function) {
212 const auto ptr = cache_.ReadCopy();
213 if (ptr) function(ptr);
214 }) {
215 const auto initial_config = GetConfig();
216}
217
218template <typename T>
219CachingComponentBase<T>::~CachingComponentBase() {
220 // Avoid a deadlock in WaitForAllTokens
221 cache_.Assign(nullptr);
222 // We must wait for destruction of all instances of T to finish, otherwise
223 // it's UB if T's destructor accesses dependent components
224 wait_token_storage_.WaitForAllTokens();
225}
226
227template <typename T>
228utils::SharedReadablePtr<T> CachingComponentBase<T>::Get() const {
229 auto ptr = GetUnsafe();
230 if (!ptr && !MayReturnNull()) {
231 throw cache::EmptyCacheError(Name());
232 }
233 return ptr;
234}
235
236template <typename T>
237template <typename Class>
238concurrent::AsyncEventSubscriberScope CachingComponentBase<T>::UpdateAndListen(
239 Class* obj, std::string name,
240 void (Class::*func)(const std::shared_ptr<const T>&)) {
241 return event_channel_.DoUpdateAndListen(obj, std::move(name), func, [&] {
242 auto ptr = Get(); // TODO: extra ref
243 (obj->*func)(ptr);
244 });
245}
246
247template <typename T>
248concurrent::AsyncEventChannel<const std::shared_ptr<const T>&>&
249CachingComponentBase<T>::GetEventChannel() {
250 return event_channel_;
251}
252
253template <typename T>
254utils::SharedReadablePtr<T> CachingComponentBase<T>::GetUnsafe() const {
255 return utils::SharedReadablePtr<T>(cache_.ReadCopy());
256}
257
258template <typename T>
259void CachingComponentBase<T>::Set(std::unique_ptr<const T> value_ptr) {
260 auto deleter = [token = wait_token_storage_.GetToken(),
261 &cache_task_processor =
262 GetCacheTaskProcessor()](const T* raw_ptr) mutable {
263 std::unique_ptr<const T> ptr{raw_ptr};
264
265 // Kill garbage asynchronously as T::~T() might be very slow
266 engine::CriticalAsyncNoSpan(cache_task_processor, [ptr = std::move(ptr),
267 token = std::move(
268 token)]() mutable {
269 // Make sure *ptr is deleted before token is destroyed
270 ptr.reset();
271 }).Detach();
272 };
273
274 const std::shared_ptr<const T> new_value(value_ptr.release(),
275 std::move(deleter));
276
277 if (HasPreAssignCheck()) {
278 auto old_value = cache_.Read();
279 PreAssignCheck(old_value->get(), new_value.get());
280 }
281
282 cache_.Assign(new_value);
283 event_channel_.SendEvent(new_value);
285}
286
287template <typename T>
288void CachingComponentBase<T>::Set(T&& value) {
289 Emplace(std::move(value));
290}
291
292template <typename T>
293template <typename... Args>
294void CachingComponentBase<T>::Emplace(Args&&... args) {
295 Set(std::make_unique<T>(std::forward<Args>(args)...));
296}
297
298template <typename T>
300 cache_.Assign(std::make_unique<const T>());
301}
302
303template <typename T>
305 return false;
306}
307
308template <typename T>
309void CachingComponentBase<T>::GetAndWrite(dump::Writer& writer) const {
310 const auto contents = GetUnsafe();
311 if (!contents) throw cache::EmptyCacheError(Name());
312 WriteContents(writer, *contents);
313}
314
315template <typename T>
316void CachingComponentBase<T>::ReadAndSet(dump::Reader& reader) {
317 auto data = ReadContents(reader);
318 if constexpr (meta::kIsSizable<T>) {
319 if (data) {
320 SetDataSizeStatistic(std::size(*data));
321 }
322 }
323 Set(std::move(data));
324}
325
326template <typename T>
328 const T& contents) const {
329 if constexpr (dump::kIsDumpable<T>) {
330 writer.Write(contents);
331 } else {
332 dump::ThrowDumpUnimplemented(Name());
333 }
334}
335
336template <typename T>
337std::unique_ptr<const T> CachingComponentBase<T>::ReadContents(
338 dump::Reader& reader) const {
339 if constexpr (dump::kIsDumpable<T>) {
340 // To avoid an extra move and avoid including common_containers.hpp
341 return std::unique_ptr<const T>{new T(reader.Read<T>())};
342 } else {
343 dump::ThrowDumpUnimplemented(Name());
344 }
345}
346
347template <typename T>
348void CachingComponentBase<T>::OnAllComponentsLoaded() {
349 AssertPeriodicUpdateStarted();
350}
351
352template <typename T>
353void CachingComponentBase<T>::Cleanup() {
354 cache_.Cleanup();
355}
356
357template <typename T>
358void CachingComponentBase<T>::MarkAsExpired() {
359 Set(std::unique_ptr<const T>{});
360}
361
362namespace impl {
363
364yaml_config::Schema GetCachingComponentBaseSchema();
365
366}
367
368template <typename T>
369yaml_config::Schema CachingComponentBase<T>::GetStaticConfigSchema() {
370 return impl::GetCachingComponentBaseSchema();
371}
372
373template <typename T>
375 const T*, [[maybe_unused]] const T* new_value_ptr) const {
377 meta::kIsSizable<T>,
378 fmt::format("{} type does not support std::size(), add implementation of "
379 "the method size() for this type or "
380 "override cache::CachingComponentBase::PreAssignCheck.",
381 compiler::GetTypeName<T>()));
382
383 if constexpr (meta::kIsSizable<T>) {
384 if (!new_value_ptr || std::size(*new_value_ptr) == 0) {
385 throw cache::EmptyDataError(Name());
386 }
387 }
388}
389
390} // namespace components
391
392USERVER_NAMESPACE_END