userver: userver/cache/base_mongo_cache.hpp Source File
Loading...
Searching...
No Matches
base_mongo_cache.hpp
Go to the documentation of this file.
1#pragma once
2
3/// @file userver/cache/base_mongo_cache.hpp
4/// @brief @copybrief components::MongoCache
5
6#include <chrono>
7
8#include <fmt/format.h>
9
10#include <userver/cache/cache_statistics.hpp>
11#include <userver/cache/caching_component_base.hpp>
12#include <userver/cache/mongo_cache_type_traits.hpp>
13#include <userver/components/component_context.hpp>
14#include <userver/formats/bson/document.hpp>
15#include <userver/formats/bson/inline.hpp>
16#include <userver/formats/bson/value_builder.hpp>
17#include <userver/storages/mongo/collection.hpp>
18#include <userver/storages/mongo/operations.hpp>
19#include <userver/storages/mongo/operators.hpp>
20#include <userver/storages/mongo/options.hpp>
21#include <userver/tracing/span.hpp>
22#include <userver/utils/cpu_relax.hpp>
23#include <userver/yaml_config/merge_schemas.hpp>
24
25USERVER_NAMESPACE_BEGIN
26
27namespace components {
28
29inline const std::string kFetchAndParseStage = "fetch_and_parse";
30
31inline constexpr std::chrono::milliseconds kCpuRelaxThreshold{10};
32inline constexpr std::chrono::milliseconds kCpuRelaxInterval{2};
33
34namespace impl {
35
36std::chrono::milliseconds GetMongoCacheUpdateCorrection(const ComponentConfig&);
37
38template <class MongoCacheTraits>
39storages::mongo::operations::Find MakeDefaultFindOperation(
40 cache::UpdateType type,
41 const std::chrono::system_clock::time_point& last_update,
42 const std::chrono::system_clock::time_point& now,
43 const std::chrono::system_clock::duration& correction
44);
45
46} // namespace impl
47
48/// @ingroup userver_base_classes
49///
50/// @brief Intermediate base of components::MongoCache that declares
51/// components::MongoCache::MakeFindOperation.
52///
53/// The traits define no query, so it can only be built from the runtime
54/// state of a specific cache. The method is pure virtual, which makes the
55/// compiler require an implementation in a derived component.
56template <class MongoCacheTraits>
57class MongoCacheFindOperationBase : public CachingComponentBase<typename MongoCacheTraits::DataType> {
58protected:
59 MongoCacheFindOperationBase(const ComponentConfig& config, const ComponentContext& context)
60 : CachingComponentBase<typename MongoCacheTraits::DataType>(config, context)
61 {}
62
63 virtual storages::mongo::operations::Find MakeFindOperation(
64 cache::UpdateType type,
65 const std::chrono::system_clock::time_point& last_update,
66 const std::chrono::system_clock::time_point& now,
67 const std::chrono::system_clock::duration& correction
68 ) = 0;
69};
70
71/// @ingroup userver_base_classes
72///
73/// @brief The traits define the query (`GetFindOperation` or
74/// `kUseDefaultFindOperation`), so the method has a default implementation.
75template <class MongoCacheTraits>
76requires mongo_cache::impl::HasFindOperationInTraits<MongoCacheTraits>
77class MongoCacheFindOperationBase<MongoCacheTraits> : public CachingComponentBase<typename MongoCacheTraits::DataType> {
78protected:
79 MongoCacheFindOperationBase(const ComponentConfig& config, const ComponentContext& context)
80 : CachingComponentBase<typename MongoCacheTraits::DataType>(config, context)
81 {}
82
83 virtual storages::mongo::operations::Find MakeFindOperation(
84 cache::UpdateType type,
85 const std::chrono::system_clock::time_point& last_update,
86 const std::chrono::system_clock::time_point& now,
87 const std::chrono::system_clock::duration& correction
88 ) {
89 return impl::MakeDefaultFindOperation<MongoCacheTraits>(type, last_update, now, correction);
90 }
91};
92
93/// @ingroup userver_components
94///
95/// @brief %Base class for all caches polling mongo collection
96///
97/// You have to provide a traits class in order to use this.
98///
99/// For avoiding "memory leaks", see the respective section
100/// in @ref components::CachingComponentBase.
101///
102/// ## Static options of components::MongoCache :
103///
104/// @include{doc} scripts/docs/en/components_schema/mongo/src/cache/base_mongo_cache.md
105///
106/// Options inherited from @ref components::CachingComponentBase :
107/// @include{doc} scripts/docs/en/components_schema/core/src/cache/caching_component_base.md
108///
109/// Options inherited from @ref components::ComponentBase :
110/// @include{doc} scripts/docs/en/components_schema/core/src/components/impl/component_base.md
111///
112/// ## Traits example:
113/// All fields below (except for function overrides) are mandatory.
114///
115/// ```
116/// struct MongoCacheTraitsExample {
117/// // Component name for component
118/// static constexpr std::string_view kName = "mongo-dynamic-config";
119///
120/// // Collection to read from
121/// static constexpr auto kMongoCollectionsField =
122/// &storages::mongo::Collections::config;
123/// // Update field name to use for incremental update (optional).
124/// // When missing, incremental update is disabled.
125/// // Please use reference here to avoid global variables
126/// // initialization order issues.
127/// static constexpr const std::string& kMongoUpdateFieldName =
128/// mongo::db::taxi::config::kUpdated;
129///
130/// // Cache element type
131/// using ObjectType = CachedObject;
132/// // Cache element field name that is used as an index in the cache map
133/// static constexpr auto kKeyField = &CachedObject::name;
134/// // Type of kKeyField
135/// using KeyType = std::string;
136/// // Type of cache map, e.g. unordered_map, map, bimap
137/// using DataType = std::unordered_map<KeyType, ObjectType>;
138///
139/// // Whether the cache prefers to read from replica (if true, you might get stale data)
140/// static constexpr bool kIsSecondaryPreferred = true;
141///
142/// // Optional function that overrides BSON to ObjectType conversion
143/// static constexpr auto DeserializeObject = &CachedObject::FromBson;
144/// // or
145/// static ObjectType DeserializeObject(const formats::bson::Document& doc) {
146/// return doc["value"].As<ObjectType>();
147/// }
148/// // (default implementation calls doc.As<ObjectType>())
149/// // For using default implementation
150/// static constexpr bool kUseDefaultDeserializeObject = true;
151///
152/// // Optional function that overrides data retrieval operation
153/// static storages::mongo::operations::Find GetFindOperation(
154/// cache::UpdateType type,
155/// const std::chrono::system_clock::time_point& last_update,
156/// const std::chrono::system_clock::time_point& now,
157/// const std::chrono::system_clock::duration& correction) {
158/// mongo::operations::Find find_op({});
159/// find_op.SetOption(mongo::options::Projection{"key", "value"});
160/// return find_op;
161/// }
162/// // (default implementation queries kMongoUpdateFieldName: {$gt: last_update}
163/// // for incremental updates, and {} for full updates)
164/// // For using default implementation
165/// static constexpr bool kUseDefaultFindOperation = true;
166/// // If neither is specified, the query is built by
167/// // components::MongoCache::MakeFindOperation, see the section below
168///
169/// // Whether update part of the cache even if failed to parse some documents
170/// static constexpr bool kAreInvalidDocumentsSkipped = false;
171///
172/// // Component to get the collections
173/// using MongoCollectionsComponent = components::MongoCollections;
174/// };
175/// ```
176///
177/// ## Building the query at runtime:
178///
179/// A query defined in the traits is fixed at compile time. If the query depends
180/// on anything that is only known at runtime (the dynamic config, other
181/// components, the static config, the state of the cache itself and so on),
182/// then specify no find operation in the traits and override
183/// components::MongoCache::MakeFindOperation in a derived component instead.
184///
185/// Traits with neither `GetFindOperation` nor `kUseDefaultFindOperation` make
186/// `MakeFindOperation` pure virtual, so you must override it in a derived component.
187///
188/// @snippet mongo/functional_tests/cache/src/runtime_query_cache.hpp RuntimeQueryCache traits
189/// @snippet mongo/functional_tests/cache/src/runtime_query_cache.hpp RuntimeQueryCache
190template <class MongoCacheTraits>
191class MongoCache : public MongoCacheFindOperationBase<MongoCacheTraits> {
192 using CollectionsType = mongo_cache::impl::CollectionsType<decltype(MongoCacheTraits::kMongoCollectionsField)>;
193 using FindOperationBase = MongoCacheFindOperationBase<MongoCacheTraits>;
194
195public:
196 static constexpr std::string_view kName = MongoCacheTraits::kName;
197
198 MongoCache(const ComponentConfig&, const ComponentContext&);
199
200 static yaml_config::Schema GetStaticConfigSchema();
201
202private:
203 void Update(
204 cache::UpdateType type,
205 const std::chrono::system_clock::time_point& last_update,
206 const std::chrono::system_clock::time_point& now,
207 cache::UpdateStatisticsScope& stats_scope
208 ) override;
209
210 typename MongoCacheTraits::ObjectType DeserializeObject(const formats::bson::Document& doc) const;
211
212 storages::mongo::operations::Find GetFindOperation(
213 cache::UpdateType type,
214 const std::chrono::system_clock::time_point& last_update,
215 const std::chrono::system_clock::time_point& now,
216 const std::chrono::system_clock::duration& correction
217 );
218
219 std::unique_ptr<typename MongoCacheTraits::DataType> GetData(cache::UpdateType type);
220
221 const std::shared_ptr<CollectionsType> mongo_collections_;
222 const storages::mongo::Collection* const mongo_collection_;
223 const std::chrono::system_clock::duration correction_;
224 std::size_t cpu_relax_iterations_{0};
225};
226
227template <class MongoCacheTraits>
228inline constexpr bool kHasValidate<MongoCache<MongoCacheTraits>> = true;
229
230template <class MongoCacheTraits>
231storages::mongo::operations::Find impl::MakeDefaultFindOperation(
232 cache::UpdateType type,
233 const std::chrono::system_clock::time_point& last_update,
234 const std::chrono::system_clock::time_point& now,
235 const std::chrono::system_clock::duration& correction
236) {
237 namespace bson = formats::bson;
238 namespace sm = storages::mongo;
239
240 if constexpr (mongo_cache::impl::HasFindOperation<MongoCacheTraits>) {
241 return MongoCacheTraits::GetFindOperation(type, last_update, now, correction);
242 } else {
243 bson::ValueBuilder query_builder(bson::ValueBuilder::Type::kObject);
244 if constexpr (mongo_cache::impl::HasUpdateFieldName<MongoCacheTraits>) {
246 query_builder[MongoCacheTraits::kMongoUpdateFieldName] =
247 bson::MakeDoc(storages::mongo::operators::kGt, last_update - correction);
248 }
249 }
250 return sm::operations::Find(query_builder.ExtractValue());
251 }
252}
253
254template <class MongoCacheTraits>
255MongoCache<MongoCacheTraits>::MongoCache(const ComponentConfig& config, const ComponentContext& context)
256 : FindOperationBase(config, context),
257 mongo_collections_(context.FindComponent<typename MongoCacheTraits::MongoCollectionsComponent>()
258 .template GetCollectionForLibrary<CollectionsType>()),
259 mongo_collection_(std::addressof(mongo_collections_.get()->*MongoCacheTraits::kMongoCollectionsField)),
260 correction_(impl::GetMongoCacheUpdateCorrection(config))
261{
262 [[maybe_unused]] mongo_cache::impl::CheckTraits<MongoCacheTraits> check_traits;
263
264 if (CachingComponentBase<typename MongoCacheTraits::DataType>::GetAllowedUpdateTypes() ==
265 cache::AllowedUpdateTypes::kFullAndIncremental &&
266 !mongo_cache::impl::HasUpdateFieldName<MongoCacheTraits> &&
267 !mongo_cache::impl::HasFindOperation<MongoCacheTraits> &&
268 mongo_cache::impl::HasDefaultFindOperation<MongoCacheTraits>)
269 {
270 throw std::logic_error(fmt::format(
271 "Incremental update support is requested in config but no update field "
272 "name is specified in traits of '{}' cache",
274 ));
275 }
276 if (correction_.count() < 0) {
277 throw std::logic_error(fmt::format(
278 "Refusing to set forward (negative) update correction requested in "
279 "config for '{}' cache",
281 ));
282 }
283}
284
285template <class MongoCacheTraits>
286void MongoCache<MongoCacheTraits>::Update(
287 cache::UpdateType type,
288 const std::chrono::system_clock::time_point& last_update,
289 const std::chrono::system_clock::time_point& now,
290 cache::UpdateStatisticsScope& stats_scope
291) {
292 namespace sm = storages::mongo;
293
294 const auto* collection = mongo_collection_;
295 auto find_op = GetFindOperation(type, last_update, now, correction_);
296 auto cursor = collection->Execute(find_op);
297 if (type == cache::UpdateType::kIncremental && !cursor) {
298 // Don't touch the cache at all
299 LOG_INFO() << "No changes in cache " << MongoCacheTraits::kName;
300 stats_scope.FinishNoChanges();
301 return;
302 }
303
304 auto scope = tracing::Span::CurrentSpan().CreateScopeTime("copy_data");
305 auto new_cache = GetData(type);
306
307 // No good way to identify whether cursor accesses DB or reads buffed data
308 scope.Reset(kFetchAndParseStage);
309
310 utils::CpuRelax relax{cpu_relax_iterations_, &scope};
311 std::size_t doc_count = 0;
312
313 for (const auto& doc : cursor) {
314 ++doc_count;
315
316 relax.Relax();
317
319
320 try {
321 auto object = DeserializeObject(doc);
322 auto key = (object.*MongoCacheTraits::kKeyField);
323
324 if (type == cache::UpdateType::kIncremental || new_cache->count(key) == 0) {
325 (*new_cache)[key] = std::move(object);
326 } else {
328 << "Found duplicate key for 2 items in cache " << MongoCacheTraits::kName << ", key=" << key;
329 }
330 } catch (const std::exception& e) {
332 << "Failed to deserialize cache item of cache " << MongoCacheTraits::kName
333 << ", _id=" << doc["_id"].template ConvertTo<std::string>() << ", what(): " << e;
335
336 if (!MongoCacheTraits::kAreInvalidDocumentsSkipped) {
337 throw;
338 }
339 }
340 }
341
342 const auto elapsed_time = scope.ElapsedTotal(kFetchAndParseStage);
343 if (elapsed_time > kCpuRelaxThreshold) {
344 cpu_relax_iterations_ = static_cast<
345 std::size_t>(static_cast<double>(doc_count) / (elapsed_time / kCpuRelaxInterval));
346 LOG_TRACE() << fmt::format(
347 "Elapsed time for updating {} {} for {} data items is over threshold. "
348 "Will relax CPU every {} iterations",
349 kName,
350 elapsed_time.count(),
351 doc_count,
352 cpu_relax_iterations_
353 );
354 }
355
356 scope.Reset();
357
358 const auto size = new_cache->size();
359 this->Set(std::move(new_cache));
360 stats_scope.Finish(size);
361}
362
363template <class MongoCacheTraits>
364typename MongoCacheTraits::ObjectType MongoCache<MongoCacheTraits>::DeserializeObject(const formats::bson::Document& doc
365) const {
366 if constexpr (mongo_cache::impl::HasDeserializeObject<MongoCacheTraits>) {
367 return MongoCacheTraits::DeserializeObject(doc);
368 }
369 if constexpr (mongo_cache::impl::HasDefaultDeserializeObject<MongoCacheTraits>) {
370 return doc.As<typename MongoCacheTraits::ObjectType>();
371 }
372 UASSERT_MSG(false, "No deserialize operation defined but DeserializeObject invoked");
373}
374
375template <class MongoCacheTraits>
376storages::mongo::operations::Find MongoCache<MongoCacheTraits>::GetFindOperation(
377 cache::UpdateType type,
378 const std::chrono::system_clock::time_point& last_update,
379 const std::chrono::system_clock::time_point& now,
380 const std::chrono::system_clock::duration& correction
381) {
382 namespace sm = storages::mongo;
383
384 auto find_op = this->MakeFindOperation(type, last_update, now, correction);
385
386 if (MongoCacheTraits::kIsSecondaryPreferred) {
388 }
389 return find_op;
390}
391
392template <class MongoCacheTraits>
393std::unique_ptr<typename MongoCacheTraits::DataType> MongoCache<MongoCacheTraits>::GetData(cache::UpdateType type) {
395 auto ptr = this->Get();
396 return std::make_unique<typename MongoCacheTraits::DataType>(*ptr);
397 } else {
398 return std::make_unique<typename MongoCacheTraits::DataType>();
399 }
400}
401
402namespace impl {
403
404std::string GetMongoCacheSchema();
405
406} // namespace impl
407
408template <class MongoCacheTraits>
409yaml_config::Schema MongoCache<MongoCacheTraits>::GetStaticConfigSchema() {
410 return yaml_config::MergeSchemas<
411 CachingComponentBase<typename MongoCacheTraits::DataType>>(impl::GetMongoCacheSchema());
412}
413
414} // namespace components
415
416USERVER_NAMESPACE_END