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...>>;
390 : state(std::move(s)), action(std::move(a)), policy(p) {}
392 std::shared_ptr<State> state;
400 template<
typename Fn>
401 static Action make_action(Fn f) {
402 if constexpr (CancellableAction<Fn, Args...>) {
403 return Action(std::move(f));
404 }
else if constexpr (std::is_void_v<R>) {
405 return [f = std::move(f)](CancellationToken,
406 Args... a)
mutable -> Task<void> {
407 co_await f(std::move(a)...);
410 return [f = std::move(f)](CancellationToken,
411 Args... a)
mutable -> Task<R> {
412 co_return co_await f(std::move(a)...);
419 bool accept_new_invocation() {
424 cancel_all_in_flight();
427 return state->inflight.load(std::memory_order_acquire) == 0;
432 void cancel_all_in_flight() {
433 std::vector<std::shared_ptr<CancellationSource>> victims;
435 std::lock_guard lk(state->m_sources);
436 victims = state->invocation_sources;
438 for (
auto& s : victims) s->cancel();
441 void cancel_on_destruction() {
445 auto keep_alive = state;
446 std::vector<std::shared_ptr<CancellationSource>> victims;
448 std::lock_guard lk(keep_alive->m_sources);
449 victims.swap(keep_alive->invocation_sources);
451 keep_alive->cancel.cancel();
454 for (
auto& source : victims) source->cancel();
461template<
typename R,
typename... Args>
463 using Core = detail::AsyncCommandCore<R, Args...>;
464 using State =
typename Core::State;
465 using ArgsTuple =
typename Core::ArgsTuple;
466 using Invocation = detail::Invocation<R>;
486 template<
typename Ui,
typename Worker,
typename Fn>
487 requires (detail::CancellableAction<Fn, Args...>
488 || detail::PlainAction<Fn, Args...>)
489 && std::is_base_of_v<IExecutor, Ui>
490 && std::is_base_of_v<IExecutor, Worker>
491 && (!std::is_same_v<Ui, IExecutor>
492 || !std::is_same_v<Worker, IExecutor>)
495 : core_(
std::make_shared<State>(ui, worker),
496 Core::template make_action<Fn>(
std::move(action)),
503 if constexpr (!std::is_same_v<Ui, IExecutor>) {
505 "AsyncCommand: `ui` must be a graph-thread executor. "
506 "Specialise `aria::async::is_safe_graph_executor<YourExec>` "
507 "or use `MainThreadExecutor`.");
509 if constexpr (!std::is_same_v<Worker, IExecutor>) {
511 "AsyncCommand: `worker` must be a worker-capable executor. "
512 "Specialise `aria::async::is_safe_worker_executor<YourExec>` "
513 "or use `ThreadPoolExecutor` / `MainThreadExecutor`.");
515 if constexpr (!std::is_same_v<Ui, IExecutor>
516 && !std::is_same_v<Worker, IExecutor>) {
517 static_assert(!(std::is_same_v<Ui, InlineExecutor>
518 && !std::is_same_v<Worker, InlineExecutor>),
519 "AsyncCommand: cannot use `InlineExecutor` as the graph-thread "
520 "executor when `worker` runs on a different thread. Use "
521 "`MainThreadExecutor` for the `ui` parameter.");
523 detail::check_executor_safety_runtime(ui, worker);
528 template<
typename Fn>
529 requires detail::CancellableAction<Fn, Args...>
530 || detail::PlainAction<Fn, Args...>
533 : core_(
std::make_shared<State>(ui, worker),
534 Core::template make_action<Fn>(
std::move(action)),
541 detail::check_executor_safety_runtime(ui, worker);
548 core_.cancel_on_destruction();
559 if (!core_.accept_new_invocation())
return;
560 auto tup = std::make_shared<std::tuple<Args...>>(std::move(args)...);
561 fire_and_forget_(tup).start_detached_();
575 if (!core_.accept_new_invocation()) {
578 auto tup = std::make_shared<std::tuple<Args...>>(std::move(args)...);
579 co_return co_await run_to_result_(tup);
596 auto r =
co_await run_to_result_(std::move(args));
597 if (r.failed() && r.error) {
611 auto state = core_.state;
612 auto action = core_.action;
613 Invocation inv{state};
620 if (inv.cmd_tok().is_cancelled() || inv.inv_tok().is_cancelled()) {
625 std::optional<R> value;
626 std::exception_ptr ex;
629 inv.throw_if_cancelled();
630 value.emplace(
co_await std::apply(
631 [&](
auto&&... a) -> Task<R> {
632 return action(inv.inv_tok(), std::forward<
decltype(a)>(a)...);
635 ex = std::current_exception();
640 auto cls = detail::classify_async_exception(
641 ex, state->last_error, state->last_error_message);
642 if (cls.kind == detail::AsyncFailureKind::Cancellation) {
647 state->last_result = value;
653template<
typename... Args>
655 using Core = detail::AsyncCommandCore<void, Args...>;
656 using State =
typename Core::State;
657 using ArgsTuple =
typename Core::ArgsTuple;
658 using Invocation = detail::Invocation<void>;
665 template<
typename Ui,
typename Worker,
typename Fn>
666 requires (detail::CancellableAction<Fn, Args...>
667 || detail::PlainAction<Fn, Args...>)
668 && std::is_base_of_v<IExecutor, Ui>
669 && std::is_base_of_v<IExecutor, Worker>
670 && (!std::is_same_v<Ui, IExecutor>
671 || !std::is_same_v<Worker, IExecutor>)
674 : core_(
std::make_shared<State>(ui, worker),
675 Core::template make_action<Fn>(
std::move(action)),
681 if constexpr (!std::is_same_v<Ui, IExecutor>) {
683 "AsyncCommand<void>: `ui` must be a graph-thread executor. "
684 "Use `MainThreadExecutor` for any multi-threaded scenario.");
686 if constexpr (!std::is_same_v<Worker, IExecutor>) {
688 "AsyncCommand<void>: `worker` must be a worker-capable executor.");
690 if constexpr (!std::is_same_v<Ui, IExecutor>
691 && !std::is_same_v<Worker, IExecutor>) {
692 static_assert(!(std::is_same_v<Ui, InlineExecutor>
693 && !std::is_same_v<Worker, InlineExecutor>),
694 "AsyncCommand<void>: cannot use `InlineExecutor` as the "
695 "graph-thread executor when `worker` runs on a different thread. "
696 "Use `MainThreadExecutor` for `ui`.");
698 detail::check_executor_safety_runtime(ui, worker);
706 template<
typename Fn>
707 requires detail::CancellableAction<Fn, Args...>
708 || detail::PlainAction<Fn, Args...>
711 : core_(
std::make_shared<State>(ui, worker),
712 Core::template make_action<Fn>(
std::move(action)),
718 detail::check_executor_safety_runtime(ui, worker);
729 if (!core_.accept_new_invocation())
return;
730 auto tup = std::make_shared<std::tuple<Args...>>(std::move(args)...);
731 fire_and_forget_(tup).start_detached_();
739 if (!core_.accept_new_invocation()) {
742 auto tup = std::make_shared<std::tuple<Args...>>(std::move(args)...);
743 co_return co_await run_to_result_(tup);
754 auto r =
co_await run_to_result_(std::move(args));
755 if (r.failed() && r.error) {
762 auto state = core_.state;
763 auto action = core_.action;
764 Invocation inv{state};
767 if (inv.cmd_tok().is_cancelled() || inv.inv_tok().is_cancelled()) {
772 std::exception_ptr ex;
775 inv.throw_if_cancelled();
777 [&](
auto&&... a) -> Task<void> {
778 return action(inv.inv_tok(), std::forward<
decltype(a)>(a)...);
781 ex = std::current_exception();
786 auto cls = detail::classify_async_exception(
787 ex, state->last_error, state->last_error_message);
788 if (cls.kind == detail::AsyncFailureKind::Cancellation) {