|
3 | 3 | #ifndef IO_ENGINE_H
|
4 | 4 | #define IO_ENGINE_H
|
5 | 5 |
|
| 6 | +#include "base/atomic.hpp" |
| 7 | +#include "base/debug.hpp" |
6 | 8 | #include "base/exception.hpp"
|
7 | 9 | #include "base/lazy-init.hpp"
|
8 | 10 | #include "base/logger.hpp"
|
9 |
| -#include "base/shared-object.hpp" |
| 11 | +#include "base/shared.hpp" |
10 | 12 | #include <atomic>
|
11 | 13 | #include <exception>
|
12 | 14 | #include <memory>
|
@@ -163,51 +165,80 @@ class AsioConditionVariable
|
163 | 165 | /**
|
164 | 166 | * I/O timeout emulator
|
165 | 167 | *
|
| 168 | + * This class provides a workaround for Boost.ASIO's lack of built-in timeout support. |
| 169 | + * While Boost.ASIO handles asynchronous operations, it does not natively support timeouts for these operations. |
| 170 | + * This class uses a boost::asio::deadline_timer to emulate a timeout by scheduling a callback to be triggered |
| 171 | + * after a specified duration, effectively adding timeout behavior where none exists. |
| 172 | + * The callback is executed within the provided strand, ensuring thread-safety. |
| 173 | + * |
| 174 | + * The constructor returns immediately after scheduling the timeout callback. |
| 175 | + * The callback itself is invoked asynchronously when the timeout occurs. |
| 176 | + * This allows the caller to continue execution while the timeout is running in the background. |
| 177 | + * |
| 178 | + * The class provides a Cancel() method to unschedule any pending callback. If the callback has already been run, |
| 179 | + * calling Cancel() has no effect. This method can be used to abort the timeout early if the monitored operation |
| 180 | + * completes before the callback has been run. The Timeout destructor also automatically cancels any pending callback. |
| 181 | + * A callback is considered pending even if the timeout has already expired, |
| 182 | + * but the callback has not been executed yet due to a busy strand. |
| 183 | + * |
166 | 184 | * @ingroup base
|
167 | 185 | */
|
168 |
| -class Timeout : public SharedObject |
| 186 | +class Timeout |
169 | 187 | {
|
170 | 188 | public:
|
171 |
| - DECLARE_PTR_TYPEDEFS(Timeout); |
172 |
| - |
173 |
| - template<class Executor, class TimeoutFromNow, class OnTimeout> |
174 |
| - Timeout(boost::asio::io_context& io, Executor& executor, TimeoutFromNow timeoutFromNow, OnTimeout onTimeout) |
175 |
| - : m_Timer(io) |
| 189 | + using Timer = boost::asio::deadline_timer; |
| 190 | + |
| 191 | + /** |
| 192 | + * Schedules onTimeout to be triggered after timeoutFromNow on strand. |
| 193 | + * |
| 194 | + * @param strand The strand in which the callback will be executed. |
| 195 | + * The caller must also run in this strand, as well as Cancel() and the destructor! |
| 196 | + * @param timeoutFromNow The duration after which the timeout callback will be triggered. |
| 197 | + * @param onTimeout The callback to invoke when the timeout occurs. |
| 198 | + */ |
| 199 | + template<class OnTimeout> |
| 200 | + Timeout(boost::asio::io_context::strand& strand, const Timer::duration_type& timeoutFromNow, OnTimeout onTimeout) |
| 201 | + : m_Timer(strand.context(), timeoutFromNow), m_Cancelled(Shared<Atomic<bool>>::Make(false)) |
176 | 202 | {
|
177 |
| - Ptr keepAlive (this); |
178 |
| - |
179 |
| - m_Cancelled.store(false); |
180 |
| - m_Timer.expires_from_now(std::move(timeoutFromNow)); |
181 |
| - |
182 |
| - IoEngine::SpawnCoroutine(executor, [this, keepAlive, onTimeout](boost::asio::yield_context yc) { |
183 |
| - if (m_Cancelled.load()) { |
184 |
| - return; |
185 |
| - } |
| 203 | + VERIFY(strand.running_in_this_thread()); |
186 | 204 |
|
187 |
| - { |
188 |
| - boost::system::error_code ec; |
189 |
| - |
190 |
| - m_Timer.async_wait(yc[ec]); |
191 |
| - |
192 |
| - if (ec) { |
193 |
| - return; |
| 205 | + m_Timer.async_wait(boost::asio::bind_executor( |
| 206 | + strand, [cancelled = m_Cancelled, onTimeout = std::move(onTimeout)](boost::system::error_code ec) { |
| 207 | + if (!ec && !cancelled->load()) { |
| 208 | + onTimeout(); |
194 | 209 | }
|
195 | 210 | }
|
| 211 | + )); |
| 212 | + } |
196 | 213 |
|
197 |
| - if (m_Cancelled.load()) { |
198 |
| - return; |
199 |
| - } |
200 |
| - |
201 |
| - auto f (onTimeout); |
202 |
| - f(std::move(yc)); |
203 |
| - }); |
| 214 | + Timeout(const Timeout&) = delete; |
| 215 | + Timeout(Timeout&&) = delete; |
| 216 | + Timeout& operator=(const Timeout&) = delete; |
| 217 | + Timeout& operator=(Timeout&&) = delete; |
| 218 | + |
| 219 | + /** |
| 220 | + * Cancels any pending timeout callback. |
| 221 | + * |
| 222 | + * Must be called in the strand in which the callback was scheduled! |
| 223 | + */ |
| 224 | + ~Timeout() |
| 225 | + { |
| 226 | + Cancel(); |
204 | 227 | }
|
205 | 228 |
|
206 | 229 | void Cancel();
|
207 | 230 |
|
208 | 231 | private:
|
209 |
| - boost::asio::deadline_timer m_Timer; |
210 |
| - std::atomic<bool> m_Cancelled; |
| 232 | + Timer m_Timer; |
| 233 | + |
| 234 | + /** |
| 235 | + * Indicates whether the Timeout has been cancelled. |
| 236 | + * |
| 237 | + * This must be Shared<> between the lambda in the constructor and Cancel() for the case |
| 238 | + * the destructor calls Cancel() while the lambda is already queued in the strand. |
| 239 | + * The whole Timeout instance can't be kept alive by the lambda because this would delay the destructor. |
| 240 | + */ |
| 241 | + Shared<Atomic<bool>>::Ptr m_Cancelled; |
211 | 242 | };
|
212 | 243 |
|
213 | 244 | }
|
|
0 commit comments