userver: userver/concurrent/async_event_channel.hpp Source File
Loading...
Searching...
No Matches
async_event_channel.hpp
Go to the documentation of this file.
1#pragma once
2
3/// @file userver/concurrent/async_event_channel.hpp
4/// @brief @copybrief concurrent::AsyncEventChannel
5
6#include <functional>
7#include <memory>
8#include <string>
9#include <string_view>
10#include <typeindex>
11#include <unordered_map>
12#include <utility>
13#include <vector>
14
15#include <userver/concurrent/async_event_source.hpp>
16#include <userver/concurrent/variable.hpp>
17#include <userver/engine/semaphore.hpp>
18#include <userver/engine/shared_mutex.hpp>
19#include <userver/engine/task/cancel.hpp>
20#include <userver/engine/task/task_with_result.hpp>
21#include <userver/utils/assert.hpp>
22#include <userver/utils/async.hpp>
23#include <userver/utils/resource_scopes.hpp>
24
25USERVER_NAMESPACE_BEGIN
26
27namespace concurrent {
28
29namespace impl {
30
31void WaitForTask(std::string_view name, engine::TaskWithResult<void>& task);
32
33[[noreturn]] void ReportAlreadySubscribed(std::string_view channel_name, std::string_view listener_name);
34
35void ReportNotSubscribed(std::string_view channel_name) noexcept;
36
37void ReportErrorWhileUnsubscribing(
38 std::string_view channel_name,
39 std::string_view listener_name,
40 std::string_view error
41) noexcept;
42
43std::string MakeAsyncChannelName(std::string_view base, std::string_view name);
44
45inline constexpr bool kCheckSubscriptionUB = utils::impl::kEnableAssert;
46
47// During `AsyncEventSubscriberScope::Unsubscribe` or automatic teardown
48// (scope destructor / ResourceScopeStorage::BeforeDestruction), all variables
49// used by the callback must still be valid. A common cause of crashes here:
50// the subscription is removed after the captured data is destroyed.
51template <typename Func>
52void CheckDataUsedByCallbackHasNotBeenDestroyedBeforeUnsubscribing(
53 std::function<void(const Func&)>& on_listener_removal,
54 const Func& listener_func,
55 std::string_view channel_name,
56 std::string_view listener_name
57) noexcept {
58 if (!on_listener_removal) {
59 return;
60 }
61 try {
62 on_listener_removal(listener_func);
63 } catch (const std::exception& e) {
64 ReportErrorWhileUnsubscribing(channel_name, listener_name, e.what());
65 }
66}
67
68} // namespace impl
69
70/// @ingroup userver_concurrency
71///
72/// AsyncEventChannel is an in-process pub-sub with strict FIFO serialization,
73/// i.e. only after the event was processed a new event may appear for
74/// processing, same listener is never called concurrently.
75///
76/// Example usage:
77/// @snippet core/src/concurrent/async_event_channel_test.cpp AsyncEventChannel sample
78template <typename... Args>
79class AsyncEventChannel : public AsyncEventSource<Args...> {
80public:
81 using Function = typename AsyncEventSource<Args...>::Function;
82 using OnRemoveCallback = std::function<void(const Function&)>;
83
84 /// @brief The primary constructor
85 /// @param name used for diagnostic purposes and is also accessible with Name
86 explicit AsyncEventChannel(std::string_view name)
87 : name_(name),
88 data_(ListenersData{{}, {}})
89 {}
90
91 /// @brief The constructor with `AsyncEventSubscriberScope` usage checking.
92 ///
93 /// The constructor with a callback that is called on listener removal,
94 /// both on `Unsubscribe` and on automatic teardown. The callback takes a
95 /// reference to `Function` as input. This is useful for checking the lifetime
96 /// of data captured by the listener update function.
97 ///
98 /// @note Works only in debug mode.
99 ///
100 /// @warning Data captured by `on_listener_removal` function must be valid
101 /// until the `AsyncEventChannel` object is completely destroyed.
102 ///
103 /// Example usage:
104 /// @snippet core/src/concurrent/async_event_channel_test.cpp OnListenerRemoval sample
105 ///
106 /// @param name used for diagnostic purposes and is also accessible with Name
107 /// @param on_listener_removal the callback used for check
108 ///
109 /// @see impl::CheckDataUsedByCallbackHasNotBeenDestroyedBeforeUnsubscribing
110 AsyncEventChannel(std::string_view name, OnRemoveCallback on_listener_removal)
111 : name_(name),
112 data_(ListenersData{{}, std::move(on_listener_removal)})
113 {}
114
115 /// @brief For use in `UpdateAndListen` of specific event channels
116 ///
117 /// Atomically calls `updater`, which should invoke `func` with the previously
118 /// sent event, and subscribes to new events as if using AddListener.
119 ///
120 /// @param id the subscriber class instance, see also a simpler `DoUpdateAndListen` overload below
121 /// @param name the name of the subscriber
122 /// @param func the callback that is called on each update
123 /// @param updater the initial `() -> void` callback that should call `func` with the current value
124 ///
125 /// @see AsyncEventSource::AddListener
126 template <typename UpdaterFunc>
127 AsyncEventSubscriberScope DoUpdateAndListen(
128 FunctionId id,
129 std::string_view name,
130 Function&& func,
131 UpdaterFunc&& updater
132 ) {
133 const std::shared_lock lock(event_mutex_);
134 std::forward<UpdaterFunc>(updater)();
135 return DoAddListener(id, name, std::move(func));
136 }
137
138 /// @overload
139 template <typename Class, typename UpdaterFunc>
140 AsyncEventSubscriberScope DoUpdateAndListen(
141 Class* obj,
142 std::string_view name,
143 void (Class::*func)(Args...),
144 UpdaterFunc&& updater
145 ) {
146 return DoUpdateAndListen(
147 FunctionId(obj),
148 name,
149 [obj, func](Args... args) { (obj->*func)(args...); },
150 std::forward<UpdaterFunc>(updater)
151 );
152 }
153
154 /// @brief Like @ref DoUpdateAndListen, but binds the subscription to @a scopes.
155 ///
156 /// Synchronously calls @a updater and subscribes with a stub that only records whether an event arrived during
157 /// construction. When the scope is entered, @a updater is called again if an event was skipped, and the stub is
158 /// replaced with @a func. Unsubscribe runs in @ref utils::ResourceScopeStorage::BeforeDestruction.
159 ///
160 /// @warning @a updater is not only invoked inline. It is stored and may run again after this call returns,
161 /// when the scope is entered. Do not capture locals by reference: copy the pointers and values the updater
162 /// needs (for example @c obj and @c func) and capture @c this explicitly.
163 template <typename UpdaterFunc>
165 utils::ResourceScopeStorage& scopes,
166 FunctionId id,
167 std::string_view name,
168 Function&& func,
169 UpdaterFunc&& updater
170 ) {
171 auto state = std::make_unique<ResourceScopeState>();
172
173 {
174 const std::shared_lock lock(event_mutex_);
175 updater();
176 state->scope = DoAddListener(id, name, [&state_ref = *state](Args...) { state_ref.event_skipped = true; });
177 auto data = data_.Lock();
178 state->listener = data->listeners.at(id);
179 }
180
181 // The first callback runs synchronously in the constructor, so the subscription
182 // lifetime must be longer than that of other scope types.
183 constexpr utils::ResourceScopeStorage::Priority kPriority{-1};
184 scopes.Register(
185 utils::impl::InternalTag{},
186 kPriority,
187 [state = std::move(state), func = std::move(func), updater = std::forward<UpdaterFunc>(updater)]() mutable {
188 const std::shared_lock sema_lock(state->listener->sema);
189 // listener callback cannot run in parallel here.
190 if (state->event_skipped) {
191 updater();
192 }
193 state->listener->callback = std::move(func);
194 return std::move(state->scope);
195 }
196 );
197 }
198
199 /// @overload
200 template <typename Class, typename UpdaterFunc>
202 utils::ResourceScopeStorage& scopes,
203 Class* obj,
204 std::string_view name,
205 void (Class::*func)(Args...),
206 UpdaterFunc&& updater
207 ) {
208 DoUpdateAndListenScoped(
209 scopes,
210 FunctionId(obj),
211 name,
212 [obj, func](Args... args) { (obj->*func)(args...); },
213 std::forward<UpdaterFunc>(updater)
214 );
215 }
216
217 /// Send the next event and wait until all the listeners process it.
218 ///
219 /// Strict FIFO serialization is guaranteed, i.e. only after this event is
220 /// processed a new event may be delivered for the subscribers, same
221 /// listener/subscriber is never called concurrently.
222 void SendEvent(Args... args) const {
223 struct Task {
224 std::shared_ptr<const Listener> listener;
225 engine::TaskWithResult<void> task;
226 };
227 std::vector<Task> tasks;
228
229 // Try to obtain unique lock for event_mutex_ to serialize
230 // calls to SendEvent()
231 event_mutex_.lock();
232
233 // Now downgrade the lock to shared to allow new subscriptions
234 event_mutex_.unlock_and_lock_shared();
235
236 // And ensure the lock releases in case of an exception
237 std::shared_lock<engine::SharedMutex> tmp_lock{event_mutex_, std::adopt_lock};
238
239 // Now we want to create N subtasks for callbacks,
240 // which must hold event_mutex_'s std::shared_lock.
241 // A naive implementation would create std::shared_lock{event_mutex_} for each subtask,
242 // however, it might deadlock if any parallel SendEvent() is called and is blocked on
243 // event_mutex_.lock(). It happens due to strict prioritization of writers above readers
244 // in SharedMutex: if there is any pending writer, nobody may lock the mutex for read.
245 {
246 auto data = data_.Lock();
247 auto& listeners = data->listeners;
248 tasks.reserve(listeners.size());
249
250 for (const auto& [_, listener] : listeners) {
251 tasks.push_back(Task{
252 listener, // an intentional copy
253 utils::Async(
254 listener->task_name,
255 [&, &callback = listener->callback, sema_lock = std::shared_lock(listener->sema)] {
256 callback(args...);
257 }
258 ),
259 });
260 }
261 }
262 // Unlock data_ here because callbacks may subscribe to this
263
264 for (auto& task : tasks) {
265 impl::WaitForTask(task.listener->name, task.task);
266 }
267 }
268
269 /// @returns the name of this event channel
270 const std::string& Name() const noexcept { return name_; }
271
272private:
273 struct Listener final {
274 // 'sema' with data_.Lock() are used to synchronize removal 'Listener' from ListenersData::listeners
275 mutable engine::Semaphore sema;
276
277 std::string name;
278 mutable Function callback;
279 std::string task_name;
280
281 Listener(std::string name, Function callback, std::string task_name)
282 : sema(1),
283 name(std::move(name)),
284 callback(std::move(callback)),
285 task_name(std::move(task_name))
286 {}
287 };
288
289 struct ResourceScopeState {
290 bool event_skipped{false};
291 std::shared_ptr<const Listener> listener;
292 // Must be the last field: while the subscription is alive, the fields above remain valid for the stub callback.
293 AsyncEventSubscriberScope scope;
294 };
295
296 struct ListenersData final {
297 std::unordered_map<FunctionId, std::shared_ptr<const Listener>, FunctionId::Hash> listeners;
298 OnRemoveCallback on_listener_removal;
299 };
300
301 void RemoveListener(FunctionId id, [[maybe_unused]] UnsubscribingKind kind) noexcept final {
302 const engine::TaskCancellationBlocker blocker;
303 const std::shared_lock lock(event_mutex_);
304 std::shared_ptr<const Listener> listener;
305 OnRemoveCallback on_listener_removal;
306
307 {
308 auto data = data_.Lock();
309 auto& listeners = data->listeners;
310 const auto iter = listeners.find(id);
311
312 if (iter == listeners.end()) {
313 impl::ReportNotSubscribed(Name());
314 return;
315 }
316
317 listener = iter->second;
318
319 on_listener_removal = data->on_listener_removal;
320
321 listeners.erase(iter);
322
323 // Lock and unlock sema under data_.Lock(),
324 // now we're sure that SendEvent() will not trigger listener->callback()
325 (void)std::shared_lock(listener->sema);
326 }
327 // Unlock data_ here to be able to (un)subscribe to *this in listener->callback (in debug)
328 // without deadlock
329
330 if constexpr (impl::kCheckSubscriptionUB) {
331 // Fake listener call to check
332 impl::CheckDataUsedByCallbackHasNotBeenDestroyedBeforeUnsubscribing(
333 on_listener_removal,
334 listener->callback,
335 name_,
336 listener->name
337 );
338 }
339 }
340
341 AsyncEventSubscriberScope DoAddListener(FunctionId id, std::string_view name, Function&& func) final {
342 UASSERT(id);
343
344 auto data = data_.Lock();
345 auto& listeners = data->listeners;
346 auto task_name = impl::MakeAsyncChannelName(name_, name);
347 const auto [iterator, success] = listeners.emplace(
348 id,
349 std::make_shared<const Listener>(std::string{name}, std::move(func), std::move(task_name))
350 );
351 if (!success) {
352 impl::ReportAlreadySubscribed(Name(), name);
353 }
354 return AsyncEventSubscriberScope(utils::impl::InternalTag{}, *this, id);
355 }
356
357 const std::string name_;
358 concurrent::Variable<ListenersData> data_;
359
360 // event_mutex_ is required only for event serialization,
361 // it doesn't protect any data. The mutex is unique locked
362 // for new event publishing, and is shared locked for calling callbacks.
363 // If any callback is working, no new event publishing is possible.
364 // It *is* possible to re-subscribe on async channel while another callback
365 // operates.
366 mutable engine::SharedMutex event_mutex_;
367};
368
369} // namespace concurrent
370
371USERVER_NAMESPACE_END