Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions include/boost/capy/detail/run_callbacks.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#define BOOST_CAPY_DETAIL_RUN_CALLBACKS_HPP

#include <boost/capy/detail/config.hpp>
#include <boost/capy/detail/stop_requested_exception.hpp>

#include <concepts>
#include <exception>
Expand All @@ -34,8 +35,16 @@ struct default_handler

void operator()(std::exception_ptr ep) const
{
if(ep)
if(!ep)
return;
try
{
std::rethrow_exception(ep);
}
catch(stop_requested_exception const&)
{
// Cancellation is a normal completion, not an error.
}
}
};

Expand Down Expand Up @@ -92,7 +101,7 @@ struct handler_pair<H1, default_handler>
if constexpr(std::invocable<H1, std::exception_ptr>)
h1_(ep);
else
std::rethrow_exception(ep);
default_handler{}(ep);
}
};

Expand Down
29 changes: 23 additions & 6 deletions include/boost/capy/ex/run_async.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ struct BOOST_CAPY_CORO_DESTROY_WHEN_COMPLETE run_async_trampoline
{
}

void unhandled_exception() noexcept {} // LCOV_EXCL_LINE unsupported: throwing task with no error handler
void unhandled_exception() { throw; }
};

std::coroutine_handle<promise_type> h_;
Expand Down Expand Up @@ -261,9 +261,7 @@ struct BOOST_CAPY_CORO_DESTROY_WHEN_COMPLETE
{
}

void unhandled_exception() noexcept
{
}
void unhandled_exception() { throw; }
};

std::coroutine_handle<promise_type> h_;
Expand Down Expand Up @@ -414,7 +412,18 @@ class [[nodiscard]] run_async_wrapper
// safe_resume is not needed here: TLS is already saved in the
// constructor (saved_tls_) and restored in the destructor.
p.task_cont_.h = task_h;
p.wg_.executor().dispatch(p.task_cont_).resume();
auto tr = tr_.h_;
try
{
p.wg_.executor().dispatch(p.task_cont_).resume();
}
catch(...)
{
// A propagating handler leaves the trampoline suspended at its
// final suspend point instead of self-destroying; reclaim it.
tr.destroy();
throw;
}
}
};

Expand All @@ -427,12 +436,20 @@ class [[nodiscard]] run_async_wrapper
storing the wrapper and calling it later violates LIFO ordering.

Uses the default recycling frame allocator for coroutine frames.
With no handlers, the result is discarded and exceptions are rethrown.
With no handlers, the result is discarded. An exception that escapes
a handler is not swallowed: it propagates out of the call that
resumes the task. Cooperative cancellation via the stop token is a
normal completion and is not propagated.

@par Thread Safety
The wrapper and handlers may be called from any thread where the
executor schedules work.

@par Exception Safety
An exception escaping a handler (including a rethrowing error
handler or the default handler) propagates out of the call that
resumes the task rather than being swallowed.

@par Example
@code
run_async(ioc.get_executor())(my_task());
Expand Down
53 changes: 49 additions & 4 deletions test/unit/ex/run_async.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,21 @@ struct run_async_test
BOOST_TEST(error_called);
}

void
testValueAllocatorPropagates()
{
// Value-allocator trampoline: with no error handler the default
// handler rethrows, exercising propagation through the primary
// template (the memory_resource* specialization is covered by
// testDefaultHandlerPropagates).
int dispatch_count = 0;
sync_executor d(dispatch_count);

BOOST_TEST_THROWS(
run_async(d, std::allocator<std::byte>{})(throws_exception()),
test_exception);
}

void
testVoidTaskResultHandler()
{
Expand Down Expand Up @@ -305,10 +320,37 @@ struct run_async_test
co_return;
}

// Note: testDefaultRethrow removed - if no error handler is provided
// and the task throws, the exception goes to unhandled_exception which
// is undefined behavior. Users must provide an error handler if they
// want to handle exceptions.
void
testDefaultHandlerPropagates()
{
// No error handler: the default handler rethrows the task's
// exception, which propagates out of the resuming run() call
// (here the inline sync dispatch).
int dispatch_count = 0;
sync_executor d(dispatch_count);

BOOST_TEST_THROWS(
run_async(d)(throws_exception()), test_exception);
}

void
testRethrowingHandlerPropagates()
{
// An error handler that rethrows propagates out of the call
// that resumes the task instead of being swallowed.
int dispatch_count = 0;
sync_executor d(dispatch_count);

BOOST_TEST_THROWS(
run_async(d,
[](int) {},
[](std::exception_ptr ep) {
if(ep)
std::rethrow_exception(ep);
}
)(throws_exception()),
test_exception);
}

void
testErrorHandlerReceivesException()
Expand Down Expand Up @@ -708,12 +750,15 @@ struct run_async_test
testNoHandlers();
testValueAllocator();
testValueAllocatorException();
testValueAllocatorPropagates();
testResultHandler();
testVoidTaskResultHandler();
testDualHandlers();
testOverloadedHandler();

// Exception Handling
testDefaultHandlerPropagates();
testRethrowingHandlerPropagates();
testErrorHandlerReceivesException();
testOverloadedHandlerException();

Expand Down
22 changes: 22 additions & 0 deletions test/unit/quitter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,27 @@ struct quitter_test
BOOST_TEST_EQ(dtor_count, 0);
}

void
testStopWithDefaultHandler()
{
// With no error handler, a stopped quitter must complete
// silently: cooperative cancellation is a normal outcome, so
// the default handler discards the stop sentinel rather than
// rethrowing it (which would escape run() and terminate).
int dispatch_count = 0;
test_executor ex(dispatch_count);
std::stop_source source;
source.request_stop();

int dtor_count = 0;
bool reached = false;

run_async(ex, source.get_token())(quitter_with_raii(dtor_count));

reached = true;
BOOST_TEST(reached);
}

//----------------------------------------------------------
// 5. Stop during I/O
//----------------------------------------------------------
Expand Down Expand Up @@ -863,6 +884,7 @@ struct quitter_test
testVoidCompletion();
testExceptionPropagation();
testStopBeforeFirstAwait();
testStopWithDefaultHandler();
testStopDuringIO();
testStopPropagationChain();
testMixingQuitterAndTask();
Expand Down
Loading