Skip to content

Complete merge_each's next receiver when a nested sequence's sender fails or is stopped - #2447

Open
0z5a wants to merge 1 commit into
NVIDIA:mainfrom
0z5a:fix/merge-each-nested-sequence-sender-completion
Open

0z5a wants to merge 1 commit into
NVIDIA:mainfrom
0z5a:fix/merge-each-nested-sequence-sender-completion

Conversation

@0z5a

@0z5a 0z5a commented Nov 11, 2026

Copy link
Copy Markdown

Fixes #2445

exec::merge_each never completes if an item of the input sequence (a sender of a nested sequence) completes with set_error or set_stopped, or if subscribe on the nested sequence throws. In those three places __receive_nested_sequence called __op_->nested_sequence_break() / __op_->nested_sequence_complete(), which update the state of the merge_each operation but don't complete the input sequence's next receiver. The input sequence then waits for that item forever. For example, this hangs in sync_wait:

ex::sync_wait(exec::merge_each(ex::just(1)
                               | ex::then(
                                 [](int i)
                                 {
                                   if (i == 1)
                                     throw 42;
                                   return ex::just(i);
                                 }))
              | exec::ignore_all_values());

A stop request that arrives between two nested sequences of merge_each(iterate(...) | transform_each(...)) is lost in the same way, because the sender of the next nested sequence is then stopped.

This change completes the next receiver through __next_seq_op_, as __receive_nested_values already does:

  • error (from the sender, or thrown by subscribe): the error is stored, the next receiver gets set_stopped, and the error is delivered as the last item, like the errors of nested sequences (Fix merge_each error propagation #2175);
  • stopped: the next receiver gets set_value, so the stopped sender is skipped and the rest is merged; if stop has been requested, the next receiver gets set_stopped so that the input sequence stops too.

Tests

Added to test/exec/sequence/test_merge_each.cpp; all four fail without the change in merge_each.hpp:

  • merge_each - completes when the sender of a nested sequence fails
  • merge_each - continues when the sender of a nested sequence is stopped
  • merge_each - completes with stopped when stop is requested between nested sequences
  • merge_each - completes when subscribing to a nested sequence throws

Run on x86_64 Linux, Debug, C++20, with the branch based on f71dc7d:

Compiler test.exec "merge_each*" on main + new tests with the fix whole test.exec with the fix
g++ 13.3.0 4 of 14 cases fail 14 pass 514 cases pass
clang 20.1.8 (libstdc++) 4 of 14 cases fail 14 pass 514 cases pass

clang-format 21.1.8 reports no violations on the two changed files. libc++, gcc 12/14/15, MSVC, -fno-exceptions, the sanitizer rows and the modules build were not run locally.

The branch was then rebased onto a8449ab (the two new commits on main don't touch these files) and the test suite was not rebuilt after the rebase. The reproducer from the issue was re-run at a8449ab with Apple clang 21 on arm64 macOS: all three operations never complete on main and complete with this change.

Open point

When the sender is stopped and no stop was requested, this skips it and continues with the next nested sequence. If you would rather end the input sequence in that case, it is a one-line change.

AI disclosure

Claude Code, model claude-fable-5-2; AI was used to find the bug, write the reproducer, write the fix and the tests, run the tests listed above, and draft this description (the rebase and the re-run at a8449ab were done with Claude Code, claude-opus-5-5); AI-generated code: include/exec/sequence/merge_each.hpp (the changes in __receive_nested_sequence, +16/−3) and test/exec/sequence/test_merge_each.cpp (the four new test cases and their helpers, +128).

🤖 Generated with Claude Code

…ails or is stopped

`merge_each` connects each item of the input sequence, which is a sender
of a nested sequence, to `__receive_nested_sequence`. When that sender
completed with `set_error` or `set_stopped`, or when subscribing to the
nested sequence threw, the receiver only updated the bookkeeping of the
`merge_each` operation and never completed the next receiver of the
input sequence. The input sequence was left waiting for that item, so
the `merge_each` operation never completed. For example,

    merge_each(just(1) | then([](int) -> decltype(just(0)) { throw 42; }))
      | ignore_all_values()

never called its receiver, and a stop request that arrived between two
nested sequences of `merge_each(iterate(...) | transform_each(...))`
was lost in the same way.

This diff completes the next receiver in those three places, as
`__receive_nested_values` already does once a nested sequence has been
subscribed. An error breaks the input sequence and is delivered as the
last item, like the errors of nested sequences. A stopped sender is
skipped, unless stop has been requested, in which case the input
sequence is asked to stop too.

The new tests cover an erroring sender, a stopped sender, a throwing
`subscribe`, and a stop request between nested sequences.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: Dezhen Lu <dezhen.lu@student.uni-tuebingen.de>
@copy-pr-bot

copy-pr-bot Bot commented Nov 11, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

exec::merge_each never completes when the sender of a nested sequence completes with set_error or set_stopped

1 participant