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 void Set(std::unique_ptr<const T> value_ptr);
156 void Set(T&& value);
157
158 template <typename... Args>
159 void Emplace(Args&&... args);
160
161 void Clear();
162
163 /// Whether Get() is expected to return nullptr.
164 /// If MayReturnNull() returns false, Get() throws an exception instead of
165 /// returning nullptr.
166 virtual bool MayReturnNull() const;
167
168 /// @{
169 /// Override to use custom serialization for cache dumps
170 virtual void WriteContents(dump::Writer& writer, const T& contents) const;
171
172 virtual std::unique_ptr<const T> ReadContents(dump::Reader& reader) const;
173 /// @}
174
175 private:
176 void OnAllComponentsLoaded() final;
177
178 void Cleanup() final;
179
180 void MarkAsExpired() final;
181
182 void GetAndWrite(dump::Writer& writer) const final;
183 void ReadAndSet(dump::Reader& reader) final;
184
185 /// @brief If the option has-pre-assign-check is set true in static config,
186 /// this function is called before assigning the new value to the cache
187 /// @note old_value_ptr and new_value_ptr can be nullptr.
188 virtual void PreAssignCheck(const T* old_value_ptr,
189 const T* new_value_ptr) const;
190
191 rcu::Variable<std::shared_ptr<const T>> cache_;
192 concurrent::AsyncEventChannel<const std::shared_ptr<const T>&> event_channel_;
193 utils::impl::WaitTokenStorage wait_token_storage_;
194};
195
196template <typename T>
197CachingComponentBase<T>::CachingComponentBase(const ComponentConfig& config,
198 const ComponentContext& context)
199 : LoggableComponentBase(config, context),
200 cache::CacheUpdateTrait(config, context),
201 event_channel_(components::GetCurrentComponentName(config),
202 [this](auto& function) {
203 const auto ptr = cache_.ReadCopy();
204 if (ptr) function(ptr);
205 }) {
206 const auto initial_config = GetConfig();
207}
208
209template <typename T>
210CachingComponentBase<T>::~CachingComponentBase() {
211 // Avoid a deadlock in WaitForAllTokens
212 cache_.Assign(nullptr);
213 // We must wait for destruction of all instances of T to finish, otherwise
214 // it's UB if T's destructor accesses dependent components
215 wait_token_storage_.WaitForAllTokens();
216}
217
218template <typename T>
219utils::SharedReadablePtr<T> CachingComponentBase<T>::Get() const {
220 auto ptr = GetUnsafe();
221 if (!ptr && !MayReturnNull()) {
222 throw cache::EmptyCacheError(Name());
223 }
224 return ptr;
225}
226
227template <typename T>
228template <typename Class>
229concurrent::AsyncEventSubscriberScope CachingComponentBase<T>::UpdateAndListen(
230 Class* obj, std::string name,
231 void (Class::*func)(const std::shared_ptr<const T>&)) {
232 return event_channel_.DoUpdateAndListen(obj, std::move(name), func, [&] {
233 auto ptr = Get(); // TODO: extra ref
234 (obj->*func)(ptr);
235 });
236}
237
238template <typename T>
239concurrent::AsyncEventChannel<const std::shared_ptr<const T>&>&
240CachingComponentBase<T>::GetEventChannel() {
241 return event_channel_;
242}
243
244template <typename T>
245utils::SharedReadablePtr<T> CachingComponentBase<T>::GetUnsafe() const {
246 return utils::SharedReadablePtr<T>(cache_.ReadCopy());
247}
248
249template <typename T>
250void CachingComponentBase<T>::Set(std::unique_ptr<const T> value_ptr) {
251 auto deleter = [token = wait_token_storage_.GetToken(),
252 &cache_task_processor =
253 GetCacheTaskProcessor()](const T* raw_ptr) mutable {
254 std::unique_ptr<const T> ptr{raw_ptr};
255
256 // Kill garbage asynchronously as T::~T() might be very slow
257 engine::CriticalAsyncNoSpan(cache_task_processor, [ptr = std::move(ptr),
258 token = std::move(
259 token)]() mutable {
260 // Make sure *ptr is deleted before token is destroyed
261 ptr.reset();
262 }).Detach();
263 };
264
265 const std::shared_ptr<const T> new_value(value_ptr.release(),
266 std::move(deleter));
267
268 if (HasPreAssignCheck()) {
269 auto old_value = cache_.Read();
270 PreAssignCheck(old_value->get(), new_value.get());
271 }
272
273 cache_.Assign(new_value);
274 event_channel_.SendEvent(new_value);
276}
277
278template <typename T>
279void CachingComponentBase<T>::Set(T&& value) {
280 Emplace(std::move(value));
281}
282
283template <typename T>
284template <typename... Args>
285void CachingComponentBase<T>::Emplace(Args&&... args) {
286 Set(std::make_unique<T>(std::forward<Args>(args)...));
287}
288
289template <typename T>
290void CachingComponentBase<T>::Clear() {
291 cache_.Assign(std::make_unique<const T>());
292}
293
294template <typename T>
296 return false;
297}
298
299template <typename T>
300void CachingComponentBase<T>::GetAndWrite(dump::Writer& writer) const {
301 const auto contents = GetUnsafe();
302 if (!contents) throw cache::EmptyCacheError(Name());
303 WriteContents(writer, *contents);
304}
305
306template <typename T>
307void CachingComponentBase<T>::ReadAndSet(dump::Reader& reader) {
308 Set(ReadContents(reader));
309}
310
311template <typename T>
313 const T& contents) const {
314 if constexpr (dump::kIsDumpable<T>) {
315 writer.Write(contents);
316 } else {
317 dump::ThrowDumpUnimplemented(Name());
318 }
319}
320
321template <typename T>
322std::unique_ptr<const T> CachingComponentBase<T>::ReadContents(
323 dump::Reader& reader) const {
324 if constexpr (dump::kIsDumpable<T>) {
325 // To avoid an extra move and avoid including common_containers.hpp
326 return std::unique_ptr<const T>{new T(reader.Read<T>())};
327 } else {
328 dump::ThrowDumpUnimplemented(Name());
329 }
330}
331
332template <typename T>
333void CachingComponentBase<T>::OnAllComponentsLoaded() {
334 AssertPeriodicUpdateStarted();
335}
336
337template <typename T>
338void CachingComponentBase<T>::Cleanup() {
339 cache_.Cleanup();
340}
341
342template <typename T>
343void CachingComponentBase<T>::MarkAsExpired() {
344 Set(std::unique_ptr<const T>{});
345}
346
347namespace impl {
348
349yaml_config::Schema GetCachingComponentBaseSchema();
350
351}
352
353template <typename T>
354yaml_config::Schema CachingComponentBase<T>::GetStaticConfigSchema() {
355 return impl::GetCachingComponentBaseSchema();
356}
357
358template <typename T>
359void CachingComponentBase<T>::PreAssignCheck(
360 const T*, [[maybe_unused]] const T* new_value_ptr) const {
362 meta::kIsSizable<T>,
363 fmt::format("{} type does not support std::size(), add implementation of "
364 "the method size() for this type or "
365 "override cache::CachingComponentBase::PreAssignCheck.",
366 compiler::GetTypeName<T>()));
367
368 if constexpr (meta::kIsSizable<T>) {
369 if (!new_value_ptr || std::size(*new_value_ptr) == 0) {
370 throw cache::EmptyDataError(Name());
371 }
372 }
373}
374
375} // namespace components
376
377USERVER_NAMESPACE_END