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/data_provider.hpp>
14#include <userver/cache/exceptions.hpp>
15#include <userver/compiler/demangle.hpp>
16#include <userver/components/component_base.hpp>
17#include <userver/components/component_fwd.hpp>
18#include <userver/concurrent/async_event_channel.hpp>
19#include <userver/dump/helpers.hpp>
20#include <userver/dump/meta.hpp>
21#include <userver/dump/operations.hpp>
22#include <userver/engine/async.hpp>
23#include <userver/rcu/rcu.hpp>
24#include <userver/utils/assert.hpp>
25#include <userver/utils/impl/wait_token_storage.hpp>
26#include <userver/utils/meta.hpp>
27#include <userver/utils/shared_readable_ptr.hpp>
28#include <userver/yaml_config/schema.hpp>
29
30USERVER_NAMESPACE_BEGIN
31
32namespace components {
33
34/// @ingroup userver_components userver_base_classes
35///
36/// @brief Base class for caching components
37///
38/// Provides facilities for creating periodically updated caches.
39/// You need to override cache::CacheUpdateTrait::Update
40/// then call cache::CacheUpdateTrait::StartPeriodicUpdates after setup
41/// and cache::CacheUpdateTrait::StopPeriodicUpdates before teardown.
42/// You can also override cache::CachingComponentBase::PreAssignCheck and set
43/// has-pre-assign-check: true in the static config to enable check.
44///
45/// Caching components must be configured in service config (see options below)
46/// and may be reconfigured dynamically via components::DynamicConfig.
47///
48/// @ref scripts/docs/en/userver/caches.md provide a more detailed introduction.
49///
50/// ## CachingComponentBase Dynamic config
51/// * @ref USERVER_CACHES
52/// * @ref USERVER_DUMPS
53///
54/// ## Static options of components::CachingComponentBase :
55/// @include{doc} scripts/docs/en/components_schema/core/src/cache/caching_component_base.md
56///
57/// Options inherited from @ref components::ComponentBase :
58/// @include{doc} scripts/docs/en/components_schema/core/src/components/impl/component_base.md
59///
60/// ### Update types
61/// * `full-and-incremental`: both `update-interval` and `full-update-interval`
62/// must be specified. Updates with UpdateType::kIncremental will be triggered
63/// each `update-interval` (adjusted by jitter) unless `full-update-interval`
64/// has passed and UpdateType::kFull is triggered.
65/// * `only-full`: only `update-interval` must be specified. UpdateType::kFull
66/// will be triggered each `update-interval` (adjusted by jitter).
67/// * `only-incremental`: only `update-interval` must be specified. UpdateType::kFull is triggered
68/// on the first update, afterwards UpdateType::kIncremental will be triggered
69/// each `update-interval` (adjusted by jitter). Warning: use carefully.
70/// If the cache loses any data, it is lost until service restart (in the worst case). If possible,
71/// use `full-and-incremental` with rare full updates, and completely avoid `only-incremental`.
72/// Also you have to explicitly remove outdated items from the cache container, otherwise
73/// the cache might grow indefinitely and eventually will lead to OOM.
74/// If not sure, just use `full-and-incremental`.
75///
76/// ### Avoiding memory leaks
77///
78/// If you don't implement the deletion of objects that are deleted from the data source and don't use full updates,
79/// you may get an effective memory leak, because garbage objects will pile up in the cached data.
80///
81/// Calculation example:
82/// * size of database: 1000 objects
83/// * removal rate: 30 objects per minute (0.5 objects per second)
84///
85/// Let's say we allow 20% extra garbage objects in cache in addition to the actual objects from the database. In this
86/// case we need:
87///
88/// full-update-interval = (size-of-database * 20% / removal-rate) = 400s
89///
90/// ### Dealing with nullptr data in CachingComponentBase
91///
92/// The cache can become `nullptr` through multiple ways:
93///
94/// * If the first cache update fails, and `first-update-fail-ok` config
95/// option is set to `true` (otherwise the service shutdown at start)
96/// * Through manually calling @ref Set with `nullptr` in @ref Update
97/// * If `failed-updates-before-expiration` is set, and that many periodic
98/// updates fail in a row
99///
100/// By default, the cache's user can expect that the pointer returned
101/// from @ref Get will never be `nullptr`. If the cache for some reason is
102/// in `nullptr` state, then @ref Get will throw. This is the safe default
103/// behavior for most cases.
104///
105/// If all systems of a service are expected to work with a cache in `nullptr`
106/// state, then such a cache should override `MayReturnNull` to return `true`.
107/// It will also serve self-documentation purposes: if a cache defines
108/// @ref MayReturnNull, then pointers returned from @ref Get should be checked
109/// for `nullptr` before usage.
110///
111/// ### `first-update-mode` modes
112///
113/// Further customizes the behavior of @ref dump::Dumper "cache dumps".
114///
115/// Mode | Description
116/// ----------- | -----------
117/// skip | after successful load from dump, do nothing
118/// required | make a synchronous update of type `first-update-type`, stop the service on failure
119/// best-effort | make a synchronous update of type `first-update-type`, keep working and use data from dump on failure
120///
121/// ### testsuite-force-periodic-update
122/// use it to enable periodic cache update for a component in testsuite environment
123/// where testsuite-periodic-update-enabled from TestsuiteSupport config is false
124///
125/// By default, update types are guessed based on update intervals presence.
126/// If both `update-interval` and `full-update-interval` are present,
127/// `full-and-incremental` types is assumed. Otherwise `only-full` is used.
128///
129/// @see `dump::Dumper` for more info on persistent cache dumps and
130/// corresponding config options.
131///
132/// @see @ref scripts/docs/en/userver/caches.md. pytest_userver.client.Client.invalidate_caches()
133/// for a function to force cache update from testsuite.
134template <typename T>
135// NOLINTNEXTLINE(fuchsia-multiple-inheritance)
137public:
138 CachingComponentBase(const ComponentConfig& config, const ComponentContext&);
139 ~CachingComponentBase() override;
140
141 using cache::CacheUpdateTrait::Name;
142
143 using cache::CacheUpdateTrait::InvalidateAsync;
144
145 using DataType = T;
146
147 /// @return cache contents. May be `nullptr` if and only if `MayReturnNull`
148 /// returns `true`.
149 /// @throws cache::EmptyCacheError if the contents are `nullptr`, and
150 /// `MayReturnNull` returns `false` (which is the default behavior).
151 utils::SharedReadablePtr<T> Get() const final;
152
153 /// @return cache contents. May be nullptr regardless of `MayReturnNull`.
154 utils::SharedReadablePtr<T> GetUnsafe() const;
155
156 /// Subscribes to cache updates using a member function. Also immediately
157 /// invokes the function with the current cache contents.
158 template <class Class>
159 concurrent::AsyncEventSubscriberScope UpdateAndListen(
160 Class* obj,
161 std::string name,
162 void (Class::*func)(const std::shared_ptr<const T>&)
163 );
164
165 concurrent::AsyncEventChannel<const std::shared_ptr<const T>&>& GetEventChannel();
166
167 static yaml_config::Schema GetStaticConfigSchema();
168
169protected:
170 /// Sets the new value of cache. As a result the `Get()` member function starts
171 /// returning the value passed into this function after the `Update()` finishes.
172 ///
173 /// @warning Do not forget to update @ref cache::UpdateStatisticsScope, otherwise
174 /// the behavior is undefined.
175 void Set(std::unique_ptr<const T> value_ptr);
176
177 /// @overload
178 void Set(T&& value);
179
180 /// Attach the value of cache. As a result the `Get()` member function starts returning the value passed into
181 /// this function after the `Update()` finishes. Does not take over into sole ownership. Do not use unless
182 /// absolutely necessary. The object must be strictly thread-safe.
183 ///
184 /// @warning Do not forget to update @ref cache::UpdateStatisticsScope, otherwise
185 /// the behavior is undefined.
186 void Attach(const std::shared_ptr<const T>& value_ptr);
187
188 /// @overload Set()
189 template <typename... Args>
190 void Emplace(Args&&... args);
191
192 /// Clears the content of the cache by string a default constructed T.
193 void Clear();
194
195 /// Whether @ref Get is expected to return `nullptr`.
196 virtual bool MayReturnNull() const;
197
198 /// @{
199 /// Override to use custom serialization for cache dumps
200 virtual void WriteContents(dump::Writer& writer, const T& contents) const;
201
202 virtual std::unique_ptr<const T> ReadContents(dump::Reader& reader) const;
203 /// @}
204
205 /// @brief If the option has-pre-assign-check is set true in static config,
206 /// this function is called before assigning the new value to the cache
207 /// @note old_value_ptr and new_value_ptr can be nullptr.
208 virtual void PreAssignCheck(const T* old_value_ptr, const T* new_value_ptr) const;
209
210private:
211 void OnAllComponentsLoaded() final;
212
213 void Cleanup() final;
214
215 void MarkAsExpired() final;
216
217 void GetAndWrite(dump::Writer& writer) const final;
218 void ReadAndSet(dump::Reader& reader) final;
219
220 std::shared_ptr<const T> TransformNewValue(std::unique_ptr<const T> new_value);
221
222 rcu::Variable<std::shared_ptr<const T>> cache_;
223 concurrent::AsyncEventChannel<const std::shared_ptr<const T>&> event_channel_;
224 utils::impl::WaitTokenStorage wait_token_storage_;
225};
226
227template <typename T>
228CachingComponentBase<T>::CachingComponentBase(const ComponentConfig& config, const ComponentContext& context)
229 : ComponentBase(config, context),
230 cache::CacheUpdateTrait(config, context),
231 event_channel_(
233 [this](const auto& function) {
234 const auto ptr = cache_.ReadCopy();
235 if (ptr) {
236 function(ptr);
237 }
238 }
239 )
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<
265 T>::UpdateAndListen(Class* obj, std::string name, void (Class::*func)(const std::shared_ptr<const T>&)) {
266 return event_channel_.DoUpdateAndListen(obj, std::move(name), func, [&] {
267 auto ptr = Get(); // TODO: extra ref
268 (obj->*func)(ptr);
269 });
270}
271
272template <typename T>
273concurrent::AsyncEventChannel<const std::shared_ptr<const T>&>& CachingComponentBase<T>::GetEventChannel() {
274 return event_channel_;
275}
276
277template <typename T>
278utils::SharedReadablePtr<T> CachingComponentBase<T>::GetUnsafe() const {
279 return utils::SharedReadablePtr<T>(cache_.ReadCopy());
280}
281
282template <typename T>
283void CachingComponentBase<T>::Set(std::unique_ptr<const T> value_ptr) {
284 Attach(TransformNewValue(std::move(value_ptr)));
285}
286
287template <typename T>
288void CachingComponentBase<T>::Set(T&& value) {
289 Emplace(std::move(value));
290}
291
292template <typename T>
293void CachingComponentBase<T>::Attach(const std::shared_ptr<const T>& new_value) {
294 if (HasPreAssignCheck()) {
295 auto old_value = cache_.Read();
296 PreAssignCheck(old_value->get(), new_value.get());
297 }
298
299 cache_.Assign(new_value);
300 event_channel_.SendEvent(new_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) {
324 throw cache::EmptyCacheError(Name());
325 }
326 WriteContents(writer, *contents);
327}
328
329template <typename T>
330void CachingComponentBase<T>::ReadAndSet(dump::Reader& reader) {
331 auto data = ReadContents(reader);
332 if constexpr (meta::kIsSizable<T>) {
333 if (data) {
334 SetDataSizeStatistic(std::size(*data));
335 }
336 }
337 Set(std::move(data));
338}
339
340template <typename T>
341void CachingComponentBase<T>::WriteContents(dump::Writer& writer, const T& contents) const {
342 if constexpr (dump::kIsDumpable<T>) {
343 writer.Write(contents);
344 } else {
345 dump::ThrowDumpUnimplemented(Name());
346 }
347}
348
349template <typename T>
350std::unique_ptr<const T> CachingComponentBase<T>::ReadContents(dump::Reader& reader) const {
351 if constexpr (dump::kIsDumpable<T>) {
352 // To avoid an extra move and avoid including common_containers.hpp
353 return std::unique_ptr<const T>{new T(reader.Read<T>())};
354 } else {
355 dump::ThrowDumpUnimplemented(Name());
356 }
357}
358
359template <typename T>
360void CachingComponentBase<T>::OnAllComponentsLoaded() {
361 AssertPeriodicUpdateStarted();
362}
363
364template <typename T>
365void CachingComponentBase<T>::Cleanup() {
366 cache_.Cleanup();
367}
368
369template <typename T>
370void CachingComponentBase<T>::MarkAsExpired() {
371 Set(std::unique_ptr<const T>{});
372}
373
374namespace impl {
375
376yaml_config::Schema GetCachingComponentBaseSchema();
377
378template <typename T, typename Deleter>
379auto MakeAsyncDeleter(engine::TaskProcessor& task_processor, Deleter deleter) {
380 return [&task_processor, deleter = std::move(deleter)](const T* raw_ptr) mutable {
381 std::unique_ptr<const T, Deleter> ptr(raw_ptr, std::move(deleter));
382
383 engine::DetachUnscopedUnsafe(engine::CriticalAsyncNoSpan(task_processor, [ptr = std::move(ptr)]() mutable {}));
384 };
385}
386
387} // namespace impl
388
389template <typename T>
390yaml_config::Schema CachingComponentBase<T>::GetStaticConfigSchema() {
391 return impl::GetCachingComponentBaseSchema();
392}
393
394template <typename T>
395void CachingComponentBase<T>::PreAssignCheck(const T*, [[maybe_unused]] const T* new_value_ptr) const {
397 meta::kIsSizable<T>,
398 fmt::format(
399 "{} type does not support std::size(), add implementation of "
400 "the method size() for this type or "
401 "override cache::CachingComponentBase::PreAssignCheck.",
402 compiler::GetTypeName<T>()
403 )
404 );
405
406 if constexpr (meta::kIsSizable<T>) {
407 if (!new_value_ptr || std::size(*new_value_ptr) == 0) {
408 throw cache::EmptyDataError(Name());
409 }
410 }
411}
412
413template <typename T>
414std::shared_ptr<const T> CachingComponentBase<T>::TransformNewValue(std::unique_ptr<const T> new_value) {
415 // Kill garbage asynchronously as T::~T() might be very slow
416 if (IsSafeDataLifetime()) {
417 // Use token only if `safe-data-lifetime` is true
418 auto deleter_with_token = [token = wait_token_storage_.GetToken()](const T* raw_ptr) {
419 // Make sure *raw_ptr is deleted before token is destroyed
420 std::default_delete<const T>{}(raw_ptr);
421 };
422 return std::shared_ptr<const T>(
423 new_value.release(),
424 impl::MakeAsyncDeleter<T>(GetCacheTaskProcessor(), std::move(deleter_with_token))
425 );
426 } else {
427 return std::shared_ptr<const T>(
428 new_value.release(),
429 impl::MakeAsyncDeleter<T>(GetCacheTaskProcessor(), std::default_delete<const T>{})
430 );
431 }
432}
433
434} // namespace components
435
436USERVER_NAMESPACE_END