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
9 changes: 7 additions & 2 deletions doc/modules/ROOT/pages/4.coroutines/4b.launching.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ Always use the two-call pattern in a single expression.

[source,cpp]
----
// Result handler only (exceptions rethrown)
// Result handler only (an unhandled exception calls std::terminate)
run_async(ex, [](int result) {
std::cout << "Got: " << result << "\n";
})(compute());
Expand All @@ -89,7 +89,12 @@ run_async(ex,
)(compute());
----

When no handlers are provided, results are discarded and exceptions are rethrown (causing `std::terminate` if uncaught).
When no result handler is provided, the result is discarded. An exception
that goes unhandled (no error handler was supplied, or a handler let one
escape) calls `std::terminate`. To react to an error, pass an error handler;
it receives the `std::exception_ptr` and should handle it in place rather
than rethrowing. To catch an error, `co_await` the work inside a coroutine
and use `try`/`catch` rather than launching it fire-and-forget.

== run: Executor Hopping Within Coroutines

Expand Down
2 changes: 1 addition & 1 deletion doc/modules/ROOT/pages/9.design/9l.RunApi.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ This document explains the naming conventions and call syntax of the two launche

=== `run_async` -- Fire-and-Forget Launch

`run_async` launches any _IoRunnable_ from non-coroutine code: `main()`, callback handlers, event loops. `task<T>` is the most common conforming type, but any user-defined type satisfying the concept works. The function does not return a value to the caller. Handlers receive the task's result or exception after completion.
`run_async` launches any _IoRunnable_ from non-coroutine code: `main()`, callback handlers, event loops. `task<T>` is the most common conforming type, but any user-defined type satisfying the concept works. The function does not return a value to the caller. Handlers receive the task's result or exception after completion, as data; they should not throw. An exception that no handler consumes (none was supplied, or a handler let one escape) calls `std::terminate`; it is never silently discarded. To catch an error instead, `co_await` the work inside a coroutine.

[source,cpp]
----
Expand Down
15 changes: 13 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,18 @@ 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.
}
// A real unhandled exception propagates to the trampoline's
// unhandled_exception, which terminates.
}
};

Expand Down Expand Up @@ -92,7 +103,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
24 changes: 16 additions & 8 deletions include/boost/capy/ex/run_async.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#include <algorithm>
#include <coroutine>
#include <cstring>
#include <exception>
#include <memory_resource>
#include <new>
#include <stop_token>
Expand Down Expand Up @@ -167,7 +168,12 @@ struct BOOST_CAPY_CORO_DESTROY_WHEN_COMPLETE run_async_trampoline
{
}

void unhandled_exception() noexcept {} // LCOV_EXCL_LINE unsupported: throwing task with no error handler
// An exception reaches here only by escaping a handler: a handler
// that threw, or the default handler rethrowing an otherwise
// unhandled task exception. Cancellation is filtered out earlier
// by default_handler, so this is always a genuine error with no
// owner to receive it: fail fast.
void unhandled_exception() noexcept { std::terminate(); } // LCOV_EXCL_LINE
};

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

void unhandled_exception() noexcept
{
}
// See primary template: an escaping handler exception is fatal.
void unhandled_exception() noexcept { std::terminate(); } // LCOV_EXCL_LINE
};

std::coroutine_handle<promise_type> h_;
Expand Down Expand Up @@ -427,7 +432,10 @@ 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 unhandled exception
thrown by the task calls `std::terminate`; pass an error handler to
receive it as an `exception_ptr`, or `co_await` the work inside a
coroutine if you want to catch it.

@par Thread Safety
The wrapper and handlers may be called from any thread where the
Expand Down Expand Up @@ -461,7 +469,7 @@ run_async(Ex ex)

The handler `h1` is called with the task's result on success. If `h1`
is also invocable with `std::exception_ptr`, it handles exceptions too.
Otherwise, exceptions are rethrown.
Otherwise, an unhandled exception calls `std::terminate`.

@par Thread Safety
The handler may be called from any thread where the executor
Expand Down Expand Up @@ -549,8 +557,8 @@ run_async(Ex ex, H1 h1, H2 h2)
/** Asynchronously launch a lazy task with stop token support.

The stop token is propagated to the task, enabling cooperative
cancellation. With no handlers, the result is discarded and
exceptions are rethrown.
cancellation. With no handlers, the result is discarded and an
unhandled exception calls `std::terminate`.

@par Thread Safety
The wrapper may be called from any thread where the executor
Expand Down
59 changes: 55 additions & 4 deletions test/unit/ex/run_async.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,19 @@

#include <atomic>
#include <cstddef>
#include <cstdio>
#include <memory>
#include <queue>
#include <stdexcept>
#include <string>
#include <thread>
#include <vector>

#if !defined(_WIN32)
#include <unistd.h>
#include <sys/wait.h>
#endif

/*
Implementation Notes for run_async
==================================
Expand Down Expand Up @@ -305,10 +311,52 @@ 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.
// Note: a task that throws with no error handler calls std::terminate
// (the trampoline's unhandled_exception). That path is fatal and not
// unit-testable here; pass an error handler to observe exceptions.

#if !defined(_WIN32)
// Death test: an exception escaping a handler must call std::terminate.
// Run each scenario in a forked child (the child aborts); the parent
// verifies the child did not exit normally. POSIX-only.
void
testTerminateOnUnhandled()
{
auto terminates = [](auto fn) {
::pid_t pid = ::fork();
BOOST_TEST(pid >= 0);
if(pid == 0)
{
// Hush the abort message; the binding satisfies freopen's
// warn_unused_result.
[[maybe_unused]] auto* f =
std::freopen("/dev/null", "w", stderr);
fn();
_exit(0); // reached only if no terminate happened
}
int status = 0;
::waitpid(pid, &status, 0);
return !(WIFEXITED(status) && WEXITSTATUS(status) == 0);
};

// No handler + throwing task: default handler rethrows -> terminate.
BOOST_TEST(terminates([] {
int dc = 0;
sync_executor d(dc);
run_async(d)(throws_exception());
}));

// Error handler rethrows -> escapes the handler -> terminate.
BOOST_TEST(terminates([] {
int dc = 0;
sync_executor d(dc);
run_async(d,
[](int) {},
[](std::exception_ptr ep) { std::rethrow_exception(ep); }
)(throws_exception());
}));
}
#endif

void
testErrorHandlerReceivesException()
Expand Down Expand Up @@ -714,6 +762,9 @@ struct run_async_test
testOverloadedHandler();

// Exception Handling
#if !defined(_WIN32)
testTerminateOnUnhandled();
#endif
testErrorHandlerReceivesException();
testOverloadedHandlerException();

Expand Down
Loading