|
| template<typename R, typename... Args, typename Fn> |
| auto | action_with_timeout (IDelayedScheduler &timer, std::chrono::milliseconds duration, Fn factory) -> std::function< Task< R >(CancellationToken, Args...)> |
| | Wrap an AsyncCommand action factory with a per-invocation timeout.
|
| template<typename R, typename... Args, typename Fn> |
| auto | action_with_retry (int max_attempts, std::chrono::milliseconds initial_backoff, IDelayedScheduler &timer, Fn factory, std::function< bool(const std::exception &)> should_retry=nullptr) -> std::function< Task< R >(CancellationToken, Args...)> |
| | Wrap an AsyncCommand action with retry + exponential backoff.
|
| ErrorSink & | error_sink_ () |
| void | set_error_sink (ErrorSink sink) noexcept |
| void | report_async_error (std::string_view msg) noexcept |
| auto | schedule_on (IExecutor &exec) |
| | Schedule a coroutine to resume on the given executor.
|
| template<typename Factory> |
| auto | retry (int max_attempts, Factory factory) -> Task< detail::factory_value_t< Factory > > |
| template<typename Predicate, typename Factory> |
| auto | retry_if (int max_attempts, Predicate should_retry, Factory factory) -> Task< detail::factory_value_t< Factory > > |
| template<typename Factory> |
| auto | retry_with_backoff (int max_attempts, std::chrono::milliseconds initial, IDelayedScheduler &timer, Factory factory) -> Task< detail::factory_value_t< Factory > > |
| template<typename Fn> |
| auto | on_ui (IExecutor &ui_exec, Fn body) -> Task< detail::body_result_t< Fn > > |
| | Run body() (a coroutine factory).
|
| template<typename Body, typename OnError> |
| Task< void > | on_ui_safe (IExecutor &ui_exec, Body body, OnError on_error) |
| | Like on_ui(), but routes any exception to on_error(e) on the UI thread.
|
| auto | operator co_await (CancellationToken tok) |
| | Awaitable that suspends the current coroutine until cancellation fires on tok.
|
| template<typename Factory> |
| auto | with_timeout (IDelayedScheduler &timer, std::chrono::milliseconds duration, Factory factory, OnTimeout on_timeout=OnTimeout::Cancel) -> Task< detail::timeout_factory_value_t< Factory > > |
| template<typename Factory> |
| auto | with_timeout (CancellationToken parent, IDelayedScheduler &timer, std::chrono::milliseconds duration, Factory factory, OnTimeout on_timeout=OnTimeout::Cancel) -> Task< detail::timeout_factory_value_t< Factory > > |
| auto | schedule_after (VirtualTimeExecutor &vt, VirtualTimeExecutor::duration delay) |
| | Awaiter that resumes the coroutine after delay of virtual time.
|
| template<typename... Ts> |
| auto | when_all (Task< Ts >... tasks) |
| template<typename T> |
| auto | when_any (std::vector< Task< T > > tasks) |
| template<typename T> |
| auto | when_any_cancellable (std::vector< std::function< Task< T >(CancellationToken)> > factories) |
template<typename R, typename... Args, typename Fn>
| auto aria::async::action_with_retry |
( |
int | max_attempts, |
|
|
std::chrono::milliseconds | initial_backoff, |
|
|
IDelayedScheduler & | timer, |
|
|
Fn | factory, |
|
|
std::function< bool(const std::exception &)> | should_retry = nullptr ) -> std::function< Task< R >(CancellationToken, Args...)> |
Wrap an AsyncCommand action with retry + exponential backoff.
Each attempt sees the SAME parent CancellationToken (the one the AsyncCommand injects on every invocation), so cancelling the command stops both the in-flight attempt AND the inter-attempt sleep.
should_retry is optional. When supplied, it gates retries: the predicate receives the exception that escaped the attempt and returns true to retry, false to propagate immediately. When null (default), every std::exception is retried up to max_attempts.
template<typename Body, typename OnError>
| Task< void > aria::async::on_ui_safe |
( |
IExecutor & | ui_exec, |
|
|
Body | body, |
|
|
OnError | on_error ) |
Like on_ui(), but routes any exception to on_error(e) on the UI thread.
Always returns Task<void> (the success value, if any, is discarded — use the success path of body to update Properties directly).
Awaitable that suspends the current coroutine until cancellation fires on tok.
It does NOT throw on resume — that responsibility belongs to the coroutine body, via an explicit throw_if_cancelled() probe:
while (true) { tok.throw_if_cancelled(); // probe — throws if cancelled co_await tok; // park until cancel arrives }
In most code you just want to PROBE periodically — use the explicit tok.throw_if_cancelled() instead.
Race-awareness contract (must hold on every supported toolchain, including MSVC and MinGW UCRT64):
The cancellation callback MUST NOT call h.resume() directly from inside the cancellation broadcast loop. Doing so means the resumed coroutine runs deeply nested below cancel(), with the following frames on the stack at the moment any in-coroutine try/catch handler matches an exception:
cancel()
└─ for c in cbs (callbacks loop, holds vector)
└─ std::function::operator()
└─ awaiter cb lambda
└─ h.resume() (the parked coroutine runs here)
└─ ... user code throws ...
MSVC release builds were observed to silently bypass the coroutine's own try/catch blocks under exactly this stack shape; MinGW UCRT64 went further and SIGSEGV'd inside the exception unwind. Both are symptoms of the SEH / DWARF personality routine getting confused about which try/catch ranges are live when the throwing PC sits inside a coroutine frame whose execution has been re-entered through an std::function::operator() indirection.
- To eliminate the nesting, cancellation callbacks instead defer the resume: they push the coroutine handle onto a thread-local pending list, and the very topmost cancel() that started the broadcast drains that list once all callbacks have returned. Resumption therefore happens on the cancel() function frame itself — a plain C++ function frame, free of std::function indirection — and any exception unwind out of the resumed coroutine sees a vanilla call stack that the personality routine handles correctly on every toolchain.
- await_suspend MUST NOT cause h.resume() to run on the current stack — neither directly, nor indirectly through a cancellation callback that fires synchronously while we are still inside await_suspend. Touching the coroutine frame after such an in-stack resume is undefined behaviour. Returning false lets the compiler resume the caller in place; we use a tiny shared state machine to coordinate that race with the callback path.
State machine:
- preparing — await_suspend is registering the callback and has not committed to suspension yet.
- suspended — the coroutine is parked; a future cancellation callback must defer-resume it.
- cancellation_before_suspend — cancellation was observed while still preparing; await_suspend must return false and let the compiler continue the coroutine.
- resumed — the callback has scheduled a deferred resume of the parked coroutine.