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_base.hpp>
16#include <userver/components/component_fwd.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///
57/// Name | Description | Default value
58/// ---- | ----------- | -------------
59/// update-types | specifies whether incremental and/or full updates will be used | see below
60/// update-interval | (*required*) interval between Update invocations | --
61/// update-jitter | max. amount of time by which update-interval may be adjusted for requests dispersal | update_interval / 10
62/// full-update-interval | interval between full updates | --
63/// full-update-jitter | max. amount of time by which full-update-interval may be adjusted for requests dispersal | full-update-interval / 10
64/// updates-enabled | if false, cache updates are disabled (except for the first one if !first-update-fail-ok) | true
65/// first-update-fail-ok | whether first update failure is non-fatal; see also @ref MayReturnNull | false
66/// task-processor | the name of the TaskProcessor for running DoWork | main-task-processor
67/// config-settings | enables dynamic reconfiguration with CacheConfigSet | true
68/// exception-interval | Used instead of `update-interval` in case of exception | update_interval
69/// additional-cleanup-interval | how often to run background RCU garbage collector | 10 seconds
70/// is-strong-period | whether to include Update execution time in update-interval | false
71/// testsuite-force-periodic-update | override testsuite-periodic-update-enabled in TestsuiteSupport component config | --
72/// failed-updates-before-expiration | the number of consecutive failed updates for data expiration | --
73/// 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
74/// 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
75/// safe-data-lifetime | enables awaiting data destructors in the component's destructor. Can be set to `false` if the stored data does not refer to the component and its dependencies. | true
76/// dump.* | Manages cache behavior after dump load | -
77/// dump.first-update-mode | Behavior of update after successful load from dump. See info on modes below | skip
78/// dump.first-update-type | Update type after successful load from dump (`full`, `incremental` or `incremental-then-async-full`) | full
79///
80/// ### Update types
81/// * `full-and-incremental`: both `update-interval` and `full-update-interval`
82/// must be specified. Updates with UpdateType::kIncremental will be triggered
83/// each `update-interval` (adjusted by jitter) unless `full-update-interval`
84/// has passed and UpdateType::kFull is triggered.
85/// * `only-full`: only `update-interval` must be specified. UpdateType::kFull
86/// will be triggered each `update-interval` (adjusted by jitter).
87/// * `only-incremental`: only `update-interval` must be specified. UpdateType::kFull is triggered
88/// on the first update, afterwards UpdateType::kIncremental will be triggered
89/// each `update-interval` (adjusted by jitter).
90///
91/// ### Avoiding memory leaks
92///
93/// If you don't implement the deletion of objects that are deleted from the data source and don't use full updates,
94/// you may get an effective memory leak, because garbage objects will pile up in the cached data.
95///
96/// Calculation example:
97/// * size of database: 1000 objects
98/// * removal rate: 30 objects per minute (0.5 objects per second)
99///
100/// 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:
101///
102/// full-update-interval = (size-of-database * 20% / removal-rate) = 400s
103///
104/// ### Dealing with nullptr data in CachingComponentBase
105///
106/// The cache can become `nullptr` through multiple ways:
107///
108/// * If the first cache update fails, and `first-update-fail-ok` config
109/// option is set to `true` (otherwise the service shutdown at start)
110/// * Through manually calling @ref Set with `nullptr` in @ref Update
111/// * If `failed-updates-before-expiration` is set, and that many periodic
112/// updates fail in a row
113///
114/// By default, the cache's user can expect that the pointer returned
115/// from @ref Get will never be `nullptr`. If the cache for some reason is
116/// in `nullptr` state, then @ref Get will throw. This is the safe default
117/// behavior for most cases.
118///
119/// If all systems of a service are expected to work with a cache in `nullptr`
120/// state, then such a cache should override `MayReturnNull` to return `true`.
121/// It will also serve self-documentation purposes: if a cache defines
122/// @ref MayReturnNull, then pointers returned from @ref Get should be checked
123/// for `nullptr` before usage.
124///
125/// ### `first-update-mode` modes
126///
127/// Further customizes the behavior of @ref dump::Dumper "cache dumps".
128///
129/// Mode | Description
130/// ------------- | -----------
131/// `skip` | after successful load from dump, do nothing
132/// `required` | make a synchronous update of type `first-update-type`, stop the service on failure
133/// `best-effort` | make a synchronous update of type `first-update-type`, keep working and use data from dump on failure
134///
135/// ### testsuite-force-periodic-update
136/// use it to enable periodic cache update for a component in testsuite environment
137/// where testsuite-periodic-update-enabled from TestsuiteSupport config is false
138///
139/// By default, update types are guessed based on update intervals presence.
140/// If both `update-interval` and `full-update-interval` are present,
141/// `full-and-incremental` types is assumed. Otherwise `only-full` is used.
142///
143/// @see `dump::Dumper` for more info on persistent cache dumps and
144/// corresponding config options.
145///
146/// @see @ref scripts/docs/en/userver/caches.md. pytest_userver.client.Client.invalidate_caches()
147/// for a function to force cache update from testsuite.
148
149// clang-format on
150
151template <typename T>
152// NOLINTNEXTLINE(fuchsia-multiple-inheritance)
154public:
155 CachingComponentBase(const ComponentConfig& config, const ComponentContext&);
156 ~CachingComponentBase() override;
157
158 using cache::CacheUpdateTrait::Name;
159
160 using cache::CacheUpdateTrait::InvalidateAsync;
161
162 using DataType = T;
163
164 /// @return cache contents. May be `nullptr` if and only if @ref MayReturnNull
165 /// returns `true`.
166 /// @throws cache::EmptyCacheError if the contents are `nullptr`, and
167 /// @ref MayReturnNull returns `false` (which is the default behavior).
168 utils::SharedReadablePtr<T> Get() const;
169
170 /// @return cache contents. May be nullptr regardless of MayReturnNull().
171 utils::SharedReadablePtr<T> GetUnsafe() const;
172
173 /// Subscribes to cache updates using a member function. Also immediately
174 /// invokes the function with the current cache contents.
175 template <class Class>
176 concurrent::AsyncEventSubscriberScope
177 UpdateAndListen(Class* obj, std::string name, void (Class::*func)(const std::shared_ptr<const T>&));
178
179 concurrent::AsyncEventChannel<const std::shared_ptr<const T>&>& GetEventChannel();
180
181 static yaml_config::Schema GetStaticConfigSchema();
182
183protected:
184 /// Sets the new value of cache. As a result the Get() member function starts
185 /// returning the value passed into this function after the Update() finishes.
186 ///
187 /// @warning Do not forget to update cache::UpdateStatisticsScope, otherwise
188 /// the behavior is undefined.
189 void Set(std::unique_ptr<const T> value_ptr);
190
191 /// @overload
192 void Set(T&& value);
193
194 /// @overload Set()
195 template <typename... Args>
196 void Emplace(Args&&... args);
197
198 /// Clears the content of the cache by string a default constructed T.
199 void Clear();
200
201 /// Whether @ref Get is expected to return `nullptr`.
202 virtual bool MayReturnNull() const;
203
204 /// @{
205 /// Override to use custom serialization for cache dumps
206 virtual void WriteContents(dump::Writer& writer, const T& contents) const;
207
208 virtual std::unique_ptr<const T> ReadContents(dump::Reader& reader) const;
209 /// @}
210
211 /// @brief If the option has-pre-assign-check is set true in static config,
212 /// this function is called before assigning the new value to the cache
213 /// @note old_value_ptr and new_value_ptr can be nullptr.
214 virtual void PreAssignCheck(const T* old_value_ptr, const T* new_value_ptr) const;
215
216private:
217 void OnAllComponentsLoaded() final;
218
219 void Cleanup() final;
220
221 void MarkAsExpired() final;
222
223 void GetAndWrite(dump::Writer& writer) const final;
224 void ReadAndSet(dump::Reader& reader) final;
225
226 std::shared_ptr<const T> TransformNewValue(std::unique_ptr<const T> new_value);
227
228 rcu::Variable<std::shared_ptr<const T>> cache_;
229 concurrent::AsyncEventChannel<const std::shared_ptr<const T>&> event_channel_;
230 utils::impl::WaitTokenStorage wait_token_storage_;
231};
232
233template <typename T>
234CachingComponentBase<T>::CachingComponentBase(const ComponentConfig& config, const ComponentContext& context)
235 : ComponentBase(config, context),
236 cache::CacheUpdateTrait(config, context),
237 event_channel_(components::GetCurrentComponentName(config), [this](auto& function) {
238 const auto ptr = cache_.ReadCopy();
239 if (ptr) function(ptr);
240 }) {
241 const auto initial_config = GetConfig();
242}
243
244template <typename T>
245CachingComponentBase<T>::~CachingComponentBase() {
246 // Avoid a deadlock in WaitForAllTokens
247 cache_.Assign(nullptr);
248 // We must wait for destruction of all instances of T to finish, otherwise
249 // it's UB if T's destructor accesses dependent components
250 wait_token_storage_.WaitForAllTokens();
251}
252
253template <typename T>
254utils::SharedReadablePtr<T> CachingComponentBase<T>::Get() const {
255 auto ptr = GetUnsafe();
256 if (!ptr && !MayReturnNull()) {
257 throw cache::EmptyCacheError(Name());
258 }
259 return ptr;
260}
261
262template <typename T>
263template <typename Class>
264concurrent::AsyncEventSubscriberScope CachingComponentBase<T>::UpdateAndListen(
265 Class* obj,
266 std::string name,
267 void (Class::*func)(const std::shared_ptr<const T>&)
268) {
269 return event_channel_.DoUpdateAndListen(obj, std::move(name), func, [&] {
270 auto ptr = Get(); // TODO: extra ref
271 (obj->*func)(ptr);
272 });
273}
274
275template <typename T>
276concurrent::AsyncEventChannel<const std::shared_ptr<const T>&>& CachingComponentBase<T>::GetEventChannel() {
277 return event_channel_;
278}
279
280template <typename T>
281utils::SharedReadablePtr<T> CachingComponentBase<T>::GetUnsafe() const {
282 return utils::SharedReadablePtr<T>(cache_.ReadCopy());
283}
284
285template <typename T>
286void CachingComponentBase<T>::Set(std::unique_ptr<const T> value_ptr) {
287 const std::shared_ptr<const T> new_value = TransformNewValue(std::move(value_ptr));
288
289 if (HasPreAssignCheck()) {
290 auto old_value = cache_.Read();
291 PreAssignCheck(old_value->get(), new_value.get());
292 }
293
294 cache_.Assign(new_value);
295 event_channel_.SendEvent(new_value);
297}
298
299template <typename T>
300void CachingComponentBase<T>::Set(T&& value) {
301 Emplace(std::move(value));
302}
303
304template <typename T>
305template <typename... Args>
306void CachingComponentBase<T>::Emplace(Args&&... args) {
307 Set(std::make_unique<T>(std::forward<Args>(args)...));
308}
309
310template <typename T>
312 cache_.Assign(std::make_unique<const T>());
313}
314
315template <typename T>
317 return false;
318}
319
320template <typename T>
321void CachingComponentBase<T>::GetAndWrite(dump::Writer& writer) const {
322 const auto contents = GetUnsafe();
323 if (!contents) throw cache::EmptyCacheError(Name());
324 WriteContents(writer, *contents);
325}
326
327template <typename T>
328void CachingComponentBase<T>::ReadAndSet(dump::Reader& reader) {
329 auto data = ReadContents(reader);
330 if constexpr (meta::kIsSizable<T>) {
331 if (data) {
332 SetDataSizeStatistic(std::size(*data));
333 }
334 }
335 Set(std::move(data));
336}
337
338template <typename T>
339void CachingComponentBase<T>::WriteContents(dump::Writer& writer, const T& contents) const {
340 if constexpr (dump::kIsDumpable<T>) {
341 writer.Write(contents);
342 } else {
343 dump::ThrowDumpUnimplemented(Name());
344 }
345}
346
347template <typename T>
348std::unique_ptr<const T> CachingComponentBase<T>::ReadContents(dump::Reader& reader) const {
349 if constexpr (dump::kIsDumpable<T>) {
350 // To avoid an extra move and avoid including common_containers.hpp
351 return std::unique_ptr<const T>{new T(reader.Read<T>())};
352 } else {
353 dump::ThrowDumpUnimplemented(Name());
354 }
355}
356
357template <typename T>
358void CachingComponentBase<T>::OnAllComponentsLoaded() {
359 AssertPeriodicUpdateStarted();
360}
361
362template <typename T>
363void CachingComponentBase<T>::Cleanup() {
364 cache_.Cleanup();
365}
366
367template <typename T>
368void CachingComponentBase<T>::MarkAsExpired() {
369 Set(std::unique_ptr<const T>{});
370}
371
372namespace impl {
373
374yaml_config::Schema GetCachingComponentBaseSchema();
375
376template <typename T, typename Deleter>
377auto MakeAsyncDeleter(engine::TaskProcessor& task_processor, Deleter deleter) {
378 return [&task_processor, deleter = std::move(deleter)](const T* raw_ptr) mutable {
379 std::unique_ptr<const T, Deleter> ptr(raw_ptr, std::move(deleter));
380
381 engine::CriticalAsyncNoSpan(task_processor, [ptr = std::move(ptr)]() mutable {}).Detach();
382 };
383}
384
385} // namespace impl
386
387template <typename T>
388yaml_config::Schema CachingComponentBase<T>::GetStaticConfigSchema() {
389 return impl::GetCachingComponentBaseSchema();
390}
391
392template <typename T>
393void CachingComponentBase<T>::PreAssignCheck(const T*, [[maybe_unused]] const T* new_value_ptr) const {
395 meta::kIsSizable<T>,
396 fmt::format(
397 "{} type does not support std::size(), add implementation of "
398 "the method size() for this type or "
399 "override cache::CachingComponentBase::PreAssignCheck.",
400 compiler::GetTypeName<T>()
401 )
402 );
403
404 if constexpr (meta::kIsSizable<T>) {
405 if (!new_value_ptr || std::size(*new_value_ptr) == 0) {
406 throw cache::EmptyDataError(Name());
407 }
408 }
409}
410
411template <typename T>
412std::shared_ptr<const T> CachingComponentBase<T>::TransformNewValue(std::unique_ptr<const T> new_value) {
413 // Kill garbage asynchronously as T::~T() might be very slow
414 if (IsSafeDataLifetime()) {
415 // Use token only if `safe-data-lifetime` is true
416 auto deleter_with_token = [token = wait_token_storage_.GetToken()](const T* raw_ptr) {
417 // Make sure *raw_ptr is deleted before token is destroyed
418 std::default_delete<const T>{}(raw_ptr);
419 };
420 return std::shared_ptr<const T>(
421 new_value.release(), impl::MakeAsyncDeleter<T>(GetCacheTaskProcessor(), std::move(deleter_with_token))
422 );
423 } else {
424 return std::shared_ptr<const T>(
425 new_value.release(), impl::MakeAsyncDeleter<T>(GetCacheTaskProcessor(), std::default_delete<const T>{})
426 );
427 }
428}
429
430} // namespace components
431
432USERVER_NAMESPACE_END