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/ex/executor_ref.hpp>
 
15 +
#include <boost/capy/ex/io_env.hpp>
 
16 +
#include <boost/capy/ex/detail/timer_service.hpp>
 
17 +

 
18 +
#include <atomic>
 
19 +
#include <chrono>
 
20 +
#include <coroutine>
 
21 +
#include <new>
 
22 +
#include <stop_token>
 
23 +
#include <utility>
 
24 +

 
25 +
namespace boost {
 
26 +
namespace capy {
 
27 +

 
28 +
/** IoAwaitable returned by @ref delay.
 
29 +

 
30 +
    Suspends the calling coroutine until the deadline elapses
 
31 +
    or the environment's stop token is activated, whichever
 
32 +
    comes first. Resumption is always posted through the
 
33 +
    executor, never inline on the timer thread.
 
34 +

 
35 +
    Not intended to be named directly; use the @ref delay
 
36 +
    factory function instead.
 
37 +

 
38 +
    @par Cancellation
 
39 +

 
40 +
    If `stop_requested()` is true before suspension, the
 
41 +
    coroutine resumes immediately without scheduling a timer.
 
42 +
    If stop is requested while suspended, the stop callback
 
43 +
    claims the resume and posts it through the executor; the
 
44 +
    pending timer is cancelled on the next `await_resume` or
 
45 +
    destructor call.
 
46 +

 
47 +
    @par Thread Safety
 
48 +

 
49 +
    A single `delay_awaitable` must not be awaited concurrently.
 
50 +
    Multiple independent `delay()` calls on the same
 
51 +
    execution_context are safe and share one timer thread.
 
52 +

 
53 +
    @see delay, timeout
 
54 +
*/
 
55 +
class delay_awaitable
 
56 +
{
 
57 +
    std::chrono::nanoseconds dur_;
 
58 +

 
59 +
    detail::timer_service* ts_ = nullptr;
 
60 +
    detail::timer_service::timer_id tid_ = 0;
 
61 +

 
62 +
    // Declared before stop_cb_buf_: the callback
 
63 +
    // accesses these members, so they must still be
 
64 +
    // alive if the stop_cb_ destructor blocks.
 
65 +
    std::atomic<bool> claimed_{false};
 
66 +
    bool canceled_ = false;
 
67 +
    bool stop_cb_active_ = false;
 
68 +

 
69 +
    struct cancel_fn
 
70 +
    {
 
71 +
        delay_awaitable* self_;
 
72 +
        executor_ref ex_;
 
73 +
        std::coroutine_handle<> h_;
 
74 +

 
75 +
        void operator()() const noexcept
 
76 +
        {
 
77 +
            if(!self_->claimed_.exchange(
 
78 +
                true, std::memory_order_acq_rel))
 
79 +
            {
 
80 +
                self_->canceled_ = true;
 
81 +
                ex_.post(h_);
 
82 +
            }
 
83 +
        }
 
84 +
    };
 
85 +

 
86 +
    using stop_cb_t = std::stop_callback<cancel_fn>;
 
87 +

 
88 +
    // Aligned storage for the stop callback.
 
89 +
    // Declared last: its destructor may block while
 
90 +
    // the callback accesses the members above.
 
91 +
#ifdef _MSC_VER
 
92 +
# pragma warning(push)
 
93 +
# pragma warning(disable: 4324)
 
94 +
#endif
 
95 +
    alignas(stop_cb_t)
 
96 +
        unsigned char stop_cb_buf_[sizeof(stop_cb_t)];
 
97 +
#ifdef _MSC_VER
 
98 +
# pragma warning(pop)
 
99 +
#endif
 
100 +

 
101 +
    stop_cb_t& stop_cb_() noexcept
 
102 +
    {
 
103 +
        return *reinterpret_cast<stop_cb_t*>(stop_cb_buf_);
 
104 +
    }
 
105 +

 
106 +
public:
 
107 +
    explicit delay_awaitable(std::chrono::nanoseconds dur) noexcept
 
108 +
        : dur_(dur)
 
109 +
    {
 
110 +
    }
 
111 +

 
112 +
    /// @pre The stop callback must not be active
 
113 +
    ///      (i.e. the object has not yet been awaited).
 
114 +
    delay_awaitable(delay_awaitable&& o) noexcept
 
115 +
        : dur_(o.dur_)
 
116 +
        , ts_(o.ts_)
 
117 +
        , tid_(o.tid_)
 
118 +
        , claimed_(o.claimed_.load(std::memory_order_relaxed))
 
119 +
        , canceled_(o.canceled_)
 
120 +
        , stop_cb_active_(std::exchange(o.stop_cb_active_, false))
 
121 +
    {
 
122 +
    }
 
123 +

 
124 +
    ~delay_awaitable()
 
125 +
    {
 
126 +
        if(stop_cb_active_)
 
127 +
            stop_cb_().~stop_cb_t();
 
128 +
        if(ts_)
 
129 +
            ts_->cancel(tid_);
 
130 +
    }
 
131 +

 
132 +
    delay_awaitable(delay_awaitable const&) = delete;
 
133 +
    delay_awaitable& operator=(delay_awaitable const&) = delete;
 
134 +
    delay_awaitable& operator=(delay_awaitable&&) = delete;
 
135 +

 
136 +
    bool await_ready() const noexcept
 
137 +
    {
 
138 +
        return dur_.count() <= 0;
 
139 +
    }
 
140 +

 
141 +
    std::coroutine_handle<>
 
142 +
    await_suspend(
 
143 +
        std::coroutine_handle<> h,
 
144 +
        io_env const* env) noexcept
 
145 +
    {
 
146 +
        // Already stopped: resume immediately
 
147 +
        if(env->stop_token.stop_requested())
 
148 +
        {
 
149 +
            canceled_ = true;
 
150 +
            return h;
 
151 +
        }
 
152 +

 
153 +
        ts_ = &env->executor.context().use_service<detail::timer_service>();
 
154 +

 
155 +
        // Schedule timer (won't fire inline since deadline is in the future)
 
156 +
        tid_ = ts_->schedule_after(dur_,
 
157 +
            [this, h, ex = env->executor]()
 
158 +
            {
 
159 +
                if(!claimed_.exchange(
 
160 +
                    true, std::memory_order_acq_rel))
 
161 +
                {
 
162 +
                    ex.post(h);
 
163 +
                }
 
164 +
            });
 
165 +

 
166 +
        // Register stop callback (may fire inline)
 
167 +
        ::new(stop_cb_buf_) stop_cb_t(
 
168 +
            env->stop_token,
 
169 +
            cancel_fn{this, env->executor, h});
 
170 +
        stop_cb_active_ = true;
 
171 +

 
172 +
        return std::noop_coroutine();
 
173 +
    }
 
174 +

 
175 +
    void await_resume() noexcept
 
176 +
    {
 
177 +
        if(stop_cb_active_)
 
178 +
        {
 
179 +
            stop_cb_().~stop_cb_t();
 
180 +
            stop_cb_active_ = false;
 
181 +
        }
 
182 +
        if(ts_)
 
183 +
            ts_->cancel(tid_);
 
184 +
    }
 
185 +
};
 
186 +

 
187 +
/** Suspend the current coroutine for a duration.
 
188 +

 
189 +
    Returns an IoAwaitable that completes at or after the
 
190 +
    specified duration, or earlier if the environment's stop
 
191 +
    token is activated. Completion is always normal (void
 
192 +
    return); no exception is thrown on cancellation.
 
193 +

 
194 +
    Zero or negative durations complete synchronously without
 
195 +
    scheduling a timer.
 
196 +

 
197 +
    @par Example
 
198 +
    @code
 
199 +
    co_await delay(std::chrono::milliseconds(100));
 
200 +
    @endcode
 
201 +

 
202 +
    @param dur The duration to wait.
 
203 +

 
204 +
    @return A @ref delay_awaitable whose `await_resume`
 
205 +
        returns `void`.
 
206 +

 
207 +
    @throws Nothing.
 
208 +

 
209 +
    @see timeout, delay_awaitable
 
210 +
*/
 
211 +
template<typename Rep, typename Period>
 
212 +
delay_awaitable
 
213 +
delay(std::chrono::duration<Rep, Period> dur) noexcept
 
214 +
{
 
215 +
    return delay_awaitable{
 
216 +
        std::chrono::duration_cast<std::chrono::nanoseconds>(dur)};
 
217 +
}
 
218 +

 
219 +
} // capy
 
220 +
} // boost
 
221 +

 
222 +
#endif