Skip to content

Commit 10dbb36

Browse files
committed
feat engine: support WaitAny in SharedTask
* Made @ref engine::SharedTask an @ref engine::Awaitable, making it usable in @ref engine::MakeWaitAny and friends. commit_hash:4ba17b631cda4f2abf17ae56228a8537f1b99664
1 parent 9a0d3d8 commit 10dbb36

5 files changed

Lines changed: 313 additions & 78 deletions

File tree

core/include/userver/engine/task/shared_task.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ class [[nodiscard]] SharedTask : public TaskBase {
5151
/// other task into this, leaving the other in an invalid state.
5252
SharedTask& operator=(SharedTask&& other) noexcept;
5353

54+
/// Satisfies @ref engine::Awaitable, for use with @ref engine::WaitAnyContext and friends.
55+
AwaitableToken GetAwaitableToken() noexcept USERVER_IMPL_LIFETIME_BOUND;
56+
5457
/// @cond
5558
static constexpr WaitMode kWaitMode = WaitMode::kMultipleAwaiters;
5659

core/src/engine/task/shared_task.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,14 @@ SharedTask& SharedTask::operator=(SharedTask&& other) noexcept {
4747
return *this;
4848
}
4949

50+
AwaitableToken SharedTask::GetAwaitableToken() noexcept USERVER_IMPL_LIFETIME_BOUND {
51+
if (!IsValid()) {
52+
return {};
53+
}
54+
55+
return AwaitableToken{utils::impl::InternalTag{}, &GetContext()};
56+
}
57+
5058
void SharedTask::DecrementSharedUsages() noexcept {
5159
if (!IsValid()) {
5260
return;

core/src/engine/task/shared_task_with_result_test.cpp

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
#include <userver/utest/utest.hpp>
22

3+
#include <array>
34
#include <type_traits>
45

6+
#include <userver/engine/future.hpp>
57
#include <userver/engine/single_consumer_event.hpp>
68
#include <userver/engine/sleep.hpp>
79
#include <userver/engine/task/shared_task_with_result.hpp>
10+
#include <userver/engine/wait_all_checked.hpp>
11+
#include <userver/engine/wait_any.hpp>
812
#include <userver/utils/async.hpp>
913

1014
USERVER_NAMESPACE_BEGIN
@@ -392,4 +396,123 @@ static_assert(std::is_move_assignable_v<engine::SharedTaskWithResult<void>>);
392396
static_assert(std::is_copy_constructible_v<engine::SharedTaskWithResult<void>>);
393397
static_assert(std::is_copy_assignable_v<engine::SharedTaskWithResult<void>>);
394398

399+
namespace {
400+
401+
constexpr int kFirstSharedTaskValue = 2;
402+
constexpr int kSecondSharedTaskValue = 3;
403+
404+
using IntSharedTask = engine::SharedTaskWithResult<int>;
405+
406+
std::array<int, 2> WaitAnyAndGetResults(IntSharedTask task1, IntSharedTask task2) {
407+
auto wait_any = engine::MakeWaitAny(task1, task2);
408+
std::array<int, 2> values{};
409+
std::array<bool, 2> completed{};
410+
411+
for (std::size_t i = 0; i < completed.size(); ++i) {
412+
const auto index = wait_any.Wait();
413+
EXPECT_TRUE(index.has_value());
414+
if (index != 0 && index != 1) {
415+
ADD_FAILURE() << "Unexpected task index";
416+
return values;
417+
}
418+
EXPECT_FALSE(completed[*index]);
419+
completed[*index] = true;
420+
values[*index] = *index == 0 ? task1.Get() : task2.Get();
421+
}
422+
423+
return values;
424+
}
425+
426+
} // namespace
427+
428+
UTEST(SharedTask, WaitAnyWithMultipleAwaiters) {
429+
engine::Promise<int> promise1;
430+
engine::Promise<int> promise2;
431+
432+
auto future1 = promise1.get_future();
433+
auto future2 = promise2.get_future();
434+
435+
auto task1 = utils::SharedAsync("shared_task_1", [future = std::move(future1)]() mutable { return future.get(); });
436+
auto task2 = utils::SharedAsync("shared_task_2", [future = std::move(future2)]() mutable { return future.get(); });
437+
438+
auto sum_waiter = utils::Async(
439+
"sum_waiter",
440+
[](IntSharedTask task1, IntSharedTask task2) {
441+
const auto values = WaitAnyAndGetResults(std::move(task1), std::move(task2));
442+
return values[0] + values[1];
443+
},
444+
task1,
445+
task2
446+
);
447+
448+
auto product_waiter = utils::Async(
449+
"product_waiter",
450+
[](IntSharedTask task1, IntSharedTask task2) {
451+
const auto values = WaitAnyAndGetResults(std::move(task1), std::move(task2));
452+
return values[0] * values[1];
453+
},
454+
task1,
455+
task2
456+
);
457+
458+
engine::Yield();
459+
EXPECT_FALSE(task1.IsFinished());
460+
EXPECT_FALSE(task2.IsFinished());
461+
462+
promise1.set_value(kFirstSharedTaskValue);
463+
promise2.set_value(kSecondSharedTaskValue);
464+
465+
const auto sum = sum_waiter.Get();
466+
const auto product = product_waiter.Get();
467+
468+
EXPECT_EQ(sum, kFirstSharedTaskValue + kSecondSharedTaskValue);
469+
EXPECT_EQ(product, kFirstSharedTaskValue * kSecondSharedTaskValue);
470+
EXPECT_NE(sum, product);
471+
}
472+
473+
UTEST(SharedTask, WaitAllCheckedWithMultipleAwaiters) {
474+
engine::Promise<int> promise1;
475+
engine::Promise<int> promise2;
476+
477+
auto future1 = promise1.get_future();
478+
auto future2 = promise2.get_future();
479+
480+
auto task1 = utils::SharedAsync("shared_task_1", [future = std::move(future1)]() mutable { return future.get(); });
481+
auto task2 = utils::SharedAsync("shared_task_2", [future = std::move(future2)]() mutable { return future.get(); });
482+
483+
auto sum_waiter = utils::Async(
484+
"sum_waiter",
485+
[](IntSharedTask task1, IntSharedTask task2) {
486+
engine::WaitAllChecked(task1, task2);
487+
return task1.Get() + task2.Get();
488+
},
489+
task1,
490+
task2
491+
);
492+
493+
auto product_waiter = utils::Async(
494+
"product_waiter",
495+
[](IntSharedTask task1, IntSharedTask task2) {
496+
engine::WaitAllChecked(task1, task2);
497+
return task1.Get() * task2.Get();
498+
},
499+
task1,
500+
task2
501+
);
502+
503+
engine::Yield();
504+
EXPECT_FALSE(task1.IsFinished());
505+
EXPECT_FALSE(task2.IsFinished());
506+
507+
promise1.set_value(kFirstSharedTaskValue);
508+
promise2.set_value(kSecondSharedTaskValue);
509+
510+
const auto sum = sum_waiter.Get();
511+
const auto product = product_waiter.Get();
512+
513+
EXPECT_EQ(sum, kFirstSharedTaskValue + kSecondSharedTaskValue);
514+
EXPECT_EQ(product, kFirstSharedTaskValue * kSecondSharedTaskValue);
515+
EXPECT_NE(sum, product);
516+
}
517+
395518
USERVER_NAMESPACE_END

core/src/engine/wait_all_checked.cpp

Lines changed: 93 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
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

4749
private:
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

82123
WaitAllCheckedContext::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

86130
FutureStatus 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

120179
void 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

160221
FutureStatus DoWaitAllChecked(std::vector<AwaitableToken>&& targets, Deadline deadline) {

0 commit comments

Comments
 (0)