135inline void check_executor_safety_runtime(IExecutor& ui, IExecutor& worker) {
136 if (!ui.is_safe_graph_executor()) {
137 throw std::invalid_argument(
138 "AsyncCommand: ui executor is not safe to use as the "
139 "graph-thread executor. Remedy: install a main-thread "
140 "IExecutor before constructing AsyncCommand-owning view "
141 "models -- use MainThreadExecutor, or wrap your platform "
142 "dispatcher with aria::runtime::DispatcherExecutor "
143 "(aria/runtime/dispatcher_executor.hpp). Third-party "
144 "executors must override IExecutor::caps() to advertise "
145 "SchedulerCaps::GraphSafe.");
147 if (!worker.is_safe_worker_executor()) {
148 throw std::invalid_argument(
149 "AsyncCommand: worker executor is not safe to host worker "
150 "tasks. Remedy: pass a ThreadPoolExecutor (or "
151 "MainThreadExecutor for single-threaded hosts) as the worker. "
152 "Third-party executors must override IExecutor::caps() to "
153 "advertise SchedulerCaps::WorkerSafe.");
155 auto* ui_inline =
dynamic_cast<InlineExecutor*
>(&ui);
156 auto* worker_inline =
dynamic_cast<InlineExecutor*
>(&worker);
157 if (ui_inline && !worker_inline) {
158 throw std::invalid_argument(
159 "AsyncCommand: cannot use InlineExecutor as the "
160 "graph-thread executor when worker runs on a different "
161 "thread. The final co_await schedule_on(ui) would "
162 "inline-resume on the worker thread and write reactive "
163 "Properties from there, tripping the graph thread-affinity "
164 "invariant. Remedy: install a real main-thread executor "
165 "BEFORE constructing this view model -- MainThreadExecutor "
166 "in tests / console apps, or "
167 "aria::runtime::DispatcherExecutor{*main_dispatcher()} in a "
168 "GUI host (aria/runtime/dispatcher_executor.hpp). See "
169 "docs/reference/lifecycle.md for the startup ordering "
176template<
typename R,
typename... Args>
177struct AsyncCommandState {
178 using ArgsTuple = std::tuple<Args...>;
182 CancellationSource cancel;
183 std::atomic<int> inflight{0};
185 Property<bool> is_executing{
false};
186 Property<std::optional<::aria::Error>> last_error{std::nullopt};
187 Property<std::string> last_error_message{
""};
188 Property<std::optional<R>> last_result{std::optional<R>{}};
190 std::mutex m_sources;
191 std::vector<std::shared_ptr<CancellationSource>> invocation_sources;
193 AsyncCommandState(IExecutor& u, IExecutor& w) : ui(&u), worker(&w) {}
197struct AsyncCommandState<void> {
200 CancellationSource cancel;
201 std::atomic<int> inflight{0};
203 Property<bool> is_executing{
false};
204 Property<std::optional<::aria::Error>> last_error{std::nullopt};
205 Property<std::string> last_error_message{
""};
207 std::mutex m_sources;
208 std::vector<std::shared_ptr<CancellationSource>> invocation_sources;
210 AsyncCommandState(IExecutor& u, IExecutor& w) : ui(&u), worker(&w) {}
217enum class AsyncFailureKind : std::uint8_t {
222struct AsyncFailureClassification {
223 AsyncFailureKind kind;
243inline AsyncFailureClassification classify_async_exception(
244 std::exception_ptr ex,
245 Property<std::optional<::aria::Error>>& last_error,
246 Property<std::string>& last_error_message,
247 std::string source_tag =
"AsyncCommand")
249 try { std::rethrow_exception(ex); }
250 catch (
const OperationCancelled&) {
254 ::aria::trace::Async{source_tag,
"cancelled", 0},
260 return {AsyncFailureKind::Cancellation, std::move(err)};
262 catch (
const TimeoutError& e) {
264 err.message = e.what();
267 ::aria::trace::Async{source_tag,
"timeout", 0},
271 last_error_message = err.message;
272 return {AsyncFailureKind::Failure, std::move(err)};
278 ::aria::trace::Async{source_tag,
"failure", 0},
281 last_error_message = err.message;
283 return {AsyncFailureKind::Failure, std::move(err)};
287template<
typename F,
typename... Args>
288concept CancellableAction =
289 std::invocable<F, CancellationToken, Args...>;
291template<
typename F,
typename... Args>
293 std::invocable<F, Args...>;
312 using State = AsyncCommandState<R>;
314 explicit Invocation(std::shared_ptr<State> s)
315 : state_(std::move(s)),
316 src_(std::make_shared<CancellationSource>()),
317 cmd_tok_(state_->cancel.token()),
318 inv_tok_(src_->token())
321 std::lock_guard lk(state_->m_sources);
322 state_->invocation_sources.push_back(src_);
324 const bool first = (state_->inflight.fetch_add(1, std::memory_order_acq_rel) == 0);
326 state_->is_executing =
true;
327 state_->last_error = std::nullopt;
328 state_->last_error_message =
"";
332 ::aria::trace::Async{
335 static_cast<std::uint64_t
>(state_->inflight.load(std::memory_order_relaxed)),
342 std::lock_guard lk(state_->m_sources);
343 auto& v = state_->invocation_sources;
344 v.erase(std::remove(v.begin(), v.end(), src_), v.end());
346 const bool last = (state_->inflight.fetch_sub(1, std::memory_order_acq_rel) == 1);
348 state_->is_executing =
false;
352 ::aria::trace::Async{
355 static_cast<std::uint64_t
>(state_->inflight.load(std::memory_order_relaxed)),
360 Invocation(
const Invocation&) =
delete;
361 Invocation& operator=(
const Invocation&) =
delete;
363 const CancellationToken& cmd_tok() const noexcept {
return cmd_tok_; }
364 const CancellationToken& inv_tok() const noexcept {
return inv_tok_; }
367 void throw_if_cancelled()
const {
368 cmd_tok_.throw_if_cancelled();
369 inv_tok_.throw_if_cancelled();
373 std::shared_ptr<State> state_;
374 std::shared_ptr<CancellationSource> src_;
375 CancellationToken cmd_tok_;
376 CancellationToken inv_tok_;
382template<
typename R,
typename... Args>
383class AsyncCommandCore {
385 using State = AsyncCommandState<R>;
386 using Action = std::function<Task<R>(CancellationToken, Args...)>;
387 using ArgsTuple = std::shared_ptr<std::tuple<Args...>>;
388 using PreparedAction = std::function<Task<R>(
const CancellationToken&, std::tuple<Args...>&)>;
391 : state(std::move(s)), action(std::move(a)), policy(p) {}
393 std::shared_ptr<State> state;
394 PreparedAction action;
403 template<
typename Fn>
404 static PreparedAction make_action(Fn f) {
405 auto target = std::make_shared<Fn>(std::move(f));
406 return [target = std::move(target)]([[maybe_unused]]
const CancellationToken& token,
407 std::tuple<Args...>& args) -> Task<R> {
408 return std::apply([&](
auto&... values) -> Task<R> {
409 if constexpr (CancellableAction<Fn, Args...>) {
410 return (*target)(token, std::move(values)...);
412 return (*target)(std::move(values)...);
420 bool accept_new_invocation() {
425 cancel_all_in_flight();
428 return state->inflight.load(std::memory_order_acquire) == 0;
433 void cancel_all_in_flight() {
434 std::vector<std::shared_ptr<CancellationSource>> victims;
436 std::lock_guard lk(state->m_sources);
437 victims = state->invocation_sources;
439 for (
auto& s : victims) s->cancel();
442 void cancel_on_destruction() {
446 auto keep_alive = state;
447 std::vector<std::shared_ptr<CancellationSource>> victims;
449 std::lock_guard lk(keep_alive->m_sources);
450 victims.swap(keep_alive->invocation_sources);
452 keep_alive->cancel.cancel();
455 for (
auto& source : victims) source->cancel();
462template<
typename R,
typename... Args>
464 using Core = detail::AsyncCommandCore<R, Args...>;
465 using State =
typename Core::State;
466 using ArgsTuple =
typename Core::ArgsTuple;
467 using Invocation = detail::Invocation<R>;
487 template<
typename Ui,
typename Worker,
typename Fn>
488 requires (detail::CancellableAction<Fn, Args...>
489 || detail::PlainAction<Fn, Args...>)
490 && std::is_base_of_v<IExecutor, Ui>
491 && std::is_base_of_v<IExecutor, Worker>
492 && (!std::is_same_v<Ui, IExecutor>
493 || !std::is_same_v<Worker, IExecutor>)
496 : core_(
std::make_shared<State>(ui, worker),
497 Core::template make_action<Fn>(
std::move(action)),
504 if constexpr (!std::is_same_v<Ui, IExecutor>) {
506 "AsyncCommand: `ui` must be a graph-thread executor. "
507 "Specialise `aria::async::is_safe_graph_executor<YourExec>` "
508 "or use `MainThreadExecutor`.");
510 if constexpr (!std::is_same_v<Worker, IExecutor>) {
512 "AsyncCommand: `worker` must be a worker-capable executor. "
513 "Specialise `aria::async::is_safe_worker_executor<YourExec>` "
514 "or use `ThreadPoolExecutor` / `MainThreadExecutor`.");
516 if constexpr (!std::is_same_v<Ui, IExecutor>
517 && !std::is_same_v<Worker, IExecutor>) {
518 static_assert(!(std::is_same_v<Ui, InlineExecutor>
519 && !std::is_same_v<Worker, InlineExecutor>),
520 "AsyncCommand: cannot use `InlineExecutor` as the graph-thread "
521 "executor when `worker` runs on a different thread. Use "
522 "`MainThreadExecutor` for the `ui` parameter.");
524 detail::check_executor_safety_runtime(ui, worker);
529 template<
typename Fn>
530 requires detail::CancellableAction<Fn, Args...>
531 || detail::PlainAction<Fn, Args...>
534 : core_(
std::make_shared<State>(ui, worker),
535 Core::template make_action<Fn>(
std::move(action)),
542 detail::check_executor_safety_runtime(ui, worker);
549 core_.cancel_on_destruction();
560 if (!core_.accept_new_invocation())
return;
561 auto tup = std::make_shared<std::tuple<Args...>>(std::move(args)...);
562 fire_and_forget_(tup).start_detached_();
576 if (!core_.accept_new_invocation()) {
579 auto tup = std::make_shared<std::tuple<Args...>>(std::move(args)...);
580 co_return co_await run_to_result_(tup);
597 auto r =
co_await run_to_result_(std::move(args));
598 if (r.failed() && r.error) {
612 auto state = core_.state;
613 auto action = core_.action;
614 Invocation inv{state};
621 if (inv.cmd_tok().is_cancelled() || inv.inv_tok().is_cancelled()) {
626 std::optional<R> value;
627 std::exception_ptr ex;
630 inv.throw_if_cancelled();
631 value.emplace(
co_await action(inv.inv_tok(), *args));
633 ex = std::current_exception();
638 auto cls = detail::classify_async_exception(
639 ex, state->last_error, state->last_error_message);
640 if (cls.kind == detail::AsyncFailureKind::Cancellation) {
645 state->last_result = value;
651template<
typename... Args>
653 using Core = detail::AsyncCommandCore<void, Args...>;
654 using State =
typename Core::State;
655 using ArgsTuple =
typename Core::ArgsTuple;
656 using Invocation = detail::Invocation<void>;
663 template<
typename Ui,
typename Worker,
typename Fn>
664 requires (detail::CancellableAction<Fn, Args...>
665 || detail::PlainAction<Fn, Args...>)
666 && std::is_base_of_v<IExecutor, Ui>
667 && std::is_base_of_v<IExecutor, Worker>
668 && (!std::is_same_v<Ui, IExecutor>
669 || !std::is_same_v<Worker, IExecutor>)
672 : core_(
std::make_shared<State>(ui, worker),
673 Core::template make_action<Fn>(
std::move(action)),
679 if constexpr (!std::is_same_v<Ui, IExecutor>) {
681 "AsyncCommand<void>: `ui` must be a graph-thread executor. "
682 "Use `MainThreadExecutor` for any multi-threaded scenario.");
684 if constexpr (!std::is_same_v<Worker, IExecutor>) {
686 "AsyncCommand<void>: `worker` must be a worker-capable executor.");
688 if constexpr (!std::is_same_v<Ui, IExecutor>
689 && !std::is_same_v<Worker, IExecutor>) {
690 static_assert(!(std::is_same_v<Ui, InlineExecutor>
691 && !std::is_same_v<Worker, InlineExecutor>),
692 "AsyncCommand<void>: cannot use `InlineExecutor` as the "
693 "graph-thread executor when `worker` runs on a different thread. "
694 "Use `MainThreadExecutor` for `ui`.");
696 detail::check_executor_safety_runtime(ui, worker);
704 template<
typename Fn>
705 requires detail::CancellableAction<Fn, Args...>
706 || detail::PlainAction<Fn, Args...>
709 : core_(
std::make_shared<State>(ui, worker),
710 Core::template make_action<Fn>(
std::move(action)),
716 detail::check_executor_safety_runtime(ui, worker);
727 if (!core_.accept_new_invocation())
return;
728 auto tup = std::make_shared<std::tuple<Args...>>(std::move(args)...);
729 fire_and_forget_(tup).start_detached_();
737 if (!core_.accept_new_invocation()) {
740 auto tup = std::make_shared<std::tuple<Args...>>(std::move(args)...);
741 co_return co_await run_to_result_(tup);
752 auto r =
co_await run_to_result_(std::move(args));
753 if (r.failed() && r.error) {
760 auto state = core_.state;
761 auto action = core_.action;
762 Invocation inv{state};
765 if (inv.cmd_tok().is_cancelled() || inv.inv_tok().is_cancelled()) {
770 std::exception_ptr ex;
773 inv.throw_if_cancelled();
774 co_await action(inv.inv_tok(), *args);
776 ex = std::current_exception();
781 auto cls = detail::classify_async_exception(
782 ex, state->last_error, state->last_error_message);
783 if (cls.kind == detail::AsyncFailureKind::Cancellation) {