Skip to content

Add tests for saved_handler cancellation_slot::emplace exception safety#3075

Closed
ssam18 wants to merge 0 commit intoboostorg:developfrom
ssam18:fix-async-base-executor-binding
Closed

Add tests for saved_handler cancellation_slot::emplace exception safety#3075
ssam18 wants to merge 0 commit intoboostorg:developfrom
ssam18:fix-async-base-executor-binding

Conversation

@ssam18
Copy link
Copy Markdown

@ssam18 ssam18 commented Jan 24, 2026

Adds a test case to verify that saved_handler::emplace correctly cleans up when cancellation_slot::emplace throws an exception — specifically that the handler has no value after the throw.

This tests the behavior introduced in:

  • ad2c9a3 Fix cancellation slot retrieval in saved_handler::emplace
  • 0620a68 saved_handler destroys handler if cancellation_slot::emplace throws

@sehe
Copy link
Copy Markdown
Contributor

sehe commented Jan 24, 2026

The fix (also?) doesn't look accurate to me.

In particular, you applied it to the branch where the documentation claims the immediate executor should be used, however, your bind overrides it with any associated non-immediate executor.

Note that this->get_immediate_executor() correctly invokes boost::asio::get_associated_immediate_executor on the underlying handler h_.

Assuming that you ran into this as a problem in your code, can you share a minimal example where the wrong executor is used? Then we can understand what actually needs to be fixed. I can see two potential causes here:

That is, assuming you have demonstrated a case where the wrong executor is effectively used.

@ashtum
Copy link
Copy Markdown
Collaborator

ashtum commented Feb 1, 2026

I believe this is related to the issue briefly described in #3047.

Asio recommends using async_immediate inside composed operations to ensure the correct semantics for immediate completion. For example:

asio::async_immediate(self.get_io_executor(), std::move(self));

Internally, this leads to a call to dispatch that looks like:

template <typename CompletionHandler>
void operator()(CompletionHandler&& handler) const
{
  typename associated_immediate_executor<
    CompletionHandler, executor_type>::type ex =
      (get_associated_immediate_executor)(handler, ex_);
  (dispatch)(ex, static_cast<CompletionHandler&&>(handler));
}

If the handler has an associated executor, dispatch wraps it in asio::detail::work_dispatcher, which then dispatches the handler on that associated executor.

In this context, the handler is the composed operation object (self). When the supplied completion_token does not have an associated executor, self falls back to using the stream’s executor. As a result, asio::detail::work_dispatcher is always involved, and the completion handler is guaranteed to run either on its associated executor or on the stream’s executor.

Our current implementation behaves slightly differently. Instead of passing the composed operation object to dispatch, we pass the final handler (h_) directly:

auto const ex = this->get_immediate_executor();
net::dispatch(
    ex,
    net::append(std::move(h_), std::forward<Args>(args)...));

Because of this, dispatch does not attempt to use asio::detail::work_dispatcher with the stream’s executor when the handler lacks an associated executor. Consequently, the earlier guarantee no longer holds:

and the completion handler is guaranteed to run either on its associated executor or on the stream’s executor.

@ssam18 ssam18 closed this Apr 2, 2026
@ssam18 ssam18 force-pushed the fix-async-base-executor-binding branch from 478fea8 to 67106eb Compare April 2, 2026 23:15
@ssam18 ssam18 changed the title Fix async handlers running on wrong executor Add tests for saved_handler cancellation_slot::emplace exception safety Apr 2, 2026
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.

3 participants