Line data Source code
1 : //
2 : // Copyright (c) 2025 Vinnie Falco (vinnie dot falco at gmail dot com)
3 : //
4 : // Distributed under the Boost Software License, Version 1.0. (See accompanying
5 : // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
6 : //
7 : // Official repository: https://github.com/cppalliance/capy
8 : //
9 :
10 : #ifndef BOOST_CAPY_TEST_RUN_BLOCKING_HPP
11 : #define BOOST_CAPY_TEST_RUN_BLOCKING_HPP
12 :
13 : #include <boost/capy/coro.hpp>
14 : #include <boost/capy/concept/execution_context.hpp>
15 : #include <boost/capy/concept/executor.hpp>
16 : #include <boost/capy/ex/run_async.hpp>
17 : #include <boost/capy/ex/system_context.hpp>
18 :
19 : #include <condition_variable>
20 : #include <exception>
21 : #include <mutex>
22 : #include <stdexcept>
23 : #include <stop_token>
24 : #include <type_traits>
25 : #include <utility>
26 :
27 : namespace boost {
28 : namespace capy {
29 : namespace test {
30 :
31 : struct inline_executor;
32 :
33 : /** Execution context for inline blocking execution.
34 :
35 : This execution context is used with inline_executor for
36 : blocking synchronous execution. It satisfies the
37 : ExecutionContext concept requirements.
38 :
39 : @see inline_executor
40 : @see run_blocking
41 : */
42 : class inline_context : public execution_context
43 : {
44 : public:
45 : using executor_type = inline_executor;
46 :
47 19 : inline_context() = default;
48 :
49 : executor_type
50 : get_executor() noexcept;
51 : };
52 :
53 : /** Synchronous executor that executes inline and disallows posting.
54 :
55 : This executor executes work synchronously on the calling thread
56 : via `dispatch()`. Calling `post()` throws `std::logic_error`
57 : because posting implies deferred execution which is incompatible
58 : with blocking synchronous semantics.
59 :
60 : @par Thread Safety
61 : All member functions are thread-safe.
62 :
63 : @see run_blocking
64 : */
65 : struct inline_executor
66 : {
67 : /// Compare two inline executors for equality.
68 : bool
69 1 : operator==(inline_executor const&) const noexcept
70 : {
71 1 : return true;
72 : }
73 :
74 : /** Return the associated execution context.
75 :
76 : @return A reference to a function-local static `inline_context`.
77 : */
78 : inline_context&
79 1541 : context() const noexcept
80 : {
81 1541 : static inline_context ctx;
82 1541 : return ctx;
83 : }
84 :
85 : /// Called when work is submitted (no-op).
86 0 : void on_work_started() const noexcept {}
87 :
88 : /// Called when work completes (no-op).
89 0 : void on_work_finished() const noexcept {}
90 :
91 : /** Dispatch work for immediate inline execution.
92 :
93 : @param h The coroutine handle to execute.
94 :
95 : @return The same handle for symmetric transfer.
96 : */
97 : coro
98 1542 : dispatch(coro h) const
99 : {
100 1542 : return h;
101 : }
102 :
103 : /** Post work for deferred execution.
104 :
105 : @par Exception Safety
106 : Always throws.
107 :
108 : @throws std::logic_error Always, because posting is not
109 : supported in a blocking context.
110 :
111 : @param h The coroutine handle (unused).
112 : */
113 : [[noreturn]] void
114 1 : post(coro) const
115 : {
116 : throw std::logic_error(
117 1 : "post not supported in blocking context");
118 : }
119 : };
120 :
121 : inline inline_context::executor_type
122 : inline_context::get_executor() noexcept
123 : {
124 : return inline_executor{};
125 : }
126 :
127 : static_assert(Executor<inline_executor>);
128 : static_assert(ExecutionContext<inline_context>);
129 :
130 : //----------------------------------------------------------
131 : //
132 : // blocking_state
133 : //
134 : //----------------------------------------------------------
135 :
136 : /** Synchronization state for blocking execution.
137 :
138 : Holds the mutex, condition variable, and completion flag
139 : used to block the caller until the task completes.
140 :
141 : @par Thread Safety
142 : Thread-safe when accessed under the mutex.
143 :
144 : @see run_blocking
145 : @see blocking_handler_wrapper
146 : */
147 : struct blocking_state
148 : {
149 : std::mutex mtx;
150 : std::condition_variable cv;
151 : bool done = false;
152 : std::exception_ptr ep;
153 : };
154 :
155 : //----------------------------------------------------------
156 : //
157 : // blocking_handler_wrapper
158 : //
159 : //----------------------------------------------------------
160 :
161 : /** Wrapper that signals completion after invoking the underlying handler_pair.
162 :
163 : This wrapper forwards invocations to the contained handler_pair,
164 : then signals the `blocking_state` condition variable so
165 : that `run_blocking` can unblock. Any exceptions thrown by the
166 : handler are captured and stored for later rethrow.
167 :
168 : @tparam H1 The success handler type.
169 : @tparam H2 The error handler type.
170 :
171 : @par Thread Safety
172 : Safe to invoke from any thread. Signals the condition
173 : variable after calling the handler.
174 :
175 : @see run_blocking
176 : @see blocking_state
177 : */
178 : template<class H1, class H2>
179 : struct blocking_handler_wrapper
180 : {
181 : blocking_state* state_;
182 : detail::handler_pair<H1, H2> handlers_;
183 :
184 : /** Invoke the handler with non-void result and signal completion. */
185 : template<class T>
186 21 : void operator()(T&& v)
187 : {
188 : try
189 : {
190 21 : handlers_(std::forward<T>(v));
191 : }
192 : catch(...)
193 : {
194 : std::lock_guard<std::mutex> lock(state_->mtx);
195 : state_->ep = std::current_exception();
196 : state_->done = true;
197 : state_->cv.notify_one();
198 : return;
199 : }
200 : {
201 21 : std::lock_guard<std::mutex> lock(state_->mtx);
202 21 : state_->done = true;
203 21 : }
204 21 : state_->cv.notify_one();
205 : }
206 :
207 : /** Invoke the handler for void result and signal completion. */
208 919 : void operator()()
209 : {
210 : try
211 : {
212 919 : handlers_();
213 : }
214 : catch(...)
215 : {
216 : std::lock_guard<std::mutex> lock(state_->mtx);
217 : state_->ep = std::current_exception();
218 : state_->done = true;
219 : state_->cv.notify_one();
220 : return;
221 : }
222 : {
223 919 : std::lock_guard<std::mutex> lock(state_->mtx);
224 919 : state_->done = true;
225 919 : }
226 919 : state_->cv.notify_one();
227 : }
228 :
229 : /** Invoke the handler with exception and signal completion. */
230 602 : void operator()(std::exception_ptr ep)
231 : {
232 : try
233 : {
234 1201 : handlers_(ep);
235 : }
236 1198 : catch(...)
237 : {
238 599 : std::lock_guard<std::mutex> lock(state_->mtx);
239 599 : state_->ep = std::current_exception();
240 599 : state_->done = true;
241 599 : state_->cv.notify_one();
242 599 : return;
243 599 : }
244 : {
245 3 : std::lock_guard<std::mutex> lock(state_->mtx);
246 3 : state_->done = true;
247 3 : }
248 3 : state_->cv.notify_one();
249 : }
250 : };
251 :
252 : //----------------------------------------------------------
253 : //
254 : // run_blocking_wrapper
255 : //
256 : //----------------------------------------------------------
257 :
258 : /** Wrapper returned by run_blocking that accepts a task for execution.
259 :
260 : This wrapper holds the blocking state and handlers. When invoked
261 : with a task, it launches the task via `run_async` and blocks
262 : until the task completes.
263 :
264 : The rvalue ref-qualifier on `operator()` ensures the wrapper
265 : can only be used as a temporary.
266 :
267 : @tparam Ex The executor type satisfying the `Executor` concept.
268 : @tparam H1 The success handler type.
269 : @tparam H2 The error handler type.
270 :
271 : @par Thread Safety
272 : The wrapper itself should only be used from one thread.
273 : The calling thread blocks until the task completes.
274 :
275 : @par Example
276 : @code
277 : // Block until task completes
278 : int result = 0;
279 : run_blocking([&](int v) { result = v; })(my_task());
280 : @endcode
281 :
282 : @see run_blocking
283 : @see run_async
284 : */
285 : template<Executor Ex, class H1, class H2>
286 : class [[nodiscard]] run_blocking_wrapper
287 : {
288 : Ex ex_;
289 : std::stop_token st_;
290 : H1 h1_;
291 : H2 h2_;
292 :
293 : public:
294 : /** Construct wrapper with executor, stop token, and handlers.
295 :
296 : @param ex The executor to execute the task on.
297 : @param st The stop token for cooperative cancellation.
298 : @param h1 The success handler.
299 : @param h2 The error handler.
300 : */
301 1542 : run_blocking_wrapper(
302 : Ex ex,
303 : std::stop_token st,
304 : H1 h1,
305 : H2 h2)
306 1542 : : ex_(std::move(ex))
307 1542 : , st_(std::move(st))
308 1542 : , h1_(std::move(h1))
309 1542 : , h2_(std::move(h2))
310 : {
311 1542 : }
312 :
313 : run_blocking_wrapper(run_blocking_wrapper const&) = delete;
314 : run_blocking_wrapper(run_blocking_wrapper&&) = delete;
315 : run_blocking_wrapper& operator=(run_blocking_wrapper const&) = delete;
316 : run_blocking_wrapper& operator=(run_blocking_wrapper&&) = delete;
317 :
318 : /** Launch the task and block until completion.
319 :
320 : This operator accepts a task, launches it via `run_async`
321 : with wrapped handlers, and blocks until the task completes.
322 :
323 : @tparam Task The IoLaunchableTask type.
324 :
325 : @param t The task to execute.
326 : */
327 : template<IoLaunchableTask Task>
328 : void
329 1542 : operator()(Task t) &&
330 : {
331 1542 : blocking_state state;
332 :
333 3084 : auto make_handlers = [&]() {
334 : if constexpr(std::is_same_v<H2, detail::default_handler>)
335 1539 : return detail::handler_pair<H1, H2>{std::move(h1_)};
336 : else
337 3 : return detail::handler_pair<H1, H2>{std::move(h1_), std::move(h2_)};
338 : };
339 :
340 : run_async(
341 : ex_,
342 1542 : st_,
343 1542 : blocking_handler_wrapper<H1, H2>{&state, make_handlers()}
344 1542 : )(std::move(t));
345 :
346 1542 : std::unique_lock<std::mutex> lock(state.mtx);
347 3084 : state.cv.wait(lock, [&] { return state.done; });
348 1542 : if(state.ep)
349 1198 : std::rethrow_exception(state.ep);
350 2141 : }
351 : };
352 :
353 : //----------------------------------------------------------
354 : //
355 : // run_blocking Overloads
356 : //
357 : //----------------------------------------------------------
358 :
359 : // With inline_executor (default)
360 :
361 : /** Block until task completes and discard result.
362 :
363 : Executes a lazy task using the inline executor and blocks the
364 : calling thread until the task completes or throws.
365 :
366 : @par Thread Safety
367 : The calling thread is blocked. The task executes inline
368 : on the calling thread.
369 :
370 : @par Exception Safety
371 : Basic guarantee. If the task throws, the exception is
372 : rethrown to the caller.
373 :
374 : @par Example
375 : @code
376 : run_blocking()(my_void_task());
377 : @endcode
378 :
379 : @return A wrapper that accepts a task for blocking execution.
380 :
381 : @see run_async
382 : */
383 : [[nodiscard]] inline auto
384 1518 : run_blocking()
385 : {
386 : return run_blocking_wrapper<
387 : inline_executor,
388 : detail::default_handler,
389 : detail::default_handler>(
390 : inline_executor{},
391 3036 : std::stop_token{},
392 : detail::default_handler{},
393 1518 : detail::default_handler{});
394 : }
395 :
396 : /** Block until task completes and invoke handler with result.
397 :
398 : Executes a lazy task using the inline executor and blocks until
399 : completion. The handler `h1` is called with the result on success.
400 : If `h1` is also invocable with `std::exception_ptr`, it handles
401 : exceptions too. Otherwise, exceptions are rethrown.
402 :
403 : @par Thread Safety
404 : The calling thread is blocked. The task and handler execute
405 : inline on the calling thread.
406 :
407 : @par Exception Safety
408 : Basic guarantee. Exceptions from the task are passed to `h1`
409 : if it accepts `std::exception_ptr`, otherwise rethrown.
410 :
411 : @par Example
412 : @code
413 : int result = 0;
414 : run_blocking([&](int v) { result = v; })(compute_value());
415 : @endcode
416 :
417 : @param h1 Handler invoked with the result on success, and
418 : optionally with `std::exception_ptr` on failure.
419 :
420 : @return A wrapper that accepts a task for blocking execution.
421 :
422 : @see run_async
423 : */
424 : template<class H1>
425 : [[nodiscard]] auto
426 18 : run_blocking(H1 h1)
427 : {
428 : return run_blocking_wrapper<
429 : inline_executor,
430 : H1,
431 : detail::default_handler>(
432 : inline_executor{},
433 36 : std::stop_token{},
434 18 : std::move(h1),
435 18 : detail::default_handler{});
436 : }
437 :
438 : /** Block until task completes with separate handlers.
439 :
440 : Executes a lazy task using the inline executor and blocks until
441 : completion. The handler `h1` is called on success, `h2` on failure.
442 :
443 : @par Thread Safety
444 : The calling thread is blocked. The task and handlers execute
445 : inline on the calling thread.
446 :
447 : @par Exception Safety
448 : Basic guarantee. Exceptions from the task are passed to `h2`.
449 :
450 : @par Example
451 : @code
452 : int result = 0;
453 : run_blocking(
454 : [&](int v) { result = v; },
455 : [](std::exception_ptr ep) {
456 : std::rethrow_exception(ep);
457 : }
458 : )(compute_value());
459 : @endcode
460 :
461 : @param h1 Handler invoked with the result on success.
462 : @param h2 Handler invoked with the exception on failure.
463 :
464 : @return A wrapper that accepts a task for blocking execution.
465 :
466 : @see run_async
467 : */
468 : template<class H1, class H2>
469 : [[nodiscard]] auto
470 3 : run_blocking(H1 h1, H2 h2)
471 : {
472 : return run_blocking_wrapper<
473 : inline_executor,
474 : H1,
475 : H2>(
476 : inline_executor{},
477 6 : std::stop_token{},
478 3 : std::move(h1),
479 6 : std::move(h2));
480 : }
481 :
482 : // With explicit executor
483 :
484 : /** Block until task completes on the given executor.
485 :
486 : Executes a lazy task on the specified executor and blocks the
487 : calling thread until the task completes.
488 :
489 : @par Thread Safety
490 : The calling thread is blocked. The task may execute on
491 : a different thread depending on the executor.
492 :
493 : @par Exception Safety
494 : Basic guarantee. If the task throws, the exception is
495 : rethrown to the caller.
496 :
497 : @par Example
498 : @code
499 : run_blocking(my_executor)(my_void_task());
500 : @endcode
501 :
502 : @param ex The executor to execute the task on.
503 :
504 : @return A wrapper that accepts a task for blocking execution.
505 :
506 : @see run_async
507 : */
508 : template<Executor Ex>
509 : [[nodiscard]] auto
510 : run_blocking(Ex ex)
511 : {
512 : return run_blocking_wrapper<
513 : Ex,
514 : detail::default_handler,
515 : detail::default_handler>(
516 : std::move(ex),
517 : std::stop_token{},
518 : detail::default_handler{},
519 : detail::default_handler{});
520 : }
521 :
522 : /** Block until task completes on executor with handler.
523 :
524 : Executes a lazy task on the specified executor and blocks until
525 : completion. The handler `h1` is called with the result.
526 :
527 : @par Thread Safety
528 : The calling thread is blocked. The task and handler may
529 : execute on a different thread depending on the executor.
530 :
531 : @par Exception Safety
532 : Basic guarantee. Exceptions from the task are passed to `h1`
533 : if it accepts `std::exception_ptr`, otherwise rethrown.
534 :
535 : @param ex The executor to execute the task on.
536 : @param h1 Handler invoked with the result on success.
537 :
538 : @return A wrapper that accepts a task for blocking execution.
539 :
540 : @see run_async
541 : */
542 : template<Executor Ex, class H1>
543 : [[nodiscard]] auto
544 1 : run_blocking(Ex ex, H1 h1)
545 : {
546 : return run_blocking_wrapper<
547 : Ex,
548 : H1,
549 : detail::default_handler>(
550 1 : std::move(ex),
551 2 : std::stop_token{},
552 1 : std::move(h1),
553 1 : detail::default_handler{});
554 : }
555 :
556 : /** Block until task completes on executor with separate handlers.
557 :
558 : Executes a lazy task on the specified executor and blocks until
559 : completion. The handler `h1` is called on success, `h2` on failure.
560 :
561 : @par Thread Safety
562 : The calling thread is blocked. The task and handlers may
563 : execute on a different thread depending on the executor.
564 :
565 : @par Exception Safety
566 : Basic guarantee. Exceptions from the task are passed to `h2`.
567 :
568 : @param ex The executor to execute the task on.
569 : @param h1 Handler invoked with the result on success.
570 : @param h2 Handler invoked with the exception on failure.
571 :
572 : @return A wrapper that accepts a task for blocking execution.
573 :
574 : @see run_async
575 : */
576 : template<Executor Ex, class H1, class H2>
577 : [[nodiscard]] auto
578 : run_blocking(Ex ex, H1 h1, H2 h2)
579 : {
580 : return run_blocking_wrapper<
581 : Ex,
582 : H1,
583 : H2>(
584 : std::move(ex),
585 : std::stop_token{},
586 : std::move(h1),
587 : std::move(h2));
588 : }
589 :
590 : // With stop_token
591 :
592 : /** Block until task completes with stop token support.
593 :
594 : Executes a lazy task using the inline executor with the given
595 : stop token and blocks until completion.
596 :
597 : @par Thread Safety
598 : The calling thread is blocked. The task executes inline
599 : on the calling thread.
600 :
601 : @par Exception Safety
602 : Basic guarantee. If the task throws, the exception is
603 : rethrown to the caller.
604 :
605 : @param st The stop token for cooperative cancellation.
606 :
607 : @return A wrapper that accepts a task for blocking execution.
608 :
609 : @see run_async
610 : */
611 : [[nodiscard]] inline auto
612 : run_blocking(std::stop_token st)
613 : {
614 : return run_blocking_wrapper<
615 : inline_executor,
616 : detail::default_handler,
617 : detail::default_handler>(
618 : inline_executor{},
619 : std::move(st),
620 : detail::default_handler{},
621 : detail::default_handler{});
622 : }
623 :
624 : /** Block until task completes with stop token and handler.
625 :
626 : @param st The stop token for cooperative cancellation.
627 : @param h1 Handler invoked with the result on success.
628 :
629 : @return A wrapper that accepts a task for blocking execution.
630 :
631 : @see run_async
632 : */
633 : template<class H1>
634 : [[nodiscard]] auto
635 2 : run_blocking(std::stop_token st, H1 h1)
636 : {
637 : return run_blocking_wrapper<
638 : inline_executor,
639 : H1,
640 : detail::default_handler>(
641 : inline_executor{},
642 2 : std::move(st),
643 2 : std::move(h1),
644 2 : detail::default_handler{});
645 : }
646 :
647 : /** Block until task completes with stop token and separate handlers.
648 :
649 : @param st The stop token for cooperative cancellation.
650 : @param h1 Handler invoked with the result on success.
651 : @param h2 Handler invoked with the exception on failure.
652 :
653 : @return A wrapper that accepts a task for blocking execution.
654 :
655 : @see run_async
656 : */
657 : template<class H1, class H2>
658 : [[nodiscard]] auto
659 : run_blocking(std::stop_token st, H1 h1, H2 h2)
660 : {
661 : return run_blocking_wrapper<
662 : inline_executor,
663 : H1,
664 : H2>(
665 : inline_executor{},
666 : std::move(st),
667 : std::move(h1),
668 : std::move(h2));
669 : }
670 :
671 : // Executor + stop_token
672 :
673 : /** Block until task completes on executor with stop token.
674 :
675 : @param ex The executor to execute the task on.
676 : @param st The stop token for cooperative cancellation.
677 :
678 : @return A wrapper that accepts a task for blocking execution.
679 :
680 : @see run_async
681 : */
682 : template<Executor Ex>
683 : [[nodiscard]] auto
684 : run_blocking(Ex ex, std::stop_token st)
685 : {
686 : return run_blocking_wrapper<
687 : Ex,
688 : detail::default_handler,
689 : detail::default_handler>(
690 : std::move(ex),
691 : std::move(st),
692 : detail::default_handler{},
693 : detail::default_handler{});
694 : }
695 :
696 : /** Block until task completes on executor with stop token and handler.
697 :
698 : @param ex The executor to execute the task on.
699 : @param st The stop token for cooperative cancellation.
700 : @param h1 Handler invoked with the result on success.
701 :
702 : @return A wrapper that accepts a task for blocking execution.
703 :
704 : @see run_async
705 : */
706 : template<Executor Ex, class H1>
707 : [[nodiscard]] auto
708 : run_blocking(Ex ex, std::stop_token st, H1 h1)
709 : {
710 : return run_blocking_wrapper<
711 : Ex,
712 : H1,
713 : detail::default_handler>(
714 : std::move(ex),
715 : std::move(st),
716 : std::move(h1),
717 : detail::default_handler{});
718 : }
719 :
720 : /** Block until task completes on executor with stop token and handlers.
721 :
722 : @param ex The executor to execute the task on.
723 : @param st The stop token for cooperative cancellation.
724 : @param h1 Handler invoked with the result on success.
725 : @param h2 Handler invoked with the exception on failure.
726 :
727 : @return A wrapper that accepts a task for blocking execution.
728 :
729 : @see run_async
730 : */
731 : template<Executor Ex, class H1, class H2>
732 : [[nodiscard]] auto
733 : run_blocking(Ex ex, std::stop_token st, H1 h1, H2 h2)
734 : {
735 : return run_blocking_wrapper<
736 : Ex,
737 : H1,
738 : H2>(
739 : std::move(ex),
740 : std::move(st),
741 : std::move(h1),
742 : std::move(h2));
743 : }
744 :
745 : } // namespace test
746 : } // namespace capy
747 : } // namespace boost
748 :
749 : #endif
|