Aria 3.2.6
C++23 MVVM framework — reactive, coroutine-first, ABI-layered
Loading...
Searching...
No Matches
virtual_time_executor.hpp
Go to the documentation of this file.
1#pragma once
2
3// VirtualTimeExecutor — deterministic time-based scheduler for tests.
4//
5// Production code uses real wall-clock executors (ThreadPoolExecutor /
6// SimpleDispatcher). Async tests that involve `debounce(300ms)` or
7// `retry_with_backoff(...)` would otherwise have to actually sleep, making the
8// test suite slow and flaky.
9//
10// VirtualTimeExecutor decouples "logical time" from wall-clock time. Tasks
11// scheduled with a delay sit in a sorted queue keyed by virtual deadline;
12// `advance_by(n)` jumps the clock forward and synchronously fires every task
13// whose deadline has passed. No real sleeping, no threads.
14//
15// Usage:
16//
17// VirtualTimeExecutor vt;
18// bool fired = false;
19// vt.post_after(500ms, [&]{ fired = true; });
20// vt.advance_by(499ms); CHECK(!fired);
21// vt.advance_by(1ms); CHECK(fired);
22//
23// Combined with `schedule_on` / `schedule_after` it produces fully
24// deterministic coroutine tests:
25//
26// Task<int> body(VirtualTimeExecutor& vt) {
27// co_await schedule_after(vt, 300ms);
28// co_return 42;
29// }
30// auto t = body(vt);
31// vt.advance_by(300ms);
32// CHECK(t.get() == 42);
33
35#include "aria/property_ops.hpp"
37
38#include <chrono>
39#include <coroutine>
40#include <cstdint>
41#include <functional>
42#include <mutex>
43#include <queue>
44#include <stdexcept>
45#include <vector>
46
47namespace aria::async {
48
50public:
51 using clock = std::chrono::steady_clock;
52 using duration = std::chrono::milliseconds;
53
59
61 decltype(queue_) retired;
62 {
63 std::scoped_lock lk(m_);
64 closed_ = true;
65 std::swap(queue_, retired);
66 }
67 // User captures can reenter post while the closed state is still live.
68 }
69
84 void schedule(std::function<void()> fn) override { post(std::move(fn)); }
85 void schedule_after(std::chrono::milliseconds delay,
86 std::function<void()> fn) override {
87 post_after(delay, std::move(fn));
88 }
89
92 void post(std::function<void()> fn) override {
93 post_after(duration{0}, std::move(fn));
94 }
95
98 void post_after(duration delay, std::function<void()> fn) override {
99 if (!fn) { return; }
100 std::lock_guard lk(m_);
101 if (closed_) { return; }
102 queue_.push(Entry{add_delay(now_, delay), ++seq_, std::move(fn)});
103 }
104
106 [[nodiscard]] duration now() const noexcept {
107 std::lock_guard lk(m_);
108 return now_;
109 }
110
112 [[nodiscard]] std::size_t pending() const noexcept {
113 std::lock_guard lk(m_);
114 return queue_.size();
115 }
116
119 std::size_t advance_by(duration delta) {
120 if (delta < duration::zero()) {
121 throw std::invalid_argument("VirtualTimeExecutor cannot move time backwards");
122 }
123 return advance_to(add_delay(now(), delta));
124 }
125
127 std::size_t advance_to(duration target) {
128 if (target < now()) {
129 throw std::invalid_argument("VirtualTimeExecutor cannot move time backwards");
130 }
131 std::size_t fired = 0;
132 while (true) {
133 std::function<void()> fn;
134 {
135 std::lock_guard lk(m_);
136 if (queue_.empty() || queue_.top().deadline > target) {
137 // A callback may itself advance time beyond our target.
138 if (target > now_) { now_ = target; }
139 break;
140 }
141 const auto deadline = queue_.top().deadline;
142 fn = std::move(queue_.top().fn);
143 queue_.pop();
144 if (deadline > now_) { now_ = deadline; }
145 }
146 // Run outside lock — task may schedule new tasks.
147 try {
148 fn();
149 } catch (...) {
151 std::string_view{"executor.virtual_time.advance"},
152 std::current_exception());
153 }
154 ++fired;
155 }
156 return fired;
157 }
158
162 std::size_t run_until_idle() {
163 std::size_t fired = 0;
164 while (true) {
165 std::function<void()> fn;
166 {
167 std::lock_guard lk(m_);
168 if (queue_.empty()) break;
169 const auto deadline = queue_.top().deadline;
170 fn = std::move(queue_.top().fn);
171 queue_.pop();
172 if (deadline > now_) { now_ = deadline; }
173 }
174 try {
175 fn();
176 } catch (...) {
178 std::string_view{"executor.virtual_time.run_until_idle"},
179 std::current_exception());
180 }
181 ++fired;
182 }
183 return fired;
184 }
185
187 void clear() noexcept {
188 std::priority_queue<Entry, std::vector<Entry>, Cmp> retired;
189 {
190 std::lock_guard lk(m_);
191 std::swap(queue_, retired);
192 }
193 // Capture destructors can safely schedule new work after unlocking.
194 }
195
196private:
197 static duration add_delay(duration now, duration delay) noexcept {
198 if (delay <= duration::zero()) { return now; }
199 return now > duration::max() - delay ? duration::max() : now + delay;
200 }
201
202 struct Entry {
203 duration deadline;
204 std::uint64_t seq; // tie-breaker so insertion order is preserved
205 mutable std::function<void()> fn;
206 };
207 struct Cmp {
208 bool operator()(const Entry& a, const Entry& b) const noexcept {
209 if (a.deadline != b.deadline) return a.deadline > b.deadline;
210 return a.seq > b.seq;
211 }
212 };
213
214 mutable std::mutex m_;
215 duration now_{0};
216 std::uint64_t seq_{0};
217 std::priority_queue<Entry, std::vector<Entry>, Cmp> queue_;
218 bool closed_ = false;
219};
220
226 struct Awaiter {
229 bool await_ready() const noexcept { return false; }
230 void await_suspend(std::coroutine_handle<> h) const {
231 vt.post_after(delay, [h]() mutable { h.resume(); });
232 }
233 void await_resume() const noexcept {}
234 };
235 return Awaiter{vt, delay};
236}
237
238} // namespace aria::async
Tiny interface — anything that can post a function to run after a delay.
Definition property_ops.hpp:62
Abstract executor interface — schedules a callable to run "somewhere".
Definition executor.hpp:37
Definition virtual_time_executor.hpp:49
aria::SchedulerCaps caps() const noexcept override
Capabilities: Post (immediate), Delay (deadline-keyed), GraphSafe.
Definition virtual_time_executor.hpp:77
duration now() const noexcept
Current virtual time (since construction).
Definition virtual_time_executor.hpp:106
~VirtualTimeExecutor() override
Definition virtual_time_executor.hpp:60
std::chrono::milliseconds duration
Definition virtual_time_executor.hpp:52
std::size_t advance_to(duration target)
Advance to an absolute virtual time target (must be >= now()).
Definition virtual_time_executor.hpp:127
VirtualTimeExecutor & operator=(VirtualTimeExecutor &&)=delete
void post_after(duration delay, std::function< void()> fn) override
Schedule to run after delay of virtual time.
Definition virtual_time_executor.hpp:98
void schedule_after(std::chrono::milliseconds delay, std::function< void()> fn) override
Submit fn for execution after delay.
Definition virtual_time_executor.hpp:85
std::size_t advance_by(duration delta)
Advance virtual time by delta, firing tasks in deadline order.
Definition virtual_time_executor.hpp:119
std::size_t pending() const noexcept
Number of scheduled tasks not yet fired.
Definition virtual_time_executor.hpp:112
std::chrono::steady_clock clock
Definition virtual_time_executor.hpp:51
void clear() noexcept
Drop all scheduled tasks without firing them.
Definition virtual_time_executor.hpp:187
void post(std::function< void()> fn) override
IExecutor: schedule "now" — runs at the current virtual time when the next advance_by()/run_until_idl...
Definition virtual_time_executor.hpp:92
VirtualTimeExecutor(const VirtualTimeExecutor &)=delete
VirtualTimeExecutor & operator=(const VirtualTimeExecutor &)=delete
std::size_t run_until_idle()
Run everything currently queued without advancing time beyond the last deadline.
Definition virtual_time_executor.hpp:162
void schedule(std::function< void()> fn) override
Submit fn for execution "soon". Defines Caps::Post.
Definition virtual_time_executor.hpp:84
VirtualTimeExecutor(VirtualTimeExecutor &&)=delete
Definition async_command.hpp:118
auto schedule_after(VirtualTimeExecutor &vt, VirtualTimeExecutor::duration delay)
Awaiter that resumes the coroutine after delay of virtual time.
Definition virtual_time_executor.hpp:224
void report_callback_failure(std::string_view category, std::exception_ptr exception, std::string_view message={}) noexcept
Report a callback failure.
SchedulerCaps
Definition scheduler.hpp:83
@ Post
Can submit "fire now" work.
Definition scheduler.hpp:89
@ GraphSafe
Safe to use as the graph-thread executor — i.e.
Definition scheduler.hpp:107
@ Pumpable
Posted work is held in a queue until a pump-style call drains it (e.g.
Definition scheduler.hpp:102
@ Delay
Can submit work after a wall-clock or virtual-time delay.
Definition scheduler.hpp:93
@ WorkerSafe
Safe to host blocking worker tasks (e.g.
Definition scheduler.hpp:111