28#include <asio/any_io_executor.hpp>
29#include <asio/awaitable.hpp>
30#include <asio/bind_cancellation_slot.hpp>
31#include <asio/cancellation_signal.hpp>
32#include <asio/co_spawn.hpp>
33#include <asio/detached.hpp>
34#include <asio/error.hpp>
35#include <asio/io_context.hpp>
36#include <asio/post.hpp>
37#include <asio/query.hpp>
38#include <asio/system_error.hpp>
39#include <asio/thread_pool.hpp>
40#include <asio/traits/static_query.hpp>
43#include <condition_variable>
51namespace neograph::async {
62 static thread_local const asio::execution_context* context =
nullptr;
72class SerialIoContextScope {
74 explicit SerialIoContextScope(
const asio::execution_context& context) noexcept
75 : previous_(serial_io_context()) {
76 serial_io_context() = &context;
78 ~SerialIoContextScope() { serial_io_context() = previous_; }
79 SerialIoContextScope(
const SerialIoContextScope&) =
delete;
80 SerialIoContextScope& operator=(
const SerialIoContextScope&) =
delete;
83 const asio::execution_context* previous_;
90class SyncIoQuiescence {
92 explicit SyncIoQuiescence(
const asio::execution_context& context) noexcept
93 : context_(&context) {}
95 ~SyncIoQuiescence() { wait(); }
97 void wait() noexcept {
99 if (!foreign_used_.load(std::memory_order_acquire))
return;
100 std::unique_lock lock(mutex_);
101 finished_.wait(lock, [
this] {
return active_ == 0; });
106 explicit Call(SyncIoQuiescence* owner) noexcept
109 std::lock_guard lock(owner_->mutex_);
111 owner_->foreign_used_.store(
true, std::memory_order_release);
117 std::lock_guard lock(owner_->mutex_);
118 if (--owner_->active_ == 0) {
121 owner_->finished_.notify_all();
126 Call(
const Call&) =
delete;
127 Call& operator=(
const Call&) =
delete;
130 SyncIoQuiescence* owner_;
134 const asio::execution_context* context_;
135 std::atomic<bool> foreign_used_{
false};
137 std::condition_variable finished_;
138 std::size_t active_ = 0;
145template <
typename Executor>
146class SyncIoExecutor {
148 SyncIoExecutor(Executor executor, SyncIoQuiescence& owner) noexcept
149 : executor_(std::move(executor)), owner_(&owner) {}
151 SyncIoExecutor(
const SyncIoExecutor&)
noexcept =
default;
152 SyncIoExecutor(SyncIoExecutor&& other) noexcept
153 : executor_(std::move(other.executor_)),
154 owner_(std::exchange(other.owner_,
nullptr)) {}
157 if constexpr (asio::traits::static_query<
158 Executor, asio::execution::outstanding_work_t>::value() ==
159 asio::execution::outstanding_work.tracked) {
160 SyncIoQuiescence::Call call(owner_);
163 auto released = std::move(executor_);
164 static_cast<void>(released);
168 SyncIoExecutor& operator=(SyncIoExecutor other)
noexcept {
170 swap(executor_, other.executor_);
171 swap(owner_, other.owner_);
175 template <
typename Function>
176 void execute(Function&& function)
const {
177 SyncIoQuiescence::Call call(owner_);
178 executor_.execute(std::forward<Function>(function));
181 template <
typename Property>
182 auto query(
const Property& property)
const
183 noexcept(
noexcept(asio::query(executor_, property)))
184 ->
decltype(asio::query(std::declval<const Executor&>(), property)) {
185 return asio::query(executor_, property);
188 template <
typename Property>
189 requires asio::can_require<const Executor&, const Property&>::value
190 auto require(
const Property& property)
const
191 -> SyncIoExecutor<std::decay_t<asio::require_result_t<const Executor&, const Property&>>> {
192 return {asio::require(executor_, property), *owner_};
195 template <
typename Property>
196 requires asio::can_prefer<const Executor&, const Property&>::value
197 auto prefer(
const Property& property)
const
198 -> SyncIoExecutor<std::decay_t<asio::prefer_result_t<const Executor&, const Property&>>> {
199 return {asio::prefer(executor_, property), *owner_};
202 friend bool operator==(
const SyncIoExecutor& left,
const SyncIoExecutor& right)
noexcept {
203 return left.owner_ == right.owner_ && left.executor_ == right.executor_;
206 friend bool operator!=(
const SyncIoExecutor& left,
const SyncIoExecutor& right)
noexcept {
207 return !(left == right);
212 SyncIoQuiescence* owner_;
220 explicit SyncIoRun(std::exception_ptr& operation_error)
221 : io_(std::in_place), quiescence_(*io_), serial_scope_(*io_),
222 operation_error_(operation_error) {}
227 cleanup_signal_.slot().clear();
232 auto executor() noexcept {
233 return SyncIoExecutor(io_->get_executor(), quiescence_);
236 asio::cancellation_slot cleanup_slot() noexcept {
237 return cleanup_signal_.slot();
240 template <
typename Executor>
241 void spawn(
const Executor& executor, asio::awaitable<void> body,
242 asio::cancellation_slot slot) {
245 asio::co_spawn(executor, std::move(body),
246 asio::bind_cancellation_slot(slot,
247 [
this](std::exception_ptr error) {
249 if (error && !operation_error_) {
250 operation_error_ = std::move(error);
255 remember(std::current_exception());
259 template <
typename Cancel>
260 std::size_t drain(Cancel&& cancel) {
262 std::size_t handled = 0;
264 handled = io_->run();
269 remember(std::current_exception());
276 remember(std::current_exception());
283 if (!started_ || completed_)
return handled;
290 std::size_t drain() {
291 return drain([
this] { cleanup_signal_.emit(asio::cancellation_type::all); });
296 cleanup_signal_.slot().clear();
309 }
while (drain() != 0);
312 if (error_) std::rethrow_exception(error_);
316 void remember(std::exception_ptr error)
noexcept {
317 if (error && !error_) error_ = std::move(error);
320 std::optional<asio::io_context> io_;
321 SyncIoQuiescence quiescence_;
322 SerialIoContextScope serial_scope_;
323 asio::cancellation_signal cleanup_signal_;
324 std::exception_ptr error_;
325 std::exception_ptr& operation_error_;
326 bool started_ =
false;
327 bool completed_ =
false;
338 asio::awaitable<T> aw,
339 std::shared_ptr<neograph::graph::CancelToken> operation) {
341 operation->throw_if_cancelled(
"run_sync operation entry");
344 std::optional<T> result;
345 std::exception_ptr err;
347 auto body = [&](std::shared_ptr<neograph::graph::CancelToken> execution)
348 -> asio::awaitable<void> {
350 if (execution) execution->throw_if_cancelled(
"run_sync operation execution entry");
351 result.emplace(
co_await std::move(aw));
353 err = std::current_exception();
358 const auto executor = run.executor();
363 neograph::graph::CancelExecutorLease operation_lease(operation);
364 const auto operation_executor = operation->bind_executor(executor);
365 auto execution = operation->fork();
366 neograph::graph::CancelExecutorLease execution_lease(execution);
367 const auto execution_executor = execution->bind_executor(operation_executor);
368 asio::post(execution_executor, [execution_executor, execution, &body, &run] {
369 run.spawn(execution_executor, body(execution), execution->slot());
371 run.drain([&execution] { execution->cancel(); });
373 run.spawn(executor, body(std::shared_ptr<neograph::graph::CancelToken>{}),
380 if (operation && operation->is_cancelled()) {
382 std::rethrow_exception(err);
383 }
catch (
const asio::system_error& error) {
384 if (error.code() == asio::error::operation_aborted) {
386 "run_sync operation aborted");
391 std::rethrow_exception(err);
393 if (!result && operation && operation->is_cancelled()) {
395 "run_sync operation aborted before entry");
397 return std::move(*result);
433 if (cancel && cancel->is_cancelled()) {
437 std::optional<T> result;
438 std::exception_ptr err;
440 auto body = [&](std::shared_ptr<neograph::graph::CancelToken> child)
441 -> asio::awaitable<void> {
443 if (child) child->throw_if_cancelled(
"run_sync execution entry");
444 result.emplace(
co_await std::move(aw));
446 err = std::current_exception();
450 detail::SyncIoRun run(err);
451 const auto executor = run.executor();
481 auto child = cancel->fork();
482 neograph::graph::CancelExecutorLease child_lease(child);
483 const auto child_executor = child->bind_executor(executor);
484 asio::post(child_executor, [child_executor, child, &body, &run] {
485 run.spawn(child_executor, body(child), child->slot());
487 run.drain([&child] { child->cancel(); });
492 run.spawn(executor, body(std::shared_ptr<neograph::graph::CancelToken>{}),
500 if (err && cancel && cancel->is_cancelled()) {
504 if (err) std::rethrow_exception(err);
505 return std::move(*result);
513 if (cancel && cancel->is_cancelled()) {
517 std::exception_ptr err;
519 auto body = [&](std::shared_ptr<neograph::graph::CancelToken> child)
520 -> asio::awaitable<void> {
522 if (child) child->throw_if_cancelled(
"run_sync execution entry");
523 co_await std::move(aw);
525 err = std::current_exception();
528 detail::SyncIoRun run(err);
529 const auto executor = run.executor();
535 auto child = cancel->fork();
536 neograph::graph::CancelExecutorLease child_lease(child);
537 const auto child_executor = child->bind_executor(executor);
538 asio::post(child_executor, [child_executor, child, &body, &run] {
539 run.spawn(child_executor, body(child), child->slot());
541 run.drain([&child] { child->cancel(); });
543 run.spawn(executor, body(std::shared_ptr<neograph::graph::CancelToken>{}),
549 if (err && cancel && cancel->is_cancelled()) {
553 if (err) std::rethrow_exception(err);
565 asio::thread_pool pool(n_threads > 0 ? n_threads : 1);
566 std::optional<T> result;
567 std::exception_ptr err;
569 auto body = [&]() -> asio::awaitable<void> {
571 result.emplace(
co_await std::move(aw));
573 err = std::current_exception();
577 asio::co_spawn(pool.get_executor(), body(), asio::detached);
581 if (err) std::rethrow_exception(err);
582 return std::move(*result);
587 asio::thread_pool pool(n_threads > 0 ? n_threads : 1);
588 std::exception_ptr err;
590 auto body = [&]() -> asio::awaitable<void> {
592 co_await std::move(aw);
594 err = std::current_exception();
597 asio::co_spawn(pool.get_executor(), body(), asio::detached);
601 if (err) std::rethrow_exception(err);
Cooperative cancellation primitive for graph runs.
Cooperative cancel primitive shared between caller and engine.
Thrown by the engine when a run is cancelled mid-flight via CancelToken::cancel().
const asio::execution_context *& serial_io_context() noexcept
The io_context an engine run is currently driving on this thread through run_sync_operation,...
bool on_serial_io_context(const asio::any_io_executor &executor) noexcept
Whether executor runs on the private io_context of a blocking bridge on this thread (run_sync,...
T run_sync(asio::awaitable< T > aw, neograph::graph::CancelToken *cancel=nullptr)
Run aw to completion on a fresh single-threaded io_context and return its result.
T run_sync_pool(asio::awaitable< T > aw, std::size_t n_threads)
Run aw to completion on a fresh N-worker asio::thread_pool and return its result.