99#include < userver/engine/exception.hpp>
1010#include < userver/engine/impl/awaiter.hpp>
1111#include < userver/engine/single_consumer_event.hpp>
12+ #include < userver/utils/fast_scope_guard.hpp>
13+ #include < userver/utils/fixed_array.hpp>
1214#include < userver/utils/impl/intrusive_ref_counter_one.hpp>
1315#include < userver/utils/impl/userver_experiments.hpp>
1416
@@ -45,8 +47,11 @@ class WaitAllCheckedContext final {
4547 FutureStatus WaitUntil (Deadline deadline) { return impl_->WaitUntil (deadline); }
4648
4749private:
48- // NOLINTNEXTLINE(fuchsia-multiple-inheritance)
49- class Impl final : public impl::PolymorphicAwaiter, public utils::impl::IntrusiveRefCounterOne<Impl> {
50+ // The refcounter of Impl counts:
51+ // 1. the reference from WaitAllCheckedContext (impl_);
52+ // 2. one reference per subscribed Node. This way, Impl stays alive until all in-flight
53+ // notifications complete, even if WaitAllCheckedContext is destroyed concurrently with them.
54+ class Impl final : public utils::impl::IntrusiveRefCounterOne<Impl> {
5055 public:
5156 explicit Impl (std::vector<AwaitableToken>&& targets);
5257
@@ -61,15 +66,51 @@ class WaitAllCheckedContext final {
6166 void Unsubscribe () noexcept ;
6267
6368 private:
69+ // Unlike Impl itself, a Node is an Awaiter, so it has its own wait list hook, allowing
70+ // multiple Nodes of a single WaitAllCheckedContext to be linked into the WaitList
71+ // of a single shared target (as well as into wait lists of distinct targets).
72+ //
73+ // Nodes are owned by Impl (nodes_). The AwaiterPtr ownership passed to wait lists
74+ // is logical: instead of destroying the Node, its dispose hooks release
75+ // the subscription's reference to Impl.
76+ struct Node final : public impl::PolymorphicAwaiter {
77+ Node (Impl& owner, AwaitableToken target, std::size_t index)
78+ : owner(owner),
79+ target (target),
80+ index(index)
81+ {}
82+
83+ void NotifyAndDispose (std::uintptr_t context) noexcept override {
84+ UASSERT (context == kUnusedContext );
85+
86+ owner.HandleNotification (index);
87+ // Release the subscription's reference to Impl, see above. After this point,
88+ // Impl (including `*this`) may be destroyed (if WaitAllCheckedContext
89+ // is destroyed concurrently).
90+ intrusive_ptr_release (&owner);
91+ }
92+
93+ void DisposeWithoutNotification () noexcept override {
94+ // The Node is removed from a wait list without notification. Release
95+ // the subscription's reference to Impl.
96+ intrusive_ptr_release (&owner);
97+ }
98+
99+ Impl& owner;
100+ AwaitableToken target;
101+ std::size_t index;
102+ bool subscribed{false };
103+ };
104+
105+ // Nodes do not need to pass any information through the context parameter of Awaiter:
106+ // a Node itself identifies the subscription.
107+ static constexpr std::uintptr_t kUnusedContext = 0 ;
64108 static constexpr std::size_t kNoExceptionSource = std::numeric_limits<std::size_t >::max();
65109
66- AwaiterPtr AsAwaiterPtr ( ) noexcept ;
110+ void HandleNotification (std:: size_t index ) noexcept ;
67111
68- void NotifyAndDispose (std::uintptr_t context) noexcept override ;
69-
70- void DisposeWithoutNotification () noexcept override { intrusive_ptr_release (this ); }
71-
72- std::vector<AwaitableToken> targets_;
112+ // One node per target; nodes of empty targets stay unused.
113+ utils::FixedArray<Node> nodes_;
73114 std::size_t next_subscription_index_{0 };
74115 std::atomic<std::size_t > pending_notifications_{0 };
75116 std::atomic<std::size_t > exception_source_index_{kNoExceptionSource };
@@ -80,24 +121,42 @@ class WaitAllCheckedContext final {
80121};
81122
82123WaitAllCheckedContext::Impl::Impl (std::vector<AwaitableToken>&& targets)
83- : targets_(std::move(targets))
124+ : nodes_(utils::GenerateFixedArray(
125+ targets.size(),
126+ [&](std::size_t index) { return Node (*this , targets[index], index); }
127+ ))
84128{}
85129
86130FutureStatus WaitAllCheckedContext::Impl::WaitUntil (Deadline deadline) {
87- while (next_subscription_index_ < targets_.size ()) {
88- const auto target = targets_[next_subscription_index_];
131+ while (next_subscription_index_ < nodes_.size ()) {
132+ const auto index = next_subscription_index_++;
133+ auto & node = nodes_[index];
134+ const auto target = node.target ;
89135 if (target.IsEmpty ()) {
90- ++next_subscription_index_;
91136 continue ;
92137 }
93138 auto & awaitable = target.GetAwaitable (utils::impl::InternalTag{});
94139
95140 pending_notifications_.fetch_add (1 , std::memory_order_relaxed);
96- auto awaiter_ptr = AsAwaiterPtr ();
97- awaitable.TryAppendAwaiter (awaiter_ptr, next_subscription_index_++);
98- if (awaiter_ptr != nullptr ) { // target is already ready.
99- pending_notifications_.fetch_sub (1 , std::memory_order_relaxed);
100- RethrowError (target);
141+ // Acquire the subscription's reference to Impl in advance.
142+ intrusive_ptr_add_ref (this );
143+
144+ AwaiterPtr awaiter_ptr{&node};
145+
146+ {
147+ const utils::FastScopeGuard finalize_subscription ([&]() noexcept {
148+ if (awaiter_ptr != nullptr ) {
149+ // The target is already ready: deliver the missed notification in-place.
150+ // If the target has completed with an exception, it will be rethrown
151+ // after the subscription loop.
152+ UASSERT (awaiter_ptr.get () == &node);
153+ impl::NotifyAndDispose (std::move (awaiter_ptr), kUnusedContext );
154+ } else {
155+ node.subscribed = true ;
156+ }
157+ });
158+
159+ awaitable.TryAppendAwaiter (awaiter_ptr, kUnusedContext );
101160 }
102161 }
103162
@@ -110,7 +169,7 @@ FutureStatus WaitAllCheckedContext::Impl::WaitUntil(Deadline deadline) {
110169 }
111170 auto exception_source_index = exception_source_index_.exchange (kNoExceptionSource , std::memory_order_relaxed);
112171 if (exception_source_index != kNoExceptionSource ) {
113- RethrowError (targets_ [exception_source_index]);
172+ RethrowError (nodes_ [exception_source_index]. target );
114173 utils::AbortWithStacktrace (" Should never be there" );
115174 }
116175 UASSERT (pending_notifications_.load (std::memory_order_relaxed) == 0 );
@@ -119,23 +178,27 @@ FutureStatus WaitAllCheckedContext::Impl::WaitUntil(Deadline deadline) {
119178
120179void WaitAllCheckedContext::Impl::Unsubscribe () noexcept {
121180 for (std::size_t i = 0 ; i < next_subscription_index_; ++i) {
122- if (targets_[i].IsEmpty ()) {
181+ auto & node = nodes_[i];
182+ if (!node.subscribed ) {
123183 continue ;
124184 }
125- auto & awaitable = targets_[i].GetAwaitable (utils::impl::InternalTag{});
126- awaitable.RemoveAwaiter (*this , i);
185+ auto & awaitable = node.target .GetAwaitable (utils::impl::InternalTag{});
186+ auto removed = awaitable.RemoveAwaiter (node, kUnusedContext );
187+ if (removed != nullptr ) {
188+ UASSERT (removed.get () == &node);
189+ }
190+ // Dropping `removed` calls DisposeWithoutNotification, releasing the subscription's
191+ // reference to Impl. This never destroys Impl here, because the caller
192+ // (WaitAllCheckedContext) still holds its own reference. If the node was already
193+ // notified, `removed` is null, and the in-flight NotifyAndDispose releases
194+ // the subscription's reference.
195+ removed.reset ();
196+ node.subscribed = false ;
127197 }
128198}
129199
130- AwaiterPtr WaitAllCheckedContext::Impl::AsAwaiterPtr () noexcept {
131- intrusive_ptr_add_ref (this );
132- return AwaiterPtr{this };
133- }
134-
135- void WaitAllCheckedContext::Impl::NotifyAndDispose (std::uintptr_t context) noexcept {
136- UASSERT (context <= std::numeric_limits<std::size_t >::max ());
137- const auto index = static_cast <std::size_t >(context);
138- const auto target = targets_[index];
200+ void WaitAllCheckedContext::Impl::HandleNotification (std::size_t index) noexcept {
201+ const auto target = nodes_[index].target ;
139202 auto & awaitable = target.GetAwaitable (utils::impl::InternalTag{});
140203 UASSERT (awaitable.IsReady ());
141204 UASSERT (pending_notifications_.load (std::memory_order_relaxed) > 0 );
@@ -153,8 +216,6 @@ void WaitAllCheckedContext::Impl::NotifyAndDispose(std::uintptr_t context) noexc
153216 if (ready) {
154217 result_ready_.Send ();
155218 }
156-
157- intrusive_ptr_release (this );
158219}
159220
160221FutureStatus DoWaitAllChecked (std::vector<AwaitableToken>&& targets, Deadline deadline) {
0 commit comments