userver: userver/cache/caching_component_base.hpp Source File
⚠️ This is the documentation for an old userver version. Click here to switch to the latest version.
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Modules Pages Concepts
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