userver: userver/utils/resource_scopes.hpp Source File
Loading...
Searching...
No Matches
resource_scopes.hpp
Go to the documentation of this file.
1#pragma once
2
3/// @file userver/utils/resource_scopes.hpp
4/// @brief @copybrief utils::ResourceScopeStorage
5
6#include <concepts>
7#include <cstdint>
8#include <memory>
9#include <optional>
10#include <utility>
11#include <vector>
12
13#include <userver/compiler/impl/lifetime.hpp>
14#include <userver/components/component_fwd.hpp>
15#include <userver/utils/assert.hpp>
16#include <userver/utils/impl/internal_tag.hpp>
17#include <userver/utils/move_only_function.hpp>
18
19USERVER_NAMESPACE_BEGIN
20
21namespace utils {
22
23namespace impl {
24class ScopeBase {
25public:
26 virtual ~ScopeBase() = default;
27
28 virtual void AfterConstruction() = 0;
29};
30
31template <typename Handle>
32class Scope final : public ScopeBase {
33public:
34 using AfterConstructionCallback = utils::move_only_function<Handle()>;
35
36 explicit Scope(AfterConstructionCallback after_construction)
37 : after_construction_(std::move(after_construction))
38 {}
39
40 void AfterConstruction() override { before_destruction_.emplace(after_construction_()); }
41
42private:
43 AfterConstructionCallback after_construction_;
44 std::optional<Handle> before_destruction_;
45};
46
47template <>
48class Scope<void> final : public ScopeBase {
49public:
50 using AfterConstructionCallback = utils::move_only_function<void()>;
51
52 explicit Scope(AfterConstructionCallback after_construction)
53 : after_construction_(std::move(after_construction))
54 {}
55
56 void AfterConstruction() override { after_construction_(); }
57
58private:
59 AfterConstructionCallback after_construction_;
60};
61
62/// @brief An object of ScopePtr defines actions to do after
63/// a component is constructed and just before it is destroyed.
64///
65/// @see @ref components::ComponentContext::Scopes
66using ScopePtr = std::unique_ptr<impl::ScopeBase>;
67
68} // namespace impl
69
70/// @brief Defers subscription and callback registration until the object is fully constructed.
71///
72/// Components often register external subscriptions (statistics writers, config listeners,
73/// and similar) that capture `this` and run later on another thread. Registering them
74/// directly in the constructor is unsafe: the callback may fire before the constructor
75/// finishes and observe partially initialized fields. Unregistering in the destructor
76/// is equally unsafe if the callback can still run while members are already being
77/// destroyed.
78///
79/// During construction, call @ref Register to queue a functor that performs the actual
80/// registration. The component system calls @ref AfterConstruction when the constructor
81/// (including derived classes) has completed, and @ref BeforeDestruction before the
82/// destructor body runs. That way registration callbacks see a fully built object, and
83/// unregistration runs before members used by the callback are torn down.
84///
85/// The same storage is available from @ref components::ComponentContext::Scopes in
86/// components, or as a standalone helper in unit tests and @ref WithResourceScopes.
87///
88/// @warning Do not store @ref ResourceScopeStorage as a field of the object that
89/// registers subscriptions on it. The storage would share that object's constructor
90/// and destructor, so @ref AfterConstruction cannot run after the object is complete
91/// and @ref BeforeDestruction cannot run before its members are destroyed. Wrap the
92/// object in @ref WithResourceScopes instead.
93///
94/// @snippet core/src/components/resource_scopes_test.cpp ResourceScopeStorage - HappyPathOrder
95class ResourceScopeStorage final {
96public:
97 ResourceScopeStorage() = default;
98
99 ResourceScopeStorage(ResourceScopeStorage&& other) noexcept = default;
100 ResourceScopeStorage& operator=(ResourceScopeStorage&& other) noexcept = default;
101
102 /// @brief Registers a functor to register some resource that will be
103 /// called after the component is successfully created (including all
104 /// class descendants) or after the component creation is emulated in
105 /// unit tests. The functor must return a RAII-style handle object
106 /// that unregisters the previously registered resource. The returned handle's
107 /// destructor is called just before the component destructor is called.
108 ///
109 /// During construction the callback is queued and runs from @ref AfterConstruction.
110 /// If @ref Register is called from another scope's opening callback, or after
111 /// @ref AfterConstruction has completed, the new scope is opened immediately.
112 /// Its destructor runs in reverse opening-completion order.
113 ///
114 /// @note A queued callback is not called if the component is not created OR
115 /// any previously registered callback throws an exception.
116 /// @note if you don't have an existing RAII-ish class, but still want
117 /// to do a cleanup, you might want to use @ref utils::FastScopeGuard
118 /// to wrap the cleanup function.
119 template <std::invocable<> AfterConstructionCallback>
120 void Register(AfterConstructionCallback after_construction)
121 {
122 Register(utils::impl::InternalTag{}, Priority{0}, std::move(after_construction));
123 }
124
125 /// @cond
126 // For internal use only.
127 // Lower values run earlier in AfterConstruction and later in BeforeDestruction.
128 using Priority = std::int32_t;
129
130 template <std::invocable<> AfterConstructionCallback>
131 void Register(utils::impl::InternalTag, Priority priority, AfterConstructionCallback after_construction)
132 {
133 using Handle = std::invoke_result_t<AfterConstructionCallback>;
134 auto scope = std::make_unique<impl::Scope<Handle>>(std::move(after_construction));
135 DoRegister(std::move(scope), priority);
136 }
137 /// @endcond
138
139 /// @brief Call all registered functors.
140 ///
141 /// If a functor throws, already constructed resources are unregistered via
142 /// @ref BeforeDestruction and the exception is rethrown.
144
145 /// @brief Unregister all previously registered resources.
146 ///
147 /// Also drops factories that have not run @ref AfterConstruction yet,
148 /// so captured RAII handles unregister immediately.
149 void BeforeDestruction() noexcept;
150
151private:
152 enum class State {
153 kConstruction,
154 kAfterConstruction,
155 kReady,
156 kBeforeDestruction,
157 kDestruction,
158 };
159
160 struct ScopeWithPriority {
161 Priority priority{0};
162 impl::ScopePtr scope;
163 };
164
165 void DoRegister(impl::ScopePtr resource_scope, Priority priority);
166 void OpenAndKeep(impl::ScopePtr resource_scope);
167 void OpenAndClose(impl::ScopePtr resource_scope);
168 static void SortByPriority(std::vector<ScopeWithPriority>& scopes) noexcept;
169
170 std::vector<ScopeWithPriority> registered_scopes_;
171 std::vector<impl::ScopePtr> initialized_scopes_;
172 State state_{State::kConstruction};
173};
174
175/// @brief A wrapper that provides @ref utils::ResourceScopeStorage for the wrapped object.
176///
177/// The wrapped object is passed `utils::ResourceScopeStorage&` as the first argument to the constructor.
178/// Prefer this over storing @ref ResourceScopeStorage as a field of the wrapped object itself.
179/// Use @ref MakeWithResourceScopes when the caller needs a `std::shared_ptr` to the wrapped object.
180template <typename Wrapped>
181class WithResourceScopes final {
182public:
183 /// @brief Constructs the wrapped object and passes the embedded @ref utils::ResourceScopeStorage to it
184 /// as the first argument.
185 template <typename... Args>
186 explicit WithResourceScopes(std::in_place_t, Args&&... args)
187 : wrapped_(resource_scope_storage_, std::forward<Args>(args)...)
188 {
189 resource_scope_storage_.AfterConstruction();
190 }
191
192 // Not movable: scopes pin a reference to the wrapped object.
193 WithResourceScopes(WithResourceScopes&&) = delete;
194 WithResourceScopes& operator=(WithResourceScopes&&) = delete;
195
196 ~WithResourceScopes() { resource_scope_storage_.BeforeDestruction(); }
197
198 /// @brief Returns the wrapped object.
199 Wrapped& operator*() & noexcept USERVER_IMPL_LIFETIME_BOUND { return wrapped_; }
200 /// @overload
201 const Wrapped& operator*() const& noexcept USERVER_IMPL_LIFETIME_BOUND { return wrapped_; }
202
203 /// @brief Returns the wrapped object.
204 Wrapped* operator->() noexcept USERVER_IMPL_LIFETIME_BOUND { return &wrapped_; }
205 /// @overload
206 const Wrapped* operator->() const noexcept USERVER_IMPL_LIFETIME_BOUND { return &wrapped_; }
207
208private:
209 ResourceScopeStorage resource_scope_storage_;
210 Wrapped wrapped_;
211};
212
213/// @brief Constructs @ref WithResourceScopes and returns an aliasing `std::shared_ptr` to the wrapped object.
214///
215/// The returned pointer shares ownership of the wrapper. @ref WithResourceScopes is not movable
216/// and stays at a stable heap address, so scoped registrations remain valid for the lifetime
217/// of any copy of the `shared_ptr`.
218///
219/// @snippet core/src/components/resource_scopes_test.cpp MakeWithResourceScopes
220template <typename Wrapped, typename... Args>
221std::shared_ptr<Wrapped> MakeWithResourceScopes(Args&&... args) {
222 auto holder = std::make_shared<WithResourceScopes<Wrapped>>(std::in_place, std::forward<Args>(args)...);
223 auto* const wrapped = std::addressof(**holder);
224 return std::shared_ptr<Wrapped>(std::move(holder), wrapped);
225}
226
227ResourceScopeStorage&
228LocateDependency(components::WithType<ResourceScopeStorage>, const components::ComponentConfig& config, const components::ComponentContext&);
229
230} // namespace utils
231
232USERVER_NAMESPACE_END