NeoGraph 0.16.0
A C++20 Graph Agent Engine Library — LangGraph for C++
Loading...
Searching...
No Matches
store_call.h
Go to the documentation of this file.
1
12#pragma once
13
14#include <neograph/api.h>
16
17#include <asio/awaitable.hpp>
18#include <asio/co_spawn.hpp>
19#include <asio/io_context.hpp>
20#include <asio/use_awaitable.hpp>
21
22#include <exception>
23#include <functional>
24#include <memory>
25#include <optional>
26#include <string>
27#include <string_view>
28#include <type_traits>
29#include <utility>
30
31namespace neograph::async {
33NEOGRAPH_API bool in_host_turn() noexcept;
34} // namespace neograph::async
35
36namespace neograph::program {
37
47 std::function<std::string()> refuse;
48
54 bool await_synchronous_stores = false;
55};
56
57namespace detail {
58
65class NEOGRAPH_PROGRAM_API InlineStoreCalls {
66public:
67 InlineStoreCalls() noexcept;
69 InlineStoreCalls(const InlineStoreCalls&) = delete;
70 InlineStoreCalls& operator=(const InlineStoreCalls&) = delete;
71 static bool active() noexcept;
72};
73
75NEOGRAPH_PROGRAM_API bool offload_store_call(bool offloads) noexcept;
76
82class NEOGRAPH_PROGRAM_API BlockingIoContext {
83public:
86 BlockingIoContext(const BlockingIoContext&) = delete;
87 BlockingIoContext& operator=(const BlockingIoContext&) = delete;
88 asio::io_context& get() noexcept { return *io_; }
90 void discard() noexcept { reusable_ = false; }
91
92private:
93 std::unique_ptr<asio::io_context> io_;
94 bool reusable_ = true;
95};
96
102NEOGRAPH_PROGRAM_API asio::awaitable<void> offload_to_blocking_pool(std::function<void()> work);
103
108template <typename R, typename Fn>
109asio::awaitable<R> offload_sync_call(Fn fn) {
110 auto owned = std::make_shared<Fn>(std::move(fn));
111 if constexpr (std::is_void_v<R>) {
112 co_await offload_to_blocking_pool([owned] { (*owned)(); });
113 co_return;
114 } else {
115 auto result = std::make_shared<std::optional<R>>();
116 co_await offload_to_blocking_pool([owned, result] { result->emplace((*owned)()); });
117 co_return std::move(**result);
118 }
119}
120
122template <typename R>
123asio::awaitable<R> failed_store_call(std::exception_ptr error) {
124 std::rethrow_exception(std::move(error));
125 if constexpr (!std::is_void_v<R>) co_return R{}; // never reached; this is what makes the function a coroutine
126}
127
128template <typename R>
129asio::awaitable<std::optional<R>> box_store_call(asio::awaitable<R> call) {
130 co_return std::optional<R>(co_await std::move(call));
131}
132
133[[noreturn]] inline void refuse_blocking_store_call(std::string_view operation, std::string reason) {
134 throw ProgramBlockingCallRefused(std::string(operation), {}, std::move(reason));
135}
136
137inline void check_blocking_store_call(const BlockingStorePolicy& policy, std::string_view operation) {
138 if (policy.refuse)
139 if (auto reason = policy.refuse(); !reason.empty()) refuse_blocking_store_call(operation, std::move(reason));
140 // A view that has no policy (the catalog's, the module resolver's, the grant resolver's) still must not block the
141 // thread that turns a host's event loop.
142 if (async::in_host_turn())
143 refuse_blocking_store_call(operation,
144 "it is called inside a turn of a host's event loop, which a blocked thread cannot turn");
145}
146
149 try {
150 wait.get().run();
151 } catch (...) {
152 wait.discard();
153 throw;
154 }
155}
156
162template <typename R>
163R block_on_store(asio::awaitable<R> call, const BlockingStorePolicy& policy, std::string_view operation) {
164 check_blocking_store_call(policy, operation);
166 std::optional<R> value;
167 std::exception_ptr failure;
168 bool finished = false;
169 asio::co_spawn(wait.get(), box_store_call<R>(std::move(call)),
170 [&](std::exception_ptr error, std::optional<R> result) {
171 failure = std::move(error);
172 value = std::move(result);
173 finished = true;
174 });
175 run_blocking_wait(wait);
176 // run() returns only when the call is done (the context stays busy for as long as the coroutine exists), so this is
177 // a defence: an unfinished call is never reported as a result.
178 if (!finished) {
179 wait.discard();
180 refuse_blocking_store_call(operation, "the store call did not finish on this thread");
181 }
182 if (failure) std::rethrow_exception(failure);
183 return std::move(*value);
184}
185
186inline void block_on_store(asio::awaitable<void> call, const BlockingStorePolicy& policy, std::string_view operation) {
187 check_blocking_store_call(policy, operation);
188 BlockingIoContext wait;
189 std::exception_ptr failure;
190 bool finished = false;
191 asio::co_spawn(wait.get(), std::move(call), [&](std::exception_ptr error) {
192 failure = std::move(error);
193 finished = true;
194 });
195 run_blocking_wait(wait);
196 // run() returns only when the call is done (the context stays busy for as long as the coroutine exists), so this is
197 // a defence: an unfinished call is never reported as a result.
198 if (!finished) {
199 wait.discard();
200 refuse_blocking_store_call(operation, "the store call did not finish on this thread");
201 }
202 if (failure) std::rethrow_exception(failure);
203}
204
211template <typename R>
212R run_to_completion(asio::awaitable<R> call, std::string_view operation) {
213 InlineStoreCalls inline_calls;
215 std::optional<R> value;
216 std::exception_ptr failure;
217 bool finished = false;
218 asio::co_spawn(wait.get(), box_store_call<R>(std::move(call)),
219 [&](std::exception_ptr error, std::optional<R> result) {
220 failure = std::move(error);
221 value = std::move(result);
222 finished = true;
223 });
224 run_blocking_wait(wait);
225 if (!finished) {
226 wait.discard();
227 refuse_blocking_store_call(operation, "the call did not finish on this thread");
228 }
229 if (failure) std::rethrow_exception(failure);
230 return std::move(*value);
231}
232
233inline void run_to_completion(asio::awaitable<void> call, std::string_view operation) {
234 InlineStoreCalls inline_calls;
235 BlockingIoContext wait;
236 std::exception_ptr failure;
237 bool finished = false;
238 asio::co_spawn(wait.get(), std::move(call), [&](std::exception_ptr error) {
239 failure = std::move(error);
240 finished = true;
241 });
242 run_blocking_wait(wait);
243 if (!finished) {
244 wait.discard();
245 refuse_blocking_store_call(operation, "the call did not finish on this thread");
246 }
247 if (failure) std::rethrow_exception(failure);
248}
249
250} // namespace detail
251} // namespace neograph::program
NEOGRAPH_API export/import macro for shared-library builds.
Typed refusals of a Program call that would block a thread that must not block (#557).
The private io_context of one blocking wait.
Definition store_call.h:82
void discard() noexcept
Call when the wait ended without its coroutine finishing, or with an exception from the loop.
Definition store_call.h:90
While one is alive on a thread, a Sync...Store completes its awaitable operations on that thread even...
Definition store_call.h:65
NEOGRAPH_API bool in_host_turn() noexcept
True while the calling thread is running a turn of a HostLoop (#556).
asio::awaitable< R > failed_store_call(std::exception_ptr error)
An awaitable that throws error when it is awaited: an operation the store does not support.
Definition store_call.h:123
NEOGRAPH_PROGRAM_API bool offload_store_call(bool offloads) noexcept
Whether a Sync...Store whose offloads() is offloads moves its next operation to the pool.
void run_blocking_wait(BlockingIoContext &wait)
Runs the context until its coroutine is done; a context whose loop threw is not reused.
Definition store_call.h:148
R run_to_completion(asio::awaitable< R > call, std::string_view operation)
Runs call to completion on a private io_context of the calling thread, with InlineStoreCalls in force...
Definition store_call.h:212
asio::awaitable< R > offload_sync_call(Fn fn)
Runs the synchronous operation fn on a thread of the bounded pool and resumes the awaiting coroutine ...
Definition store_call.h:109
R block_on_store(asio::awaitable< R > call, const BlockingStorePolicy &policy, std::string_view operation)
Waits for call on a private io_context of the calling thread.
Definition store_call.h:163
NEOGRAPH_PROGRAM_API asio::awaitable< void > offload_to_blocking_pool(std::function< void()> work)
Runs work on a thread of a bounded pool and resumes the awaiting coroutine on its own executor.
When a Blocking...Store may wait for a store call that does not complete at once.
Definition store_call.h:39
std::function< std::string()> refuse
Called before the facade blocks its thread on a store that is not synchronous.
Definition store_call.h:47