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)
154 protected cache::CacheUpdateTrait {
155 public:
156 CachingComponentBase(const ComponentConfig& config, const ComponentContext&);
157 ~CachingComponentBase() override;
158
159 using cache::CacheUpdateTrait::Name;
160
161 using DataType = T;
162
163 /// @return cache contents. May be `nullptr` if and only if @ref MayReturnNull
164 /// returns `true`.
165 /// @throws cache::EmptyCacheError if the contents are `nullptr`, and
166 /// @ref MayReturnNull returns `false` (which is the default behavior).
167 utils::SharedReadablePtr<T> Get() const;
168
169 /// @return cache contents. May be nullptr regardless of MayReturnNull().
170 utils::SharedReadablePtr<T> GetUnsafe() const;
171
172 /// Subscribes to cache updates using a member function. Also immediately
173 /// invokes the function with the current cache contents.
174 template <class Class>
175 concurrent::AsyncEventSubscriberScope UpdateAndListen(
176 Class* obj, std::string name,
177 void (Class::*func)(const std::shared_ptr<const T>&));
178
179 concurrent::AsyncEventChannel<const std::shared_ptr<const T>&>&
180 GetEventChannel();
181
182 static yaml_config::Schema GetStaticConfigSchema();
183
184 protected:
185 /// Sets the new value of cache. As a result the Get() member function starts
186 /// returning the value passed into this function after the Update() finishes.
187 ///
188 /// @warning Do not forget to update cache::UpdateStatisticsScope, otherwise
189 /// the behavior is undefined.
190 void Set(std::unique_ptr<const T> value_ptr);
191
192 /// @overload
193 void Set(T&& value);
194
195 /// @overload Set()
196 template <typename... Args>
197 void Emplace(Args&&... args);
198
199 /// Clears the content of the cache by string a default constructed T.
200 void Clear();
201
202 /// Whether @ref Get is expected to return `nullptr`.
203 virtual bool MayReturnNull() const;
204
205 /// @{
206 /// Override to use custom serialization for cache dumps
207 virtual void WriteContents(dump::Writer& writer, const T& contents) const;
208
209 virtual std::unique_ptr<const T> ReadContents(dump::Reader& reader) const;
210 /// @}
211
212 /// @brief If the option has-pre-assign-check is set true in static config,
213 /// this function is called before assigning the new value to the cache
214 /// @note old_value_ptr and new_value_ptr can be nullptr.
215 virtual void PreAssignCheck(const T* old_value_ptr,
216 const T* new_value_ptr) const;
217
218 private:
219 void OnAllComponentsLoaded() final;
220
221 void Cleanup() final;
222
223 void MarkAsExpired() final;
224
225 void GetAndWrite(dump::Writer& writer) const final;
226 void ReadAndSet(dump::Reader& reader) final;
227
228 std::shared_ptr<const T> TransformNewValue(
229 std::unique_ptr<const T> new_value);
230
231 rcu::Variable<std::shared_ptr<const T>> cache_;
232 concurrent::AsyncEventChannel<const std::shared_ptr<const T>&> event_channel_;
233 utils::impl::WaitTokenStorage wait_token_storage_;
234};
235
236template <typename T>
237CachingComponentBase<T>::CachingComponentBase(const ComponentConfig& config,
238 const ComponentContext& context)
239 : ComponentBase(config, context),
240 cache::CacheUpdateTrait(config, context),
241 event_channel_(components::GetCurrentComponentName(config),
242 [this](auto& function) {
243 const auto ptr = cache_.ReadCopy();
244 if (ptr) function(ptr);
245 }) {
246 const auto initial_config = GetConfig();
247}
248
249template <typename T>
250CachingComponentBase<T>::~CachingComponentBase() {
251 // Avoid a deadlock in WaitForAllTokens
252 cache_.Assign(nullptr);
253 // We must wait for destruction of all instances of T to finish, otherwise
254 // it's UB if T's destructor accesses dependent components
255 wait_token_storage_.WaitForAllTokens();
256}
257
258template <typename T>
259utils::SharedReadablePtr<T> CachingComponentBase<T>::Get() const {
260 auto ptr = GetUnsafe();
261 if (!ptr && !MayReturnNull()) {
262 throw cache::EmptyCacheError(Name());
263 }
264 return ptr;
265}
266
267template <typename T>
268template <typename Class>
269concurrent::AsyncEventSubscriberScope CachingComponentBase<T>::UpdateAndListen(
270 Class* obj, std::string name,
271 void (Class::*func)(const std::shared_ptr<const T>&)) {
272 return event_channel_.DoUpdateAndListen(obj, std::move(name), func, [&] {
273 auto ptr = Get(); // TODO: extra ref
274 (obj->*func)(ptr);
275 });
276}
277
278template <typename T>
279concurrent::AsyncEventChannel<const std::shared_ptr<const T>&>&
280CachingComponentBase<T>::GetEventChannel() {
281 return event_channel_;
282}
283
284template <typename T>
285utils::SharedReadablePtr<T> CachingComponentBase<T>::GetUnsafe() const {
286 return utils::SharedReadablePtr<T>(cache_.ReadCopy());
287}
288
289template <typename T>
290void CachingComponentBase<T>::Set(std::unique_ptr<const T> value_ptr) {
291 const std::shared_ptr<const T> new_value =
292 TransformNewValue(std::move(value_ptr));
293
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>
305void CachingComponentBase<T>::Set(T&& value) {
306 Emplace(std::move(value));
307}
308
309template <typename T>
310template <typename... Args>
311void CachingComponentBase<T>::Emplace(Args&&... args) {
312 Set(std::make_unique<T>(std::forward<Args>(args)...));
313}
314
315template <typename T>
317 cache_.Assign(std::make_unique<const T>());
318}
319
320template <typename T>
322 return false;
323}
324
325template <typename T>
326void CachingComponentBase<T>::GetAndWrite(dump::Writer& writer) const {
327 const auto contents = GetUnsafe();
328 if (!contents) throw cache::EmptyCacheError(Name());
329 WriteContents(writer, *contents);
330}
331
332template <typename T>
333void CachingComponentBase<T>::ReadAndSet(dump::Reader& reader) {
334 auto data = ReadContents(reader);
335 if constexpr (meta::kIsSizable<T>) {
336 if (data) {
337 SetDataSizeStatistic(std::size(*data));
338 }
339 }
340 Set(std::move(data));
341}
342
343template <typename T>
345 const T& contents) const {
346 if constexpr (dump::kIsDumpable<T>) {
347 writer.Write(contents);
348 } else {
349 dump::ThrowDumpUnimplemented(Name());
350 }
351}
352
353template <typename T>
354std::unique_ptr<const T> CachingComponentBase<T>::ReadContents(
355 dump::Reader& reader) const {
356 if constexpr (dump::kIsDumpable<T>) {
357 // To avoid an extra move and avoid including common_containers.hpp
358 return std::unique_ptr<const T>{new T(reader.Read<T>())};
359 } else {
360 dump::ThrowDumpUnimplemented(Name());
361 }
362}
363
364template <typename T>
365void CachingComponentBase<T>::OnAllComponentsLoaded() {
366 AssertPeriodicUpdateStarted();
367}
368
369template <typename T>
370void CachingComponentBase<T>::Cleanup() {
371 cache_.Cleanup();
372}
373
374template <typename T>
375void CachingComponentBase<T>::MarkAsExpired() {
376 Set(std::unique_ptr<const T>{});
377}
378
379namespace impl {
380
381yaml_config::Schema GetCachingComponentBaseSchema();
382
383template <typename T, typename Deleter>
384auto MakeAsyncDeleter(engine::TaskProcessor& task_processor, Deleter deleter) {
385 return [&task_processor,
386 deleter = std::move(deleter)](const T* raw_ptr) mutable {
387 std::unique_ptr<const T, Deleter> ptr(raw_ptr, std::move(deleter));
388
389 engine::CriticalAsyncNoSpan(task_processor, [ptr =
390 std::move(ptr)]() mutable {
391 }).Detach();
392 };
393}
394
395} // namespace impl
396
397template <typename T>
398yaml_config::Schema CachingComponentBase<T>::GetStaticConfigSchema() {
399 return impl::GetCachingComponentBaseSchema();
400}
401
402template <typename T>
404 const T*, [[maybe_unused]] const T* new_value_ptr) const {
406 meta::kIsSizable<T>,
407 fmt::format("{} type does not support std::size(), add implementation of "
408 "the method size() for this type or "
409 "override cache::CachingComponentBase::PreAssignCheck.",
410 compiler::GetTypeName<T>()));
411
412 if constexpr (meta::kIsSizable<T>) {
413 if (!new_value_ptr || std::size(*new_value_ptr) == 0) {
414 throw cache::EmptyDataError(Name());
415 }
416 }
417}
418
419template <typename T>
420std::shared_ptr<const T> CachingComponentBase<T>::TransformNewValue(
421 std::unique_ptr<const T> new_value) {
422 // Kill garbage asynchronously as T::~T() might be very slow
423 if (IsSafeDataLifetime()) {
424 // Use token only if `safe-data-lifetime` is true
425 auto deleter_with_token =
426 [token = wait_token_storage_.GetToken()](const T* raw_ptr) {
427 // Make sure *raw_ptr is deleted before token is destroyed
428 std::default_delete<const T>{}(raw_ptr);
429 };
430 return std::shared_ptr<const T>(
431 new_value.release(),
432 impl::MakeAsyncDeleter<T>(GetCacheTaskProcessor(),
433 std::move(deleter_with_token)));
434 } else {
435 return std::shared_ptr<const T>(
436 new_value.release(),
437 impl::MakeAsyncDeleter<T>(GetCacheTaskProcessor(),
438 std::default_delete<const T>{}));
439 }
440}
441
442} // namespace components
443
444USERVER_NAMESPACE_END