Skip to content

Type-erased sequence sender does not properly propagate stop token #1668

@GitJQ

Description

@GitJQ

Description

When using any_sequence_of to type-erase a sequence sender, the async_scope::request_stop() mechanism fails to stop the sequence generation, while concrete-typed sequence senders work correctly.

code

#include <iostream>
#include <chrono>
#include <ranges>
#include <atomic>

#include <stdexec/execution.hpp>
#include <exec/async_scope.hpp>
#include <exec/sequence/iterate.hpp>
#include <exec/sequence/transform_each.hpp>
#include <exec/sequence/ignore_all_values.hpp>
#include <exec/sequence/any_sequence_of.hpp>
#include <exec/timed_thread_scheduler.hpp>

using namespace std::chrono_literals;

namespace ex = stdexec;

template<class... Sigs>
using any_sequence_of = typename exec::any_sequence_receiver_ref<
    ex::completion_signatures<Sigs...>>::template any_sender<>;

using string_sequence = any_sequence_of<
    ex::set_value_t(std::string),
    ex::set_error_t(std::exception_ptr),
    ex::set_stopped_t()>;

auto generate_concrete_sequence(ex::scheduler auto sched)
{
    return exec::iterate(std::views::iota(0))
         | exec::transform_each(ex::let_value([sched](int i) {
               return exec::schedule_after(sched, 100ms)
                    | ex::then([i]() {
                          return std::to_string(i);
                      });
           }));
}

string_sequence generate_erased_sequence(ex::scheduler auto sched)
{
    return generate_concrete_sequence(sched);
}

int main()
{
    exec::timed_thread_context timer_ctx;

    auto time_sched = timer_ctx.get_scheduler();

    exec::async_scope scope;
    std::atomic<int>  erased_counter{0};
    std::atomic<int>  concrete_counter{0};

    auto process_a = generate_concrete_sequence(time_sched)
                   | exec::transform_each(ex::then([&](const std::string& str) {
                         concrete_counter.fetch_add(1);
                         std::cout << "A: " << str << std::endl;
                     }))
                   | exec::ignore_all_values();

    auto process_b = generate_erased_sequence(time_sched)
                   | exec::transform_each(ex::then([&](const std::string& str) {
                         erased_counter.fetch_add(1);
                         std::cout << "B: " << str << std::endl;
                     }))
                   | exec::ignore_all_values();

    auto timeout = exec::schedule_after(time_sched, 1s)
                 | ex::then([&]() {
                       std::cout << "\n=== Timeout - requesting stop ===" << std::endl;
                       scope.request_stop();
                   });

    scope.spawn(std::move(process_a));
    scope.spawn(std::move(process_b));
    scope.spawn(std::move(timeout));

    ex::sync_wait(scope.on_empty());

    std::cout << "\n=== Results ===" << std::endl;
    std::cout << "Concrete sequence counter: " << concrete_counter.load() << std::endl;
    std::cout << "Erased sequence counter: " << erased_counter.load() << std::endl;
    std::cout << "\nExpected: Both should stop at ~10 iterations (1s / 100ms)" << std::endl;
    std::cout << "Actual: Concrete stops correctly, Erased continues indefinitely" << std::endl;

    return 0;
}

Expected:

  • After 1 second, scope.request_stop() is called
  • Both sequences should stop generating new elements

Actual:

  • Concrete-typed sequence stops correctly
  • Type-erased sequence continues indefinitely

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions