diff --git a/include/boost/capy/detail/run_callbacks.hpp b/include/boost/capy/detail/run_callbacks.hpp index 64526ac08..20f23aa2f 100644 --- a/include/boost/capy/detail/run_callbacks.hpp +++ b/include/boost/capy/detail/run_callbacks.hpp @@ -11,6 +11,7 @@ #define BOOST_CAPY_DETAIL_RUN_CALLBACKS_HPP #include +#include #include #include @@ -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. + } } }; @@ -92,7 +101,7 @@ struct handler_pair if constexpr(std::invocable) h1_(ep); else - std::rethrow_exception(ep); + default_handler{}(ep); } }; diff --git a/include/boost/capy/ex/run_async.hpp b/include/boost/capy/ex/run_async.hpp index 09268219f..26b8e128f 100644 --- a/include/boost/capy/ex/run_async.hpp +++ b/include/boost/capy/ex/run_async.hpp @@ -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 h_; @@ -261,9 +261,7 @@ struct BOOST_CAPY_CORO_DESTROY_WHEN_COMPLETE { } - void unhandled_exception() noexcept - { - } + void unhandled_exception() { throw; } }; std::coroutine_handle h_; @@ -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; + } } }; @@ -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()); diff --git a/test/unit/ex/run_async.cpp b/test/unit/ex/run_async.cpp index 61ebeb153..2348b3bac 100644 --- a/test/unit/ex/run_async.cpp +++ b/test/unit/ex/run_async.cpp @@ -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{})(throws_exception()), + test_exception); + } + void testVoidTaskResultHandler() { @@ -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() @@ -708,12 +750,15 @@ struct run_async_test testNoHandlers(); testValueAllocator(); testValueAllocatorException(); + testValueAllocatorPropagates(); testResultHandler(); testVoidTaskResultHandler(); testDualHandlers(); testOverloadedHandler(); // Exception Handling + testDefaultHandlerPropagates(); + testRethrowingHandlerPropagates(); testErrorHandlerReceivesException(); testOverloadedHandlerException(); diff --git a/test/unit/quitter.cpp b/test/unit/quitter.cpp index a0778cdea..5e7a762fb 100644 --- a/test/unit/quitter.cpp +++ b/test/unit/quitter.cpp @@ -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 //---------------------------------------------------------- @@ -863,6 +884,7 @@ struct quitter_test testVoidCompletion(); testExceptionPropagation(); testStopBeforeFirstAwait(); + testStopWithDefaultHandler(); testStopDuringIO(); testStopPropagationChain(); testMixingQuitterAndTask();