run_with_timeout.hpp 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. //
  2. // Copyright (c) 2019-2024 Ruben Perez Hidalgo (rubenperez038 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. #ifndef BOOST_MYSQL_IMPL_INTERNAL_CONNECTION_POOL_RUN_WITH_TIMEOUT_HPP
  8. #define BOOST_MYSQL_IMPL_INTERNAL_CONNECTION_POOL_RUN_WITH_TIMEOUT_HPP
  9. #include <boost/mysql/client_errc.hpp>
  10. #include <boost/mysql/error_code.hpp>
  11. #include <boost/asio/any_io_executor.hpp>
  12. #include <boost/asio/associated_allocator.hpp>
  13. #include <boost/asio/bind_executor.hpp>
  14. #include <boost/asio/cancellation_signal.hpp>
  15. #include <chrono>
  16. #include <cstddef>
  17. #include <memory>
  18. #include <type_traits>
  19. #include <utility>
  20. // Runs a certain operation with a timeout. This is a lightweight replacement
  21. // for parallel_group, since the latter has bugs (https://github.com/chriskohlhoff/asio/issues/1397)
  22. // that make it unsuitable for us.
  23. namespace boost {
  24. namespace mysql {
  25. namespace detail {
  26. // Shared state, between the timer and the op.
  27. // Not thread-safe - should only be used within the pool's executor.
  28. template <class Timer, class Handler>
  29. struct run_with_timeout_state
  30. {
  31. using this_type = run_with_timeout_state<Timer, Handler>;
  32. // A cancellation signal to cancel the op if the timer fires first.
  33. asio::cancellation_signal op_signal;
  34. // The number of ops remaining. We won't call the handler until timer and op finish.
  35. std::size_t remaining;
  36. // The error code to call the handler with
  37. error_code final_ec;
  38. // The final handler
  39. Handler handler;
  40. // The timer that provides our timeout
  41. Timer& timer;
  42. run_with_timeout_state(Handler&& handler, Timer& timer)
  43. : remaining(2), handler(std::move(handler)), timer(timer)
  44. {
  45. }
  46. // Used by handlers. Ensures that memory is released before calling the handler
  47. static void complete_one_op(std::shared_ptr<this_type>&& ptr)
  48. {
  49. // All finished
  50. if (ptr->remaining == 0u)
  51. {
  52. // Save members required to call the handler
  53. auto h = std::move(ptr->handler);
  54. error_code ec = ptr->final_ec;
  55. // Free memory
  56. ptr.reset();
  57. // Call the handler
  58. std::move(h)(ec);
  59. }
  60. }
  61. // A specialized handler for the timer
  62. struct timer_handler
  63. {
  64. std::shared_ptr<this_type> st;
  65. void operator()(error_code ec)
  66. {
  67. // If the op has already completed, we don't care about the timer's result
  68. // Emitting the signal may call the handler inline, so we decrement first
  69. if (st->remaining-- == 2u)
  70. {
  71. st->final_ec = ec ? client_errc::cancelled : client_errc::timeout;
  72. st->op_signal.emit(asio::cancellation_type::terminal);
  73. }
  74. // Notify
  75. complete_one_op(std::move(st));
  76. }
  77. };
  78. // A specialized handler for the op. Ensures that the op is
  79. // run with the timer's executor and with the adequate cancellation slot
  80. struct op_handler
  81. {
  82. std::shared_ptr<this_type> st;
  83. void operator()(error_code ec)
  84. {
  85. // If the timer finished first, we don't care about the result
  86. if (st->remaining-- == 2u)
  87. {
  88. st->final_ec = ec;
  89. st->timer.cancel();
  90. }
  91. // Notify
  92. complete_one_op(std::move(st));
  93. }
  94. // Executor binding
  95. using executor_type = asio::any_io_executor;
  96. executor_type get_executor() const { return st->timer.get_executor(); }
  97. // Cancellation slot binding
  98. using cancellation_slot_type = asio::cancellation_slot;
  99. cancellation_slot_type get_cancellation_slot() const noexcept { return st->op_signal.slot(); }
  100. };
  101. };
  102. // Runs op in parallel with a timer. op must be a deferred operation with void(error_code) signature.
  103. // Handler must be a suitable completion handler. Arbitrary completion tokens are not supported.
  104. // Handler is called with the following error code:
  105. // - If the op finishes first, with op's error code.
  106. // - If the timer finishes first, without interruptions, with client_errc::timeout.
  107. // - If the timer finishes first because it was cancelled, with client_errc::cancelled.
  108. // Both op and timer are run within the timer's executor.
  109. // If timeout == 0, the timeout is disabled.
  110. template <class Op, class Timer, class Handler>
  111. void run_with_timeout(Op&& op, Timer& timer, std::chrono::steady_clock::duration timeout, Handler&& handler)
  112. {
  113. if (timeout.count() > 0)
  114. {
  115. using state_t = run_with_timeout_state<Timer, typename std::decay<Handler>::type>;
  116. // Allocate the shared state
  117. auto alloc = asio::get_associated_allocator(handler);
  118. using alloc_t = typename std::allocator_traits<decltype(alloc)>::template rebind_alloc<state_t>;
  119. auto st = std::allocate_shared<state_t>(alloc_t(alloc), std::move(handler), timer);
  120. // Launch the timer
  121. timer.expires_after(timeout);
  122. timer.async_wait(typename state_t::timer_handler{st});
  123. // Launch the op
  124. std::move(op)(typename state_t::op_handler{std::move(st)});
  125. }
  126. else
  127. {
  128. std::forward<Op>(op)(asio::bind_executor(timer.get_executor(), std::move(handler)));
  129. }
  130. }
  131. } // namespace detail
  132. } // namespace mysql
  133. } // namespace boost
  134. #endif