userver
C++ Async Framework
Toggle main menu visibility
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
31
USERVER_NAMESPACE_BEGIN
32
33
namespace
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.
133
template
<
typename
T>
134
// NOLINTNEXTLINE(fuchsia-multiple-inheritance)
135
class
CachingComponentBase
:
public
ComponentBase
,
public
cache
::
DataProvider
<T>,
protected
cache
::
CacheUpdateTrait
{
136
public
:
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
168
protected
:
169
/// @brief Sets the new value of cache. As a result, @ref Get member function starts returning the new value.
170
///
171
/// Notifies subscribers after setting the new value, see @ref UpdateAndListen.
172
/// Should only be called from @ref Update normally.
173
///
174
/// @warning Do not forget to update @ref cache::UpdateStatisticsScope, otherwise 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
210
private
:
211
void
Cleanup()
final
;
212
213
void
MarkAsExpired()
final
;
214
215
void
GetAndWrite(
dump
::
Writer
& writer)
const
final
;
216
void
ReadAndSet(
dump
::
Reader
& reader)
final
;
217
218
std::shared_ptr<
const
T> TransformNewValue(std::unique_ptr<
const
T> new_value);
219
220
rcu
::
Variable
<std::shared_ptr<
const
T>> cache_;
221
concurrent
::
AsyncEventChannel
<
const
std::shared_ptr<
const
T>&> event_channel_;
222
utils
::impl::WaitTokenStorage wait_token_storage_;
223
};
224
225
template
<
typename
T>
226
CachingComponentBase
<T>::CachingComponentBase(
const
ComponentConfig& config,
const
ComponentContext& context)
227
:
ComponentBase
(config, context),
228
cache
::
CacheUpdateTrait
(config, context),
229
event_channel_(
230
components
::
GetCurrentComponentName
(
context
)
,
231
[
this
](
const
auto
& function) {
232
const
auto
ptr = cache_.ReadCopy();
233
if
(ptr) {
234
function(ptr);
235
}
236
}
237
)
238
{}
239
240
template
<
typename
T>
241
CachingComponentBase
<T>::~CachingComponentBase() {
242
// Avoid a deadlock in WaitForAllTokens
243
cache_.Assign(
nullptr
);
244
// We must wait for destruction of all instances of T to finish, otherwise
245
// it's UB if T's destructor accesses dependent components
246
wait_token_storage_.WaitForAllTokens();
247
}
248
249
template
<
typename
T>
250
utils
::SharedReadablePtr<T>
CachingComponentBase
<T>::
Get
()
const
{
251
auto
ptr =
GetUnsafe
(
)
;
252
if
(!ptr && !
MayReturnNull
(
)
) {
253
throw
cache
::EmptyCacheError(
Name
(
)
);
254
}
255
return
ptr;
256
}
257
258
template
<
typename
T
>
259
template
<
typename
Class>
260
concurrent
::AsyncEventSubscriberScope
CachingComponentBase
<
261
T>::
UpdateAndListen
(Class* obj, std::string name,
void
(Class::*func)(
const
std::shared_ptr<
const
T>&)) {
262
return
event_channel_.DoUpdateAndListen(obj, std::move(name), func, [&] {
263
auto
ptr =
Get
(
)
;
// TODO: extra ref
264
(obj->*func)(ptr);
265
});
266
}
267
268
template
<
typename
T>
269
concurrent
::
AsyncEventChannel
<
const
std::shared_ptr<
const
T>&>&
CachingComponentBase
<T>::GetEventChannel() {
270
return
event_channel_;
271
}
272
273
template
<
typename
T>
274
utils
::SharedReadablePtr<T>
CachingComponentBase
<T>::
GetUnsafe
()
const
{
275
return
utils
::SharedReadablePtr<T>(cache_.ReadCopy());
276
}
277
278
template
<
typename
T>
279
void
CachingComponentBase
<T>::
Set
(std::unique_ptr<
const
T> value_ptr) {
280
Attach
(
TransformNewValue(std::move(value_ptr))
)
;
281
}
282
283
template
<
typename
T>
284
void
CachingComponentBase
<T>::
Set
(T&& value) {
285
Emplace(std::move(value));
286
}
287
288
template
<
typename
T>
289
void
CachingComponentBase
<T>::
Attach
(
const
std::shared_ptr<
const
T>& value_ptr) {
290
if
(HasPreAssignCheck()) {
291
auto
old_value = cache_.Read();
292
PreAssignCheck
(
old_value->get()
,
value_ptr.get()
)
;
293
}
294
295
cache_.Assign(value_ptr);
296
event_channel_.SendEvent(value_ptr);
297
OnCacheModified
(
)
;
298
}
299
300
template
<
typename
T
>
301
template
<
typename
... Args>
302
void
CachingComponentBase
<T>::Emplace(Args&&... args) {
303
Set(std::make_unique<T>(std::forward<Args>(args)...));
304
}
305
306
template
<
typename
T>
307
void
CachingComponentBase
<T>::
Clear
() {
308
cache_.Assign(std::make_unique<
const
T>());
309
}
310
311
template
<
typename
T>
312
bool
CachingComponentBase
<T>::
MayReturnNull
()
const
{
313
return
false
;
314
}
315
316
template
<
typename
T>
317
void
CachingComponentBase
<T>::GetAndWrite(
dump
::
Writer
& writer)
const
{
318
const
auto
contents =
GetUnsafe
(
)
;
319
if
(!contents) {
320
throw
cache
::EmptyCacheError(
Name
(
)
);
321
}
322
WriteContents
(
writer
,
*contents
)
;
323
}
324
325
template
<
typename
T>
326
void
CachingComponentBase
<T>::ReadAndSet(
dump
::
Reader
& reader) {
327
auto
data = ReadContents(reader);
328
if
constexpr
(meta::
kIsSizable
<T>) {
329
if
(data) {
330
SetDataSizeStatistic(std::size(*data));
331
}
332
}
333
Set(std::move(data));
334
}
335
336
template
<
typename
T>
337
void
CachingComponentBase
<T>::
WriteContents
(
dump
::
Writer
& writer,
const
T& contents)
const
{
338
if
constexpr
(
dump
::
kIsDumpable
<T>) {
339
writer.Write(contents);
340
}
else
{
341
dump
::ThrowDumpUnimplemented(
Name
(
)
);
342
}
343
}
344
345
template
<
typename
T>
346
std::unique_ptr<
const
T>
CachingComponentBase
<T>::ReadContents(
dump
::
Reader
& reader)
const
{
347
if
constexpr
(
dump
::
kIsDumpable
<T>) {
348
// To avoid an extra move and avoid including common_containers.hpp
349
return
std::unique_ptr<
const
T>{
new
T(reader.Read<T>())};
350
}
else
{
351
dump
::ThrowDumpUnimplemented(
Name
(
)
);
352
}
353
}
354
355
template
<
typename
T>
356
void
CachingComponentBase
<T>::Cleanup() {
357
cache_.Cleanup();
358
}
359
360
template
<
typename
T>
361
void
CachingComponentBase
<T>::MarkAsExpired() {
362
Set(std::unique_ptr<
const
T>{});
363
}
364
365
namespace
impl {
366
367
yaml_config::Schema GetCachingComponentBaseSchema();
368
369
template
<
typename
T,
typename
Deleter>
370
auto
MakeAsyncDeleter(engine::TaskProcessor& task_processor, Deleter deleter) {
371
return
[&task_processor, deleter = std::move(deleter)](
const
T* raw_ptr)
mutable
{
372
std::unique_ptr<
const
T, Deleter> ptr(raw_ptr, std::move(deleter));
373
374
engine
::
DetachUnscopedUnsafe
(
engine::CriticalAsyncNoTracing(task_processor, [ptr = std::move(ptr)]()
mutable
{})
375
)
;
376
};
377
}
378
379
}
// namespace impl
380
381
template
<
typename
T>
382
yaml_config::Schema
CachingComponentBase
<T>::GetStaticConfigSchema() {
383
return
impl::GetCachingComponentBaseSchema();
384
}
385
386
template
<
typename
T>
387
void
CachingComponentBase
<T>::
PreAssignCheck
(
const
T*, [[
maybe_unused
]]
const
T* new_value_ptr)
const
{
388
UINVARIANT
(
389
meta::
kIsSizable
<T>,
390
fmt::format(
391
"{} type does not support std::size(), add implementation of "
392
"the method size() for this type or "
393
"override cache::CachingComponentBase::PreAssignCheck."
,
394
compiler
::GetTypeName<T>()
395
)
396
);
397
398
if
constexpr
(meta::
kIsSizable
<T>) {
399
if
(!new_value_ptr || std::size(*new_value_ptr) == 0) {
400
throw
cache
::EmptyDataError(
Name
(
)
);
401
}
402
}
403
}
404
405
template
<
typename
T>
406
std::shared_ptr<
const
T>
CachingComponentBase
<T>::TransformNewValue(std::unique_ptr<
const
T> new_value) {
407
// Kill garbage asynchronously as T::~T() might be very slow
408
if
(IsSafeDataLifetime()) {
409
// Use token only if `safe-data-lifetime` is true
410
auto
deleter_with_token = [token = wait_token_storage_.GetToken()](
const
T* raw_ptr) {
411
// Make sure *raw_ptr is deleted before token is destroyed
412
std::default_delete<
const
T>{}(raw_ptr);
413
};
414
return
std::shared_ptr<
const
T>(
415
new_value.release(),
416
impl::MakeAsyncDeleter<T>(GetCacheTaskProcessor(), std::move(deleter_with_token))
417
);
418
}
else
{
419
return
std::shared_ptr<
const
T>(
420
new_value.release(),
421
impl::MakeAsyncDeleter<T>(GetCacheTaskProcessor(), std::default_delete<
const
T>{})
422
);
423
}
424
}
425
426
}
// namespace components
427
428
USERVER_NAMESPACE_END
userver
cache
caching_component_base.hpp
Generated on
for userver by
Doxygen
1.17.0