Lightweight 0.20260921.0
Loading...
Searching...
No Matches
Async.hpp
1// SPDX-License-Identifier: Apache-2.0
2#pragma once
3
5#include "Executor.hpp"
6#include "Task.hpp"
7
8#include <exception>
9#include <optional>
10#include <stdexcept>
11#include <stop_token>
12#include <type_traits>
13#include <utility>
14#include <variant>
15
16/// @defgroup Async Asynchronous API
17/// @brief C++23 coroutine API: tasks, executors and the offload backend.
18///
19/// Async entry points are added directly to the types you already use (@c SqlConnection,
20/// @c DataMapper, @c Pool), suffixed with @c Async. Note that this is a thread-offload model
21/// rather than protocol-level non-blocking I/O.
22
23namespace Lightweight::Async
24{
25
26namespace detail
27{
28
29 template <typename F>
30 using OffloadResult = std::invoke_result_t<F&>;
31
32 /// Awaitable that runs a blocking callable on an executor and resumes elsewhere.
33 ///
34 /// Lives in the awaiting coroutine's frame for the duration of the suspension, so the
35 /// worker thread may safely write the result/exception into it before resuming.
36 template <typename F>
37 class OffloadAwaitable
38 {
39 public:
40 using Result = OffloadResult<F>;
41
42 OffloadAwaitable(IExecutor& offload, IResumeScheduler& resume, F fn, std::stop_token token):
43 _offload { offload },
44 _resume { resume },
45 _fn { std::move(fn) },
46 _token { std::move(token) }
47 {
48 }
49
50 OffloadAwaitable(OffloadAwaitable&&) = default;
51 OffloadAwaitable(OffloadAwaitable const&) = delete;
52 OffloadAwaitable& operator=(OffloadAwaitable const&) = delete;
53 OffloadAwaitable& operator=(OffloadAwaitable&&) = delete;
54 ~OffloadAwaitable() = default;
55
56 [[nodiscard]] bool await_ready() const noexcept
57 {
58 return false;
59 }
60
61 void await_suspend(std::coroutine_handle<> awaiting)
62 {
63 // Honor cancellation *before* dispatching to the offload executor, so a pre-cancelled
64 // operation never occupies a DB worker at all (matching the "checked before the step is
65 // dispatched" contract). The cancellation is reported through the resume scheduler exactly
66 // as a normal completion would be, so the awaiting coroutine still resumes on the app thread.
67 if (_token.stop_requested())
68 {
69 _error = std::make_exception_ptr(OperationCancelledError {});
70 _resume.Resume(awaiting);
71 return;
72 }
73 _offload.Post([this, awaiting]() mutable {
74 Run();
75 _resume.Resume(awaiting);
76 });
77 }
78
79 Result await_resume()
80 {
81 if (_error)
82 std::rethrow_exception(_error);
83 if constexpr (!std::is_void_v<Result>)
84 {
85 // Run() always either emplaces _value or stores into _error; reaching here with a
86 // disengaged optional would be a broken invariant. The explicit check both documents
87 // that invariant and lets static analysis see the access as guarded.
88 if (!_value.has_value())
89 throw std::logic_error { "OffloadAwaitable: result missing without an error" };
90 return std::move(*_value);
91 }
92 }
93
94 private:
95 void Run() noexcept
96 {
97 try
98 {
99 if (_token.stop_requested())
100 throw OperationCancelledError {};
101 if constexpr (std::is_void_v<Result>)
102 _fn();
103 else
104 _value.emplace(_fn());
105 }
106 catch (...)
107 {
108 _error = std::current_exception();
109 }
110 }
111
112 IExecutor& _offload;
113 IResumeScheduler& _resume;
114 F _fn;
115 std::stop_token _token;
116 std::conditional_t<std::is_void_v<Result>, std::monostate, std::optional<Result>> _value {};
117 std::exception_ptr _error {};
118 };
119
120 /// Drives a by-value offload awaitable to completion.
121 ///
122 /// Taking the awaitable @b by value (rather than the executors by reference) keeps this
123 /// coroutine free of reference parameters; the executor references live inside the
124 /// awaitable, which is stored in this coroutine's frame for the duration of the await.
125 template <typename Awaitable>
126 Task<typename Awaitable::Result> RunOffloadTask(Awaitable awaitable)
127 {
128 if constexpr (std::is_void_v<typename Awaitable::Result>)
129 co_await awaitable;
130 else
131 co_return co_await awaitable;
132 }
133
134} // namespace detail
135
136/// Offloads a blocking callable to an executor and resumes the awaiting coroutine elsewhere.
137///
138/// @p fn runs on @p offload (typically a connection strand over the DB worker pool). When it
139/// finishes, the awaiting coroutine is resumed via @p resume (typically the app's run loop),
140/// so coroutine logic continues on the app thread while only the blocking call ran on a
141/// worker. Exceptions thrown by @p fn are captured and rethrown on resume (parity with the
142/// synchronous, throwing API). If cancellation is already requested when the work is about to
143/// run, the operation completes with @ref OperationCancelledError.
144///
145/// @tparam F A callable invocable with no arguments.
146/// @param offload The executor to run @p fn on.
147/// @param resume The scheduler used to resume the awaiting coroutine.
148/// @param fn The blocking callable (consumed).
149/// @param token Optional cancellation token (a default-constructed @c std::stop_token is non-cancellable).
150/// @return A Task producing @p fn's result.
151template <typename F>
152[[nodiscard]] Task<detail::OffloadResult<F>> Async(IExecutor& offload,
153 IResumeScheduler& resume,
154 F fn,
155 std::stop_token token = {})
156{
157 return detail::RunOffloadTask(detail::OffloadAwaitable<F> { offload, resume, std::move(fn), std::move(token) });
158}
159
160} // namespace Lightweight::Async