Aria 2.0.0
C++23 MVVM framework (C++20 minimum) — 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 <vector>
45
46namespace aria::async {
47
49public:
50 using clock = std::chrono::steady_clock;
51 using duration = std::chrono::milliseconds;
52
54
69 void schedule(std::function<void()> fn) override { post(std::move(fn)); }
70 void schedule_after(std::chrono::milliseconds delay,
71 std::function<void()> fn) override {
72 post_after(delay, std::move(fn));
73 }
74
77 void post(std::function<void()> fn) override {
78 post_after(duration{0}, std::move(fn));
79 }
80
83 void post_after(duration delay, std::function<void()> fn) override {
84 std::lock_guard lk(m_);
85 queue_.push(Entry{now_ + delay, ++seq_, std::move(fn)});
86 }
87
89 [[nodiscard]] duration now() const noexcept {
90 std::lock_guard lk(m_);
91 return now_;
92 }
93
95 [[nodiscard]] std::size_t pending() const noexcept {
96 std::lock_guard lk(m_);
97 return queue_.size();
98 }
99
102 std::size_t advance_by(duration delta) {
103 return advance_to(now() + delta);
104 }
105
107 std::size_t advance_to(duration target) {
108 std::size_t fired = 0;
109 while (true) {
110 std::function<void()> fn;
111 {
112 std::lock_guard lk(m_);
113 if (queue_.empty() || queue_.top().deadline > target) {
114 now_ = target; // catch up the clock
115 break;
116 }
117 auto e = queue_.top();
118 queue_.pop();
119 now_ = e.deadline;
120 fn = std::move(e.fn);
121 }
122 // Run outside lock — task may schedule new tasks.
123 try {
124 fn();
125 } catch (...) {
127 std::string_view{"executor.virtual_time.advance"},
128 std::current_exception());
129 }
130 ++fired;
131 }
132 return fired;
133 }
134
138 std::size_t run_until_idle() {
139 std::size_t fired = 0;
140 while (true) {
141 std::function<void()> fn;
142 {
143 std::lock_guard lk(m_);
144 if (queue_.empty()) break;
145 auto e = queue_.top();
146 queue_.pop();
147 if (e.deadline > now_) now_ = e.deadline;
148 fn = std::move(e.fn);
149 }
150 try {
151 fn();
152 } catch (...) {
154 std::string_view{"executor.virtual_time.run_until_idle"},
155 std::current_exception());
156 }
157 ++fired;
158 }
159 return fired;
160 }
161
163 void clear() noexcept {
164 std::priority_queue<Entry, std::vector<Entry>, Cmp> retired;
165 {
166 std::lock_guard lk(m_);
167 std::swap(queue_, retired);
168 }
169 // Capture destructors can safely schedule new work after unlocking.
170 }
171
172private:
173 struct Entry {
174 duration deadline;
175 std::uint64_t seq; // tie-breaker so insertion order is preserved
176 std::function<void()> fn;
177 };
178 struct Cmp {
179 bool operator()(const Entry& a, const Entry& b) const noexcept {
180 if (a.deadline != b.deadline) return a.deadline > b.deadline;
181 return a.seq > b.seq;
182 }
183 };
184
185 mutable std::mutex m_;
186 duration now_{0};
187 std::uint64_t seq_{0};
188 std::priority_queue<Entry, std::vector<Entry>, Cmp> queue_;
189};
190
196 struct Awaiter {
199 bool await_ready() const noexcept { return false; }
200 void await_suspend(std::coroutine_handle<> h) const {
201 vt.post_after(delay, [h]() mutable { h.resume(); });
202 }
203 void await_resume() const noexcept {}
204 };
205 return Awaiter{vt, delay};
206}
207
208} // 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:36
Definition virtual_time_executor.hpp:48
aria::SchedulerCaps caps() const noexcept override
Capabilities: Post (immediate), Delay (deadline-keyed), GraphSafe.
Definition virtual_time_executor.hpp:62
duration now() const noexcept
Current virtual time (since construction).
Definition virtual_time_executor.hpp:89
std::chrono::milliseconds duration
Definition virtual_time_executor.hpp:51
std::size_t advance_to(duration target)
Advance to an absolute virtual time target (must be >= now()).
Definition virtual_time_executor.hpp:107
void post_after(duration delay, std::function< void()> fn) override
Schedule to run after delay of virtual time.
Definition virtual_time_executor.hpp:83
void schedule_after(std::chrono::milliseconds delay, std::function< void()> fn) override
Submit fn for execution after delay.
Definition virtual_time_executor.hpp:70
std::size_t advance_by(duration delta)
Advance virtual time by delta, firing tasks in deadline order.
Definition virtual_time_executor.hpp:102
std::size_t pending() const noexcept
Number of scheduled tasks not yet fired.
Definition virtual_time_executor.hpp:95
std::chrono::steady_clock clock
Definition virtual_time_executor.hpp:50
void clear() noexcept
Drop all scheduled tasks without firing them.
Definition virtual_time_executor.hpp:163
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:77
std::size_t run_until_idle()
Run everything currently queued without advancing time beyond the last deadline.
Definition virtual_time_executor.hpp:138
void schedule(std::function< void()> fn) override
Submit fn for execution "soon". Defines Caps::Post.
Definition virtual_time_executor.hpp:69
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:194
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