Skip to content

Commit 3079217

Browse files
Fix merge_each error propagation (#2175)
* Fix merge_each error propagation * Guard merge_each outer error test without exceptions * Record merge_each outer error in regression test
1 parent 2aaae5c commit 3079217

2 files changed

Lines changed: 142 additions & 30 deletions

File tree

include/exec/sequence/merge_each.hpp

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -383,16 +383,12 @@ namespace experimental::execution
383383
}
384384
void set_break() noexcept
385385
{
386-
switch (__completion_.exchange(__completion_t::__stopped))
386+
auto __expected = __completion_t::__started;
387+
if (__completion_.compare_exchange_strong(__expected, __completion_t::__stopped))
387388
{
388-
case __completion_t::__started:
389-
// We must request stop. When the previous state is __error or __stopped, then stop has
390-
// already been requested.
389+
// We transitioned from started to stopped, so we must request stop. When the state is
390+
// already error or stopped, stop has already been requested.
391391
__nested_stop_.request_stop();
392-
break;
393-
case __completion_t::__stopped:
394-
[[fallthrough]]; // We're already in the "stopped" state. Ignore the break.
395-
case __completion_t::__error:; // We're already in the "error" state. Ignore the break.
396392
}
397393
}
398394

@@ -455,8 +451,9 @@ namespace experimental::execution
455451
}
456452
void error_complete() noexcept override
457453
{
458-
// do not double report error
459-
STDEXEC::set_stopped(static_cast<_Receiver&&>(__rcvr_));
454+
// The error has been delivered as an item. Complete the sequence so the consumer can
455+
// publish it, unless the operation was independently stopped.
456+
exec::__set_value_unless_stopped(static_cast<_Receiver&&>(__rcvr_));
460457
}
461458

462459
void complete_if_none_active() noexcept

test/exec/sequence/test_merge_each.cpp

Lines changed: 135 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
#include <test_common/type_helpers.hpp>
3434

3535
#include <array>
36+
#include <exception>
37+
#include <optional>
3638

3739
using namespace std::chrono_literals;
3840
using namespace exec;
@@ -227,6 +229,91 @@ namespace
227229
auto op = subscribe(std::move(merged), null_receiver{});
228230
}
229231

232+
template <class Error>
233+
struct error_sequence
234+
{
235+
using sender_concept = sequence_sender_tag;
236+
using item_types = exec::item_types<>;
237+
using completion_signatures = ex::completion_signatures<ex::set_error_t(Error)>;
238+
239+
template <ex::receiver Receiver>
240+
struct operation
241+
{
242+
void start() & noexcept
243+
{
244+
ex::set_error(static_cast<Receiver&&>(receiver_), static_cast<Error&&>(error_));
245+
}
246+
247+
Receiver receiver_;
248+
Error error_;
249+
};
250+
251+
template <ex::receiver Receiver>
252+
auto subscribe(Receiver receiver) && noexcept -> operation<Receiver>
253+
{
254+
return {static_cast<Receiver&&>(receiver), static_cast<Error&&>(error_)};
255+
}
256+
257+
Error error_;
258+
};
259+
260+
struct error_state
261+
{
262+
std::optional<int> nested_error_{};
263+
std::exception_ptr outer_error_{};
264+
bool completed_ = false;
265+
bool stopped_ = false;
266+
};
267+
268+
struct record_error_receiver
269+
{
270+
using receiver_concept = ex::receiver_tag;
271+
272+
template <ex::sender Item>
273+
auto set_next(Item&& item) &
274+
{
275+
auto record_error = [state = state_]<class Error>(Error&& error) noexcept
276+
{
277+
if constexpr (std::same_as<std::decay_t<Error>, int>)
278+
{
279+
state->nested_error_ = static_cast<Error&&>(error);
280+
}
281+
};
282+
return ex::upon_stopped(ex::upon_error(static_cast<Item&&>(item), record_error),
283+
[]() noexcept {});
284+
}
285+
286+
void set_value() noexcept
287+
{
288+
state_->completed_ = true;
289+
}
290+
291+
template <class Error>
292+
void set_error(Error&& error) noexcept
293+
{
294+
if constexpr (std::same_as<std::decay_t<Error>, int>)
295+
{
296+
state_->nested_error_ = static_cast<Error&&>(error);
297+
}
298+
else if constexpr (std::same_as<std::decay_t<Error>, std::exception_ptr>)
299+
{
300+
state_->outer_error_ = static_cast<Error&&>(error);
301+
}
302+
}
303+
304+
void set_stopped() noexcept
305+
{
306+
state_->stopped_ = true;
307+
}
308+
309+
auto get_env() const noexcept -> ex::env<>
310+
{
311+
return {};
312+
}
313+
314+
error_state* state_;
315+
};
316+
230317
TEST_CASE("merge_each - merge two sequence senders of no elements",
231318
"[sequence_senders][merge_each][empty_sequence]")
232319
{
@@ -368,31 +455,59 @@ namespace
368455
CHECK(v.has_value() == true);
369456
}
370457

