100.00% Lines (54/54) 100.00% Functions (10/10)
TLA Baseline Branch
Line Hits Code Line Hits Code
  1 + //
  2 + // Copyright (c) 2026 Michael Vandeberg
  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_DELAY_HPP
  11 + #define BOOST_CAPY_DELAY_HPP
  12 +
  13 + #include <boost/capy/detail/config.hpp>
  14 + #include <boost/capy/continuation.hpp>
  15 + #include <boost/capy/error.hpp>
  16 + #include <boost/capy/ex/executor_ref.hpp>
  17 + #include <boost/capy/ex/io_env.hpp>
  18 + #include <boost/capy/ex/detail/timer_service.hpp>
  19 + #include <boost/capy/io_result.hpp>
  20 +
  21 + #include <atomic>
  22 + #include <chrono>
  23 + #include <coroutine>
  24 + #include <new>
  25 + #include <stop_token>
  26 + #include <utility>
  27 +
  28 + namespace boost {
  29 + namespace capy {
  30 +
  31 + /** IoAwaitable returned by @ref delay.
  32 +
  33 + Suspends the calling coroutine until the deadline elapses
  34 + or the environment's stop token is activated, whichever
  35 + comes first. Resumption is always posted through the
  36 + executor, never inline on the timer thread.
  37 +
  38 + Not intended to be named directly; use the @ref delay
  39 + factory function instead.
  40 +
  41 + @par Return Value
  42 +
  43 + Returns `io_result<>{}` (no error) when the timer fires
  44 + normally, or `io_result<>{error::canceled}` when
  45 + cancellation claims the resume before the deadline.
  46 +
  47 + @par Cancellation
  48 +
  49 + If `stop_requested()` is true before suspension, the
  50 + coroutine resumes immediately without scheduling a timer
  51 + and returns `io_result<>{error::canceled}`. If stop is
  52 + requested while suspended, the stop callback claims the
  53 + resume and posts it through the executor; the pending
  54 + timer is cancelled on the next `await_resume` or
  55 + destructor call.
  56 +
  57 + @par Thread Safety
  58 +
  59 + A single `delay_awaitable` must not be awaited concurrently.
  60 + Multiple independent `delay()` calls on the same
  61 + execution_context are safe and share one timer thread.
  62 +
  63 + @see delay, timeout
  64 + */
  65 + class delay_awaitable
  66 + {
  67 + std::chrono::nanoseconds dur_;
  68 +
  69 + detail::timer_service* ts_ = nullptr;
  70 + detail::timer_service::timer_id tid_ = 0;
  71 +
  72 + // Declared before stop_cb_buf_: the callback
  73 + // accesses these members, so they must still be
  74 + // alive if the stop_cb_ destructor blocks.
  75 + continuation cont_;
  76 + std::atomic<bool> claimed_{false};
  77 + bool canceled_ = false;
  78 + bool stop_cb_active_ = false;
  79 +
  80 + struct cancel_fn
  81 + {
  82 + delay_awaitable* self_;
  83 + executor_ref ex_;
  84 +
HITGNC   85 + 2 void operator()() const noexcept
  86 + {
HITGNC   87 + 2 if(!self_->claimed_.exchange(
  88 + true, std::memory_order_acq_rel))
  89 + {
HITGNC   90 + 2 self_->canceled_ = true;
HITGNC   91 + 2 ex_.post(self_->cont_);
  92 + }
HITGNC   93 + 2 }
  94 + };
  95 +
  96 + using stop_cb_t = std::stop_callback<cancel_fn>;
  97 +
  98 + // Aligned storage for the stop callback.
  99 + // Declared last: its destructor may block while
  100 + // the callback accesses the members above.
  101 + BOOST_CAPY_MSVC_WARNING_PUSH
  102 + BOOST_CAPY_MSVC_WARNING_DISABLE(4324)
  103 + alignas(stop_cb_t)
  104 + unsigned char stop_cb_buf_[sizeof(stop_cb_t)];
  105 + BOOST_CAPY_MSVC_WARNING_POP
  106 +
HITGNC   107 + 20 stop_cb_t& stop_cb_() noexcept
  108 + {
HITGNC   109 + 20 return *reinterpret_cast<stop_cb_t*>(stop_cb_buf_);
  110 + }
  111 +
  112 + public:
HITGNC   113 + 28 explicit delay_awaitable(std::chrono::nanoseconds dur) noexcept
HITGNC   114 + 28 : dur_(dur)
  115 + {
HITGNC   116 + 28 }
  117 +
  118 + /// @pre The stop callback must not be active
  119 + /// (i.e. the object has not yet been awaited).
HITGNC   120 + 43 delay_awaitable(delay_awaitable&& o) noexcept
HITGNC   121 + 43 : dur_(o.dur_)
HITGNC   122 + 43 , ts_(o.ts_)
HITGNC   123 + 43 , tid_(o.tid_)
HITGNC   124 + 43 , cont_(o.cont_)
HITGNC   125 + 43 , claimed_(o.claimed_.load(std::memory_order_relaxed))
HITGNC   126 + 43 , canceled_(o.canceled_)
HITGNC   127 + 43 , stop_cb_active_(std::exchange(o.stop_cb_active_, false))
  128 + {
HITGNC   129 + 43 }
  130 +
HITGNC   131 + 71 ~delay_awaitable()
  132 + {
HITGNC   133 + 71 if(stop_cb_active_)
HITGNC   134 + 2 stop_cb_().~stop_cb_t();
HITGNC   135 + 71 if(ts_)
HITGNC   136 + 20 ts_->cancel(tid_);
HITGNC   137 + 71 }
  138 +
  139 + delay_awaitable(delay_awaitable const&) = delete;
  140 + delay_awaitable& operator=(delay_awaitable const&) = delete;
  141 + delay_awaitable& operator=(delay_awaitable&&) = delete;
  142 +
HITGNC   143 + 27 bool await_ready() const noexcept
  144 + {
HITGNC   145 + 27 return dur_.count() <= 0;
  146 + }
  147 +
  148 + std::coroutine_handle<>
HITGNC   149 + 25 await_suspend(
  150 + std::coroutine_handle<> h,
  151 + io_env const* env) noexcept
  152 + {
  153 + // Already stopped: resume immediately
HITGNC   154 + 25 if(env->stop_token.stop_requested())
  155 + {
HITGNC   156 + 5 canceled_ = true;
HITGNC   157 + 5 return h;
  158 + }
  159 +
HITGNC   160 + 20 cont_.h = h;
HITGNC   161 + 20 ts_ = &env->executor.context().use_service<detail::timer_service>();
  162 +
  163 + // Schedule timer (won't fire inline since deadline is in the future)
HITGNC   164 + 20 tid_ = ts_->schedule_after(dur_,
HITGNC   165 + 20 [this, ex = env->executor]()
  166 + {
HITGNC   167 + 17 if(!claimed_.exchange(
  168 + true, std::memory_order_acq_rel))
  169 + {
HITGNC   170 + 17 ex.post(cont_);
  171 + }
HITGNC   172 + 17 });
  173 +
  174 + // Register stop callback (may fire inline)
HITGNC   175 + 60 ::new(stop_cb_buf_) stop_cb_t(
HITGNC   176 + 20 env->stop_token,
HITGNC   177 + 20 cancel_fn{this, env->executor});
HITGNC   178 + 20 stop_cb_active_ = true;
  179 +
HITGNC   180 + 20 return std::noop_coroutine();
  181 + }
  182 +
HITGNC   183 + 26 io_result<> await_resume() noexcept
  184 + {
HITGNC   185 + 26 if(stop_cb_active_)
  186 + {
HITGNC   187 + 18 stop_cb_().~stop_cb_t();
HITGNC   188 + 18 stop_cb_active_ = false;
  189 + }
HITGNC   190 + 26 if(ts_)
HITGNC   191 + 18 ts_->cancel(tid_);
HITGNC   192 + 26 if(canceled_)
HITGNC   193 + 6 return io_result<>{make_error_code(error::canceled)};
HITGNC   194 + 20 return io_result<>{};
  195 + }
  196 + };
  197 +
  198 + /** Suspend the current coroutine for a duration.
  199 +
  200 + Returns an IoAwaitable that completes at or after the
  201 + specified duration, or earlier if the environment's stop
  202 + token is activated.
  203 +
  204 + Zero or negative durations complete synchronously without
  205 + scheduling a timer.
  206 +
  207 + @par Example
  208 + @code
  209 + auto [ec] = co_await delay(std::chrono::milliseconds(100));
  210 + @endcode
  211 +
  212 + @param dur The duration to wait.
  213 +
  214 + @return A @ref delay_awaitable whose `await_resume`
  215 + returns `io_result<>`. On normal completion, `ec`
  216 + is clear. On cancellation, `ec == error::canceled`.
  217 +
  218 + @throws Nothing.
  219 +
  220 + @see timeout, delay_awaitable
  221 + */
  222 + template<typename Rep, typename Period>
  223 + delay_awaitable
HITGNC   224 + 27 delay(std::chrono::duration<Rep, Period> dur) noexcept
  225 + {
  226 + return delay_awaitable{
HITGNC   227 + 27 std::chrono::duration_cast<std::chrono::nanoseconds>(dur)};
  228 + }
  229 +
  230 + } // capy
  231 + } // boost
  232 +
  233 + #endif