ravel
Deterministic simulation testing for C++. Seed a bug, replay it exact.
Loading...
Searching...
No Matches
scheduler.hpp
1#pragma once
2
3#include <atomic>
4#include <coroutine>
5#include <cstddef>
6#include <cstdint>
7#include <deque>
8#include <exception>
9#include <functional>
10#include <limits>
11#include <queue>
12#include <string>
13#include <thread>
14#include <vector>
15
16#include "ravel/clock.hpp"
17#include "ravel/rng.hpp"
18#include "ravel/task.hpp"
19#include "ravel/trace.hpp"
20
21namespace ravel {
22
23using TaskId = std::size_t;
24
32using TaskFactory = std::function<Task()>;
33
36enum class RunStatus {
37 Completed,
38 TaskThrew,
39 StepLimitReached,
40 TimeLimitReached,
42};
43
45struct RunReport {
47 RunStatus status = RunStatus::Completed;
48 std::uint64_t steps = 0;
49 std::string failure;
50};
51
65class Scheduler {
66 public:
68 Scheduler(VirtualClock& clock, VirtualRng& rng, Trace& trace) noexcept
69 : clock_(clock), rng_(rng), trace_(trace) {}
70
72 Scheduler(const Scheduler&) = delete;
73 Scheduler& operator=(const Scheduler&) = delete;
74
77 void spawn(std::string name, TaskFactory factory);
78
79 // Awaitables for use inside a task:
80 // co_await scheduler.yield(); // let the scheduler pick who runs next
81 // co_await scheduler.sleep(50); // suspend for 50 virtual ticks
82
85 public:
86 bool await_ready() const noexcept { return false; }
87 void await_suspend(std::coroutine_handle<>) const { scheduler_.requeue_current_task(); }
88 void await_resume() const noexcept {}
89
90 private:
91 friend class Scheduler;
92 explicit YieldAwaiter(Scheduler& scheduler) noexcept : scheduler_(scheduler) {}
93 Scheduler& scheduler_;
94 };
95
98 public:
99 bool await_ready() const noexcept { return false; }
100 void await_suspend(std::coroutine_handle<>) const {
101 scheduler_.sleep_current_task(duration_);
102 }
103 void await_resume() const noexcept {}
104
105 private:
106 friend class Scheduler;
107 SleepAwaiter(Scheduler& scheduler, VirtualClock::Tick duration) noexcept
108 : scheduler_(scheduler), duration_(duration) {}
109 Scheduler& scheduler_;
110 VirtualClock::Tick duration_;
111 };
112
114 [[nodiscard]] YieldAwaiter yield() noexcept { return YieldAwaiter(*this); }
116 [[nodiscard]] SleepAwaiter sleep(VirtualClock::Tick duration) noexcept {
117 return SleepAwaiter(*this, duration);
118 }
119
123 std::uint64_t max_steps,
124 VirtualClock::Tick time_limit = std::numeric_limits<VirtualClock::Tick>::max());
125
127 const std::string& task_name(TaskId id) const { return slots_.at(id).name; }
128
129 // Building blocks for blocking primitives such as Channel; tasks rarely
130 // need them directly.
131
133 VirtualClock::Tick now() const noexcept { return clock_.now(); }
134
136 TaskId current_task() const noexcept { return current_task_; }
137
139 void make_runnable(TaskId id) { runnable_.push_back(id); }
140
143 void call_after(VirtualClock::Tick delay, std::function<void()> action);
144
145 private:
146 struct TaskSlot {
147 TaskSlot(std::string task_name, TaskFactory task_factory)
148 : name(std::move(task_name)), factory(std::move(task_factory)) {}
149
150 std::string name;
151 TaskFactory factory; // Declared before `task`: the coroutine may use it,
152 Task task; // so it must be destroyed after the coroutine.
153 };
154
155 struct Timer {
157 std::uint64_t sequence;
158 std::function<void()> action;
159 };
160
161 struct DueLater {
162 bool operator()(const Timer& a, const Timer& b) const noexcept {
163 return a.due != b.due ? a.due > b.due : a.sequence > b.sequence;
164 }
165 };
166
167 void requeue_current_task() { make_runnable(current_task_); }
168 void sleep_current_task(VirtualClock::Tick duration);
169
170 bool has_pending_work() const noexcept { return !runnable_.empty() || !timers_.empty(); }
171 void fire_earliest_timers();
172 TaskId take_random_runnable_task();
173
176 std::exception_ptr resume_task(TaskId id);
177
178 void record(TaskId task, TraceEventKind kind) {
179 trace_.record({clock_.now(), task, kind});
180 }
181
182 VirtualClock& clock_;
183 VirtualRng& rng_;
184 Trace& trace_;
185
188 std::deque<TaskSlot> slots_;
189 std::vector<TaskId> runnable_;
190 std::priority_queue<Timer, std::vector<Timer>, DueLater> timers_;
191 std::uint64_t next_timer_sequence_ = 0;
192 TaskId current_task_ = 0;
193
194 void require_running_thread(const char* operation) const;
195 std::atomic<std::thread::id> running_thread_{};
196};
197
198} // namespace ravel
What sleep() returns. It is only ever co_awaited.
Definition scheduler.hpp:97
What yield() returns. It is only ever co_awaited.
Definition scheduler.hpp:84
Cooperative, single-threaded task scheduler.
Definition scheduler.hpp:65
YieldAwaiter yield() noexcept
co_await scheduler.yield(); lets the scheduler pick who runs next.
Definition scheduler.hpp:114
void call_after(VirtualClock::Tick delay, std::function< void()> action)
Runs action once the clock reaches now() + delay.
VirtualClock::Tick now() const noexcept
The current virtual time.
Definition scheduler.hpp:133
Scheduler(VirtualClock &clock, VirtualRng &rng, Trace &trace) noexcept
Created by Simulation; you do not construct one yourself.
Definition scheduler.hpp:68
Scheduler(const Scheduler &)=delete
Tasks hold references into the scheduler, so it must never move.
TaskId current_task() const noexcept
The task being resumed right now. Valid only inside a task.
Definition scheduler.hpp:136
void spawn(std::string name, TaskFactory factory)
Registers a task.
RunReport run_until_quiescent(std::uint64_t max_steps, VirtualClock::Tick time_limit=std::numeric_limits< VirtualClock::Tick >::max())
Runs until nothing is left to happen, a task throws, max_steps steps have been taken,...
void make_runnable(TaskId id)
Lets a suspended task run again on a later step.
Definition scheduler.hpp:139
const std::string & task_name(TaskId id) const
The name a task was spawned with.
Definition scheduler.hpp:127
SleepAwaiter sleep(VirtualClock::Tick duration) noexcept
co_await scheduler.sleep(50); suspends the task for 50 virtual ticks.
Definition scheduler.hpp:116
The coroutine type for simulated tasks.
Definition task.hpp:19
Ordered record of everything that happened during a run (scheduling decisions and message fates),...
Definition trace.hpp:48
void record(const TraceEvent &event)
Appends an event and folds it into the digest.
Monotonic virtual time.
Definition clock.hpp:12
Tick now() const noexcept
The current virtual time.
Definition clock.hpp:18
std::uint64_t Tick
Virtual time, in ticks. A tick has no fixed real-world length.
Definition clock.hpp:15
The one source of randomness a Simulation may use.
Definition rng.hpp:28
How a Scheduler run ended.
Definition scheduler.hpp:45
std::uint64_t steps
How many times a task was resumed.
Definition scheduler.hpp:48
std::string failure
Human-readable cause; empty when Completed.
Definition scheduler.hpp:49
RunStatus status
Why the run ended.
Definition scheduler.hpp:47