371-
// TODO - fix problem with stopping
372-
#if 0
373-
TEST_CASE(
374-
"merge_each - merge_each sender stops when a nested sequence fails",
375-
"[sequence_senders][static_thread_pool][merge_each][merge][iterate]") {
458+
TEST_CASE("merge_each - preserves errors from nested value senders",
459+
"[sequence_senders][merge_each]")
460+
{
461+
error_state state{};
462+
auto merged = merge_each(ex::just(ex::just_error(42)));
463+
auto op = subscribe(std::move(merged), record_error_receiver{&state});
464+
465+
ex::start(op);
466+
467+
CHECK(state.nested_error_ == 42);
468+
CHECK(state.completed_);
469+
CHECK_FALSE(state.stopped_);
470+
}
471+
472+
TEST_CASE("merge_each - preserves errors from nested sequences", "[sequence_senders][merge_each]")
473+
{
474+
error_state state{};
475+
auto nested_sequence = error_sequence<int>{42};
476+
auto merged = merge_each(ex::just(std::move(nested_sequence)));
477+
auto op = subscribe(std::move(merged), record_error_receiver{&state});
376478

377-
auto sequences = merge(
378-
log_start(range(100, 120), "range 100-120"),
379-
ex::just(emits_error(std::runtime_error{"failed sequence "})),
380-
log_start(range(200, 220), "range 200-220")
381-
);
479+
ex::start(op);
382480

383-
[[maybe_unused]] auto merged = merge_each(std::move(sequences));
481+
CHECK(state.nested_error_ == 42);
482+
CHECK(state.completed_);
483+
CHECK_FALSE(state.stopped_);
484+
}
384485

385-
int count = 0;
486+
#if !STDEXEC_NO_STDCPP_EXCEPTIONS()
487+
TEST_CASE("merge_each - preserves errors from the outer sequence",
488+
"[sequence_senders][merge_each]")
489+
{
490+
error_state state{};
491+
auto merged = merge_each(error_sequence<int>{42});
492+
auto op = subscribe(std::move(merged), record_error_receiver{&state});
386493

387-
auto v = ex::sync_wait(ignore_all_values(merged | then_each([&count](int x){
388-
++count;
389-
UNSCOPED_INFO("item: " << x
390-
<< ", on thread id: " << std::this_thread::get_id());
391-
})));
494+
ex::start(op);
392495

393-
CHECK(count == 20);
394-
CHECK(v.has_value() == false);
496+
CHECK(state.outer_error_ != nullptr);
497+
bool caught = false;
498+
try
499+
{
500+
std::rethrow_exception(state.outer_error_);
501+
}
502+
catch (int error)
503+
{
504+
caught = true;
505+
CHECK(error == 42);
506+
}
507+
CHECK(caught);
508+
CHECK_FALSE(state.completed_);
509+
CHECK_FALSE(state.stopped_);
395510
}
396-
#endif // 0
511+
#endif
397512

398513
} // namespace

0 commit comments

Comments
 (0)