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