ravel
Deterministic simulation testing for C++. Seed a bug, replay it exact.
Loading...
Searching...
No Matches
disk.hpp
1#pragma once
2
3#include <coroutine>
4#include <cstddef>
5#include <cstdint>
6#include <functional>
7#include <map>
8#include <string>
9#include <utility>
10#include <vector>
11
12#include "ravel/clock.hpp"
13#include "ravel/rng.hpp"
14#include "ravel/scheduler.hpp"
15#include "ravel/trace.hpp"
16
17namespace ravel {
18
19using DiskId = std::size_t;
20
21enum class DiskStatus {
22 Ok,
23 NoSpace,
24 IoError,
27 Crashed,
28 NotFound,
29};
30
31const char* to_string(DiskStatus status) noexcept;
32
34struct ReadResult {
36 DiskStatus status = DiskStatus::Ok;
37 std::string data;
38};
39
41struct ListResult {
43 DiskStatus status = DiskStatus::Ok;
44 std::vector<std::string> names;
45};
46
66
99class Disk {
100 public:
102 static constexpr std::size_t kSectorSize = 512;
103
105 Disk(DiskId id, std::string name, DiskFaultSpec fault, Scheduler& scheduler, VirtualRng& rng,
106 Trace& trace);
107
109 Disk(const Disk&) = delete;
110 Disk& operator=(const Disk&) = delete;
111
113 const std::string& name() const noexcept { return name_; }
115 const DiskFaultSpec& fault() const noexcept { return fault_; }
116
118 template <typename Result>
119 class Operation {
120 public:
121 bool await_ready() const noexcept { return false; }
122 void await_suspend(std::coroutine_handle<>) {
123 Scheduler& scheduler = disk_.scheduler_;
124 scheduler.call_after(disk_.draw_latency(), [this, task = scheduler.current_task()] {
125 result_ = finish_();
126 disk_.scheduler_.make_runnable(task);
127 });
128 }
129 Result await_resume() { return std::move(result_); }
130
131 private:
132 friend class Disk;
133 Operation(Disk& disk, std::function<Result()> finish)
134 : disk_(disk), finish_(std::move(finish)) {}
135
136 Disk& disk_;
137 std::function<Result()> finish_;
138 Result result_{};
139 };
140
143 [[nodiscard]] Operation<DiskStatus> write(std::string path, std::uint64_t offset,
144 std::string data);
145
148 [[nodiscard]] Operation<ReadResult> read(std::string path, std::uint64_t offset,
149 std::uint64_t length);
150
153 [[nodiscard]] Operation<DiskStatus> sync(std::string path);
154
156 [[nodiscard]] Operation<DiskStatus> rename(std::string from, std::string to);
157
159 [[nodiscard]] Operation<DiskStatus> remove(std::string path);
160
164 [[nodiscard]] Operation<DiskStatus> sync_dir(std::string dir);
165
168 [[nodiscard]] Operation<ListResult> list(std::string dir);
169
174 void crash();
175
179 std::uint64_t crash_count() const noexcept { return epoch_; }
180
182 std::uint64_t file_size(const std::string& path) const;
183
186 std::string durable_contents(const std::string& path) const;
187
189 bool durable_exists(const std::string& path) const;
190
192 std::uint64_t used_bytes() const;
193
194 private:
195 using InodeId = std::uint64_t;
196
197 struct PendingWrite {
198 std::uint64_t sequence;
199 std::uint64_t offset;
200 std::string data;
201 };
202
203 struct Inode {
204 std::string durable;
205 std::vector<PendingWrite> pending;
206 std::uint64_t logical_size = 0;
207 };
208
209 enum class NameChange { Create, Rename, Remove };
210
212 struct PendingNameChange {
213 std::uint64_t sequence;
214 NameChange kind;
215 std::string path;
216 std::string target;
217 InodeId inode;
218 };
219
220 using Names = std::map<std::string, InodeId>;
221
222 VirtualClock::Tick draw_latency() {
223 return rng_.next_between(fault_.latency_min, fault_.latency_max);
224 }
225 void record(TraceEventKind kind) { trace_.record({scheduler_.now(), id_, kind}); }
226
227 DiskStatus finish_write(const std::string& path, std::uint64_t offset, const std::string& data,
228 std::uint64_t epoch);
229 ReadResult finish_read(const std::string& path, std::uint64_t offset, std::uint64_t length,
230 std::uint64_t epoch);
231 DiskStatus finish_sync(const std::string& path, std::uint64_t horizon, std::uint64_t epoch);
232 DiskStatus finish_rename(const std::string& from, const std::string& to, std::uint64_t epoch);
233 DiskStatus finish_remove(const std::string& path, std::uint64_t epoch);
234 DiskStatus finish_sync_dir(const std::string& dir, std::uint64_t horizon, std::uint64_t epoch);
235 ListResult finish_list(const std::string& dir, std::uint64_t epoch);
236
238 void make_durable(std::size_t count);
239
240 static void apply(std::string& bytes, std::uint64_t offset, const std::string& data);
241 static void apply(Names& names, const PendingNameChange& change);
242 static void refresh_logical_size(Inode& inode);
243 static std::string parent_of(const std::string& path);
244
245 DiskId id_;
246 std::string name_;
247 DiskFaultSpec fault_;
248 Scheduler& scheduler_;
249 VirtualRng& rng_;
250 Trace& trace_;
251
252 std::map<InodeId, Inode> inodes_;
253 Names visible_names_;
254 Names durable_names_;
255 std::vector<PendingNameChange> pending_names_;
256 InodeId next_inode_ = 1;
257 std::uint64_t next_sequence_ = 0;
258 std::uint64_t epoch_ = 0;
259};
260
261} // namespace ravel
An operation in flight; co_await it to get its result.
Definition disk.hpp:119
A virtual disk holding named files, with the failure behavior that makes storage code hard to get rig...
Definition disk.hpp:99
Disk(const Disk &)=delete
Pending operations refer to this object, so it must never move.
Operation< DiskStatus > sync(std::string path)
Makes every write to the file that completed before this call was issued durable, and the file's crea...
std::uint64_t crash_count() const noexcept
How many times crash() has been called.
Definition disk.hpp:179
static constexpr std::size_t kSectorSize
A write torn by a crash is cut at a multiple of this many bytes.
Definition disk.hpp:102
void crash()
Power loss.
Disk(DiskId id, std::string name, DiskFaultSpec fault, Scheduler &scheduler, VirtualRng &rng, Trace &trace)
Created by Simulation::add_disk; you do not construct one yourself.
Operation< DiskStatus > rename(std::string from, std::string to)
Renames a file, replacing to if it exists. NotFound if from does not.
std::string durable_contents(const std::string &path) const
The file as a crash right now would leave it at best: only durable data, and empty unless the file's ...
bool durable_exists(const std::string &path) const
Whether the file's name would survive a crash right now.
Operation< DiskStatus > sync_dir(std::string dir)
Makes durable every directory change, made before this call was issued, up to the last one that touch...
Operation< ListResult > list(std::string dir)
The entries directly inside dir.
std::uint64_t file_size(const std::string &path) const
The file's size as reads see it (0 if it does not exist).
std::uint64_t used_bytes() const
Total bytes of all files, as reads see them.
const DiskFaultSpec & fault() const noexcept
The fault settings this disk was created with.
Definition disk.hpp:115
Operation< DiskStatus > write(std::string path, std::uint64_t offset, std::string data)
Writes data at offset, creating the file if it does not exist and growing it (with zero bytes) if nee...
Operation< DiskStatus > remove(std::string path)
Removes a file. NotFound if it does not exist.
const std::string & name() const noexcept
The name given to add_disk.
Definition disk.hpp:113
Operation< ReadResult > read(std::string path, std::uint64_t offset, std::uint64_t length)
Reads up to length bytes at offset, seeing every completed write.
Cooperative, single-threaded task scheduler.
Definition scheduler.hpp:65
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
TaskId current_task() const noexcept
The task being resumed right now. Valid only inside a task.
Definition scheduler.hpp:136
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.
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
std::uint64_t next_between(std::uint64_t low, std::uint64_t high)
Uniform in [low, high], both included. low must not exceed high.
Injectable disk behavior.
Definition disk.hpp:49
double sync_error_probability
In [0, 1].
Definition disk.hpp:64
VirtualClock::Tick latency_max
The upper end of the range above.
Definition disk.hpp:57
VirtualClock::Tick latency_min
Each operation takes a uniformly random time in [min, max].
Definition disk.hpp:55
std::uint64_t capacity_bytes
Total bytes across all files.
Definition disk.hpp:52
double write_error_probability
In [0, 1]. Writes, renames and removes fail with IoError, changing nothing.
Definition disk.hpp:60
The outcome of Disk::list.
Definition disk.hpp:41
std::vector< std::string > names
Sorted; a subdirectory is listed as "name/".
Definition disk.hpp:44
DiskStatus status
Ok, or why the listing failed.
Definition disk.hpp:43
The outcome of Disk::read.
Definition disk.hpp:34
std::string data
Shorter than asked if the read ran past the end of the file.
Definition disk.hpp:37
DiskStatus status
Ok, or why the read failed (for example NotFound).
Definition disk.hpp:36
How one run ended.
Definition simulation.hpp:56