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_context.hpp>
18#include <userver/components/component_fwd.hpp>
19#include <userver/concurrent/async_event_channel.hpp>
20#include <userver/dump/helpers.hpp>
21#include <userver/dump/meta.hpp>
22#include <userver/dump/operations.hpp>
23#include <userver/engine/async.hpp>
24#include <userver/rcu/rcu.hpp>
25#include <userver/utils/assert.hpp>
26#include <userver/utils/impl/wait_token_storage.hpp>
27#include <userver/utils/meta.hpp>
28#include <userver/utils/resource_scopes.hpp>
29#include <userver/utils/shared_readable_ptr.hpp>
30#include <userver/yaml_config/schema.hpp>
31
32USERVER_NAMESPACE_BEGIN
33
34namespace components {
35
36/// @ingroup userver_components userver_base_classes
37///
38/// @brief Base class for caching components
39///
40/// Provides facilities for creating periodically updated caches.
41/// You need to override cache::CacheUpdateTrait::Update.
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 ///
159 /// Further updates are delivered after @ref utils::ResourceScopeStorage::AfterConstruction, including those updates
160 /// that arrived before it. Unsubscribe runs in @ref utils::ResourceScopeStorage::BeforeDestruction.
161 ///
162 /// @param scopes storage that owns the subscription lifetime. In a component constructor pass `context.Scopes()`
163 /// or @ref components::GetResourceScopes.
164 template <class Class>
166 utils::ResourceScopeStorage& scopes,
167 Class* obj,
168 std::string name,
169 void (Class::*func)(const std::shared_ptr<const T>&)
170 );
171
172 /// @overload
173 /// @deprecated Use the overload that takes @ref utils::ResourceScopeStorage.
174 ///
175 /// Store the returned scope as a member and call `Unsubscribe` explicitly.
176 template <class Class>
177 concurrent::AsyncEventSubscriberScope UpdateAndListen(
178 Class* obj,
179 std::string name,
180 void (Class::*func)(const std::shared_ptr<const T>&)
181 );
182
183 concurrent::AsyncEventChannel<const std::shared_ptr<const T>&>& GetEventChannel();
184
185 static yaml_config::Schema GetStaticConfigSchema();
186
187protected:
188 /// @brief Sets the new value of cache. As a result, @ref Get member function starts returning the new value.
189 ///
190 /// Notifies subscribers after setting the new value, see @ref UpdateAndListen.
191 /// Should only be called from @ref Update normally.
192 ///
193 /// @warning Do not forget to update @ref cache::UpdateStatisticsScope, otherwise the behavior is undefined.
194 void Set(std::unique_ptr<const T> value_ptr);
195
196 /// @overload
197 void Set(T&& value);
198
199 /// Attach the value of cache. As a result the `Get()` member function starts returning the value passed into
200 /// this function after the `Update()` finishes. Does not take over into sole ownership. Do not use unless
201 /// absolutely necessary. The object must be strictly thread-safe.
202 ///
203 /// @warning Do not forget to update @ref cache::UpdateStatisticsScope, otherwise
204 /// the behavior is undefined.
205 void Attach(const std::shared_ptr<const T>& value_ptr);
206
207 /// @overload Set()
208 template <typename... Args>
209 void Emplace(Args&&... args);
210
211 /// Clears the content of the cache by string a default constructed T.
212 void Clear();
213
214 /// Whether @ref Get is expected to return `nullptr`.
215 virtual bool MayReturnNull() const;
216
217 /// @{
218 /// Override to use custom serialization for cache dumps
219 virtual void WriteContents(dump::Writer& writer, const T& contents) const;
220
221 virtual std::unique_ptr<const T> ReadContents(dump::Reader& reader) const;
222 /// @}
223
224 /// @brief If the option has-pre-assign-check is set true in static config,
225 /// this function is called before assigning the new value to the cache
226 /// @note old_value_ptr and new_value_ptr can be nullptr.
227 virtual void PreAssignCheck(const T* old_value_ptr, const T* new_value_ptr) const;
228
229private:
230 void Cleanup() final;
231
232 void MarkAsExpired() final;
233
234 void GetAndWrite(dump::Writer& writer) const final;
235 void ReadAndSet(dump::Reader& reader) final;
236
237 std::shared_ptr<const T> TransformNewValue(std::unique_ptr<const T> new_value);
238
239 rcu::Variable<std::shared_ptr<const T>> cache_;
240 concurrent::AsyncEventChannel<const std::shared_ptr<const T>&> event_channel_;
241 utils::impl::WaitTokenStorage wait_token_storage_;
242};
243
244template <typename T>
245CachingComponentBase<T>::CachingComponentBase(const ComponentConfig& config, const ComponentContext& context)
246 : ComponentBase(config, context),
247 cache::CacheUpdateTrait(config, context),
248 event_channel_(
250 [this](const auto& function) {
251 const auto ptr = cache_.ReadCopy();
252 if (ptr) {
253 function(ptr);
254 }
255 }
256 )
257{}
258
259template <typename T>
260CachingComponentBase<T>::~CachingComponentBase() {
261 // Avoid a deadlock in WaitForAllTokens
262 cache_.Assign(nullptr);
263 // We must wait for destruction of all instances of T to finish, otherwise
264 // it's UB if T's destructor accesses dependent components
265 wait_token_storage_.WaitForAllTokens();
266}
267
268template <typename T>
269utils::SharedReadablePtr<T> CachingComponentBase<T>::Get() const {
270 auto ptr = GetUnsafe();
271 if (!ptr && !MayReturnNull()) {
272 throw cache::EmptyCacheError(Name());
273 }
274 return ptr;
275}
276
277template <typename T>
278template <typename Class>
280 utils::ResourceScopeStorage& scopes,
281 Class* obj,
282 std::string name,
283 void (Class::*func)(const std::shared_ptr<const T>&)
284) {
285 event_channel_.DoUpdateAndListenScoped(scopes, obj, std::move(name), func, [this, obj, func] {
286 auto ptr = Get(); // TODO: extra ref
287 (obj->*func)(ptr);
288 });
289}
290
291template <typename T>
292template <typename Class>
293concurrent::AsyncEventSubscriberScope CachingComponentBase<
294 T>::UpdateAndListen(Class* obj, std::string name, void (Class::*func)(const std::shared_ptr<const T>&)) {
295 return event_channel_.DoUpdateAndListen(obj, std::move(name), func, [&] {
296 auto ptr = Get(); // TODO: extra ref
297 (obj->*func)(ptr);
298 });
299}
300
301template <typename T>
302concurrent::AsyncEventChannel<const std::shared_ptr<const T>&>& CachingComponentBase<T>::GetEventChannel() {
303 return event_channel_;
304}
305
306template <typename T>
307utils::SharedReadablePtr<T> CachingComponentBase<T>::GetUnsafe() const {
308 return utils::SharedReadablePtr<T>(cache_.ReadCopy());
309}
310
311template <typename T>
312void CachingComponentBase<T>::Set(std::unique_ptr<const T> value_ptr) {
313 Attach(TransformNewValue(std::move(value_ptr)));
314}
315
316template <typename T>
317void CachingComponentBase<T>::Set(T&& value) {
318 Emplace(std::move(value));
319}
320
321template <typename T>
322void CachingComponentBase<T>::Attach(const std::shared_ptr<const T>& value_ptr) {
323 if (HasPreAssignCheck()) {
324 auto old_value = cache_.Read();
325 PreAssignCheck(old_value->get(), value_ptr.get());
326 }
327
328 cache_.Assign(value_ptr);
329 event_channel_.SendEvent(value_ptr);
331}
332
333template <typename T>
334template <typename... Args>
335void CachingComponentBase<T>::Emplace(Args&&... args) {
336 Set(std::make_unique<T>(std::forward<Args>(args)...));
337}
338
339template <typename T>
341 cache_.Assign(std::make_unique<const T>());
342}
343
344template <typename T>
346 return false;
347}
348
349template <typename T>
350void CachingComponentBase<T>::GetAndWrite(dump::Writer& writer) const {
351 const auto contents = GetUnsafe();
352 if (!contents) {
353 throw cache::EmptyCacheError(Name());
354 }
355 WriteContents(writer, *contents);
356}
357
358template <typename T>
359void CachingComponentBase<T>::ReadAndSet(dump::Reader& reader) {
360 auto data = ReadContents(reader);
361 if constexpr (meta::kIsSizable<T>) {
362 if (data) {
363 SetDataSizeStatistic(std::size(*data));
364 }
365 }
366 Set(std::move(data));
367}
368
369template <typename T>
370void CachingComponentBase<T>::WriteContents(dump::Writer& writer, const T& contents) const {
371 if constexpr (dump::kIsDumpable<T>) {
372 writer.Write(contents);
373 } else {
374 dump::ThrowDumpUnimplemented(Name());
375 }
376}
377
378template <typename T>
379std::unique_ptr<const T> CachingComponentBase<T>::ReadContents(dump::Reader& reader) const {
380 if constexpr (dump::kIsDumpable<T>) {
381 // To avoid an extra move and avoid including common_containers.hpp
382 return std::unique_ptr<const T>{new T(reader.Read<T>())};
383 } else {
384 dump::ThrowDumpUnimplemented(Name());
385 }
386}
387
388template <typename T>
389void CachingComponentBase<T>::Cleanup() {
390 cache_.Cleanup();
391}
392
393template <typename T>
394void CachingComponentBase<T>::MarkAsExpired() {
395 Set(std::unique_ptr<const T>{});
396}
397
398namespace impl {
399
400yaml_config::Schema GetCachingComponentBaseSchema();
401
402template <typename T, typename Deleter>
403auto MakeAsyncDeleter(engine::TaskProcessor& task_processor, Deleter deleter) {
404 return [&task_processor, deleter = std::move(deleter)](const T* raw_ptr) mutable {
405 std::unique_ptr<const T, Deleter> ptr(raw_ptr, std::move(deleter));
406
407 engine::DetachUnscopedUnsafe(engine::CriticalAsyncNoTracing(task_processor, [ptr = std::move(ptr)]() mutable {})
408 );
409 };
410}
411
412} // namespace impl
413
414template <typename T>
415yaml_config::Schema CachingComponentBase<T>::GetStaticConfigSchema() {
416 return impl::GetCachingComponentBaseSchema();
417}
418
419template <typename T>
420void CachingComponentBase<T>::PreAssignCheck(const T*, [[maybe_unused]] const T* new_value_ptr) const {
422 meta::kIsSizable<T>,
423 fmt::format(
424 "{} type does not support std::size(), add implementation of "
425 "the method size() for this type or "
426 "override cache::CachingComponentBase::PreAssignCheck.",
427 compiler::GetTypeName<T>()
428 )
429 );
430
431 if constexpr (meta::kIsSizable<T>) {
432 if (!new_value_ptr || std::size(*new_value_ptr) == 0) {
433 throw cache::EmptyDataError(Name());
434 }
435 }
436}
437
438template <typename T>
439std::shared_ptr<const T> CachingComponentBase<T>::TransformNewValue(std::unique_ptr<const T> new_value) {
440 // Kill garbage asynchronously as T::~T() might be very slow
441 if (IsSafeDataLifetime()) {
442 // Use token only if `safe-data-lifetime` is true
443 auto deleter_with_token = [token = wait_token_storage_.GetToken()](const T* raw_ptr) {
444 // Make sure *raw_ptr is deleted before token is destroyed
445 std::default_delete<const T>{}(raw_ptr);
446 };
447 return std::shared_ptr<const T>(
448 new_value.release(),
449 impl::MakeAsyncDeleter<T>(GetCacheTaskProcessor(), std::move(deleter_with_token))
450 );
451 } else {
452 return std::shared_ptr<const T>(
453 new_value.release(),
454 impl::MakeAsyncDeleter<T>(GetCacheTaskProcessor(), std::default_delete<const T>{})
455 );
456 }
457}
458
459} // namespace components
460
461USERVER_NAMESPACE_END