tigerbeetle-node 0.11.0 → 0.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.client.node.sha256 +1 -0
- package/package.json +5 -3
- package/src/tigerbeetle/scripts/fuzz_loop.sh +1 -1
- package/src/tigerbeetle/scripts/pre-commit.sh +2 -2
- package/src/tigerbeetle/scripts/validate_docs.sh +17 -0
- package/src/tigerbeetle/src/benchmark.zig +25 -11
- package/src/tigerbeetle/src/c/tb_client/context.zig +248 -47
- package/src/tigerbeetle/src/c/tb_client/echo_client.zig +108 -0
- package/src/tigerbeetle/src/c/tb_client/packet.zig +2 -2
- package/src/tigerbeetle/src/c/tb_client/signal.zig +2 -4
- package/src/tigerbeetle/src/c/tb_client/thread.zig +17 -256
- package/src/tigerbeetle/src/c/tb_client.h +18 -4
- package/src/tigerbeetle/src/c/tb_client.zig +88 -26
- package/src/tigerbeetle/src/c/tb_client_header_test.zig +135 -0
- package/src/tigerbeetle/src/c/test.zig +371 -1
- package/src/tigerbeetle/src/cli.zig +90 -18
- package/src/tigerbeetle/src/config.zig +12 -4
- package/src/tigerbeetle/src/demo.zig +2 -1
- package/src/tigerbeetle/src/demo_01_create_accounts.zig +1 -1
- package/src/tigerbeetle/src/demo_03_create_transfers.zig +13 -0
- package/src/tigerbeetle/src/ewah.zig +11 -33
- package/src/tigerbeetle/src/ewah_benchmark.zig +8 -9
- package/src/tigerbeetle/src/lsm/README.md +97 -3
- package/src/tigerbeetle/src/lsm/compaction.zig +32 -7
- package/src/tigerbeetle/src/{eytzinger_benchmark.zig → lsm/eytzinger_benchmark.zig} +34 -21
- package/src/tigerbeetle/src/lsm/forest_fuzz.zig +34 -32
- package/src/tigerbeetle/src/lsm/grid.zig +39 -21
- package/src/tigerbeetle/src/lsm/groove.zig +1 -0
- package/src/tigerbeetle/src/lsm/k_way_merge.zig +3 -3
- package/src/tigerbeetle/src/lsm/level_iterator.zig +1 -1
- package/src/tigerbeetle/src/lsm/manifest.zig +13 -0
- package/src/tigerbeetle/src/lsm/manifest_level.zig +0 -49
- package/src/tigerbeetle/src/lsm/manifest_log.zig +173 -335
- package/src/tigerbeetle/src/lsm/manifest_log_fuzz.zig +665 -0
- package/src/tigerbeetle/src/lsm/node_pool.zig +4 -0
- package/src/tigerbeetle/src/lsm/posted_groove.zig +1 -0
- package/src/tigerbeetle/src/lsm/segmented_array.zig +24 -15
- package/src/tigerbeetle/src/lsm/table.zig +32 -20
- package/src/tigerbeetle/src/lsm/table_immutable.zig +1 -1
- package/src/tigerbeetle/src/lsm/table_iterator.zig +4 -5
- package/src/tigerbeetle/src/lsm/test.zig +13 -2
- package/src/tigerbeetle/src/lsm/tree.zig +45 -7
- package/src/tigerbeetle/src/lsm/tree_fuzz.zig +36 -32
- package/src/tigerbeetle/src/main.zig +69 -13
- package/src/tigerbeetle/src/message_bus.zig +18 -7
- package/src/tigerbeetle/src/message_pool.zig +8 -2
- package/src/tigerbeetle/src/ring_buffer.zig +7 -3
- package/src/tigerbeetle/src/simulator.zig +38 -11
- package/src/tigerbeetle/src/state_machine.zig +48 -23
- package/src/tigerbeetle/src/test/accounting/workload.zig +9 -5
- package/src/tigerbeetle/src/test/cluster.zig +15 -33
- package/src/tigerbeetle/src/test/conductor.zig +2 -1
- package/src/tigerbeetle/src/test/network.zig +45 -19
- package/src/tigerbeetle/src/test/packet_simulator.zig +40 -29
- package/src/tigerbeetle/src/test/state_checker.zig +5 -7
- package/src/tigerbeetle/src/test/storage.zig +453 -110
- package/src/tigerbeetle/src/test/storage_checker.zig +204 -0
- package/src/tigerbeetle/src/tigerbeetle.zig +1 -0
- package/src/tigerbeetle/src/unit_tests.zig +7 -1
- package/src/tigerbeetle/src/util.zig +97 -11
- package/src/tigerbeetle/src/vopr.zig +2 -1
- package/src/tigerbeetle/src/vsr/client.zig +8 -3
- package/src/tigerbeetle/src/vsr/journal.zig +280 -202
- package/src/tigerbeetle/src/vsr/replica.zig +169 -31
- package/src/tigerbeetle/src/vsr/superblock.zig +356 -629
- package/src/tigerbeetle/src/vsr/superblock_client_table.zig +7 -6
- package/src/tigerbeetle/src/vsr/superblock_free_set.zig +414 -151
- package/src/tigerbeetle/src/vsr/superblock_free_set_fuzz.zig +332 -0
- package/src/tigerbeetle/src/vsr/superblock_fuzz.zig +349 -0
- package/src/tigerbeetle/src/vsr/superblock_manifest.zig +44 -9
- package/src/tigerbeetle/src/vsr/superblock_quorums.zig +394 -0
- package/src/tigerbeetle/src/vsr/superblock_quorums_fuzz.zig +312 -0
- package/src/tigerbeetle/src/vsr.zig +19 -5
- package/src/tigerbeetle/src/benchmark_array_search.zig +0 -317
- package/src/tigerbeetle/src/benchmarks/perf.zig +0 -299
- package/src/tigerbeetle/src/vopr_hub/README.md +0 -58
- package/src/tigerbeetle/src/vopr_hub/SETUP.md +0 -199
- package/src/tigerbeetle/src/vopr_hub/go.mod +0 -3
- package/src/tigerbeetle/src/vopr_hub/main.go +0 -1022
- package/src/tigerbeetle/src/vopr_hub/scheduler/go.mod +0 -3
- package/src/tigerbeetle/src/vopr_hub/scheduler/main.go +0 -403
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
//! Fuzz FreeSet reserve/acquire/release flow.
|
|
2
|
+
//!
|
|
3
|
+
//! This fuzzer does *not* cover FreeSet encoding/decoding.
|
|
4
|
+
const std = @import("std");
|
|
5
|
+
const assert = std.debug.assert;
|
|
6
|
+
const log = std.log.scoped(.fuzz_vsr_superblock_free_set);
|
|
7
|
+
|
|
8
|
+
const FreeSet = @import("./superblock_free_set.zig").FreeSet;
|
|
9
|
+
const Reservation = @import("./superblock_free_set.zig").Reservation;
|
|
10
|
+
const fuzz = @import("../test/fuzz.zig");
|
|
11
|
+
|
|
12
|
+
pub fn main() !void {
|
|
13
|
+
const allocator = std.testing.allocator;
|
|
14
|
+
const args = try fuzz.parse_fuzz_args(allocator);
|
|
15
|
+
|
|
16
|
+
var prng = std.rand.DefaultPrng.init(args.seed);
|
|
17
|
+
|
|
18
|
+
const blocks_count = FreeSet.shard_size * (1 + prng.random().uintLessThan(usize, 10));
|
|
19
|
+
const events = try generate_events(allocator, prng.random(), blocks_count);
|
|
20
|
+
defer allocator.free(events);
|
|
21
|
+
|
|
22
|
+
try run_fuzz(allocator, prng.random(), blocks_count, events);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
fn run_fuzz(
|
|
26
|
+
allocator: std.mem.Allocator,
|
|
27
|
+
random: std.rand.Random,
|
|
28
|
+
blocks_count: usize,
|
|
29
|
+
events: []const FreeSetEvent,
|
|
30
|
+
) !void {
|
|
31
|
+
var free_set = try FreeSet.init(allocator, blocks_count);
|
|
32
|
+
defer free_set.deinit(allocator);
|
|
33
|
+
|
|
34
|
+
var free_set_model = try FreeSetModel.init(allocator, blocks_count);
|
|
35
|
+
defer free_set_model.deinit(allocator);
|
|
36
|
+
|
|
37
|
+
var active_reservations = std.ArrayList(Reservation).init(allocator);
|
|
38
|
+
defer active_reservations.deinit();
|
|
39
|
+
|
|
40
|
+
var active_addresses = std.ArrayList(u64).init(allocator);
|
|
41
|
+
defer active_addresses.deinit();
|
|
42
|
+
|
|
43
|
+
for (events) |event| {
|
|
44
|
+
log.debug("event={}", .{event});
|
|
45
|
+
switch (event) {
|
|
46
|
+
.reserve => |reserve| {
|
|
47
|
+
const reservation_actual = free_set.reserve(reserve.blocks);
|
|
48
|
+
const reservation_expect = free_set_model.reserve(reserve.blocks);
|
|
49
|
+
assert(std.meta.eql(reservation_expect, reservation_actual));
|
|
50
|
+
|
|
51
|
+
if (reservation_expect) |reservation| {
|
|
52
|
+
assert(reserve.blocks == free_set_model.count_free_reserved(reservation));
|
|
53
|
+
assert(reserve.blocks == free_set.count_free_reserved(reservation));
|
|
54
|
+
try active_reservations.append(reservation);
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
.forfeit => {
|
|
58
|
+
random.shuffle(Reservation, active_reservations.items);
|
|
59
|
+
for (active_reservations.items) |reservation| {
|
|
60
|
+
free_set.forfeit(reservation);
|
|
61
|
+
free_set_model.forfeit(reservation);
|
|
62
|
+
}
|
|
63
|
+
active_reservations.clearRetainingCapacity();
|
|
64
|
+
},
|
|
65
|
+
.acquire => |data| {
|
|
66
|
+
if (active_reservations.items.len == 0) continue;
|
|
67
|
+
const reservation = active_reservations.items[
|
|
68
|
+
data.reservation % active_reservations.items.len
|
|
69
|
+
];
|
|
70
|
+
const address_actual = free_set.acquire(reservation);
|
|
71
|
+
const address_expect = free_set_model.acquire(reservation);
|
|
72
|
+
assert(std.meta.eql(address_expect, address_actual));
|
|
73
|
+
if (address_expect) |address| {
|
|
74
|
+
try active_addresses.append(address);
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
.release => |data| {
|
|
78
|
+
if (active_addresses.items.len == 0) continue;
|
|
79
|
+
|
|
80
|
+
const address_index = data.address % active_addresses.items.len;
|
|
81
|
+
const address = active_addresses.swapRemove(address_index);
|
|
82
|
+
free_set.release(address);
|
|
83
|
+
free_set_model.release(address);
|
|
84
|
+
},
|
|
85
|
+
.checkpoint => {
|
|
86
|
+
random.shuffle(Reservation, active_reservations.items);
|
|
87
|
+
for (active_reservations.items) |reservation| {
|
|
88
|
+
free_set.forfeit(reservation);
|
|
89
|
+
free_set_model.forfeit(reservation);
|
|
90
|
+
}
|
|
91
|
+
active_reservations.clearRetainingCapacity();
|
|
92
|
+
|
|
93
|
+
free_set.checkpoint();
|
|
94
|
+
free_set_model.checkpoint();
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
assert(free_set_model.count_reservations() == free_set.count_reservations());
|
|
99
|
+
assert(free_set_model.count_free() == free_set.count_free());
|
|
100
|
+
assert(free_set_model.count_acquired() == free_set.count_acquired());
|
|
101
|
+
assert(std.meta.eql(
|
|
102
|
+
free_set_model.highest_address_acquired(),
|
|
103
|
+
free_set.highest_address_acquired(),
|
|
104
|
+
));
|
|
105
|
+
|
|
106
|
+
for (active_reservations.items) |reservation| {
|
|
107
|
+
assert(
|
|
108
|
+
free_set_model.count_free_reserved(reservation) ==
|
|
109
|
+
free_set.count_free_reserved(reservation),
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const FreeSetEventType = std.meta.Tag(FreeSetEvent);
|
|
116
|
+
const FreeSetEvent = union(enum) {
|
|
117
|
+
reserve: struct { blocks: usize },
|
|
118
|
+
forfeit: void,
|
|
119
|
+
acquire: struct { reservation: usize },
|
|
120
|
+
release: struct { address: usize },
|
|
121
|
+
checkpoint: void,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
fn generate_events(
|
|
125
|
+
allocator: std.mem.Allocator,
|
|
126
|
+
random: std.rand.Random,
|
|
127
|
+
blocks_count: usize,
|
|
128
|
+
) ![]const FreeSetEvent {
|
|
129
|
+
const event_distribution = fuzz.Distribution(FreeSetEventType){
|
|
130
|
+
.reserve = 1 + random.float(f64) * 100,
|
|
131
|
+
.forfeit = 1,
|
|
132
|
+
.acquire = random.float(f64) * 1000,
|
|
133
|
+
.release = if (random.boolean()) 0 else 500 * random.float(f64),
|
|
134
|
+
.checkpoint = random.floatExp(f64) * 10,
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const events = try allocator.alloc(FreeSetEvent, std.math.min(
|
|
138
|
+
@as(usize, 2_000_000),
|
|
139
|
+
fuzz.random_int_exponential(random, usize, blocks_count * 100),
|
|
140
|
+
));
|
|
141
|
+
errdefer allocator.free(events);
|
|
142
|
+
|
|
143
|
+
log.info("event_distribution = {d:.2}", .{event_distribution});
|
|
144
|
+
log.info("event_count = {d}", .{events.len});
|
|
145
|
+
|
|
146
|
+
const reservation_blocks_mean = 1 + random.uintLessThan(usize, @divFloor(blocks_count, 20));
|
|
147
|
+
for (events) |*event| {
|
|
148
|
+
event.* = switch (fuzz.random_enum(random, FreeSetEventType, event_distribution)) {
|
|
149
|
+
.reserve => FreeSetEvent{ .reserve = .{
|
|
150
|
+
.blocks = 1 + fuzz.random_int_exponential(random, usize, reservation_blocks_mean),
|
|
151
|
+
} },
|
|
152
|
+
.forfeit => FreeSetEvent{ .forfeit = {} },
|
|
153
|
+
.acquire => FreeSetEvent{ .acquire = .{ .reservation = random.int(usize) } },
|
|
154
|
+
.release => FreeSetEvent{ .release = .{
|
|
155
|
+
.address = random.int(usize),
|
|
156
|
+
} },
|
|
157
|
+
.checkpoint => FreeSetEvent{ .checkpoint = {} },
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
return events;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const FreeSetModel = struct {
|
|
164
|
+
/// Set bits indicate acquired blocks.
|
|
165
|
+
blocks_acquired: std.DynamicBitSetUnmanaged,
|
|
166
|
+
|
|
167
|
+
/// Set bits indicate blocks that will be released at the next checkpoint.
|
|
168
|
+
blocks_released: std.DynamicBitSetUnmanaged,
|
|
169
|
+
|
|
170
|
+
/// Set bits indicate blocks that are currently reserved and not yet forfeited.
|
|
171
|
+
blocks_reserved: std.DynamicBitSetUnmanaged,
|
|
172
|
+
|
|
173
|
+
reservation_count: usize = 0,
|
|
174
|
+
reservation_session: usize = 1,
|
|
175
|
+
|
|
176
|
+
fn init(allocator: std.mem.Allocator, blocks_count: usize) !FreeSetModel {
|
|
177
|
+
var blocks_acquired = try std.DynamicBitSetUnmanaged.initEmpty(allocator, blocks_count);
|
|
178
|
+
errdefer blocks_acquired.deinit(allocator);
|
|
179
|
+
|
|
180
|
+
var blocks_released = try std.DynamicBitSetUnmanaged.initEmpty(allocator, blocks_count);
|
|
181
|
+
errdefer blocks_released.deinit(allocator);
|
|
182
|
+
|
|
183
|
+
var blocks_reserved = try std.DynamicBitSetUnmanaged.initEmpty(allocator, blocks_count);
|
|
184
|
+
errdefer blocks_reserved.deinit(allocator);
|
|
185
|
+
|
|
186
|
+
return FreeSetModel{
|
|
187
|
+
.blocks_acquired = blocks_acquired,
|
|
188
|
+
.blocks_released = blocks_released,
|
|
189
|
+
.blocks_reserved = blocks_reserved,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
fn deinit(set: *FreeSetModel, allocator: std.mem.Allocator) void {
|
|
194
|
+
set.blocks_acquired.deinit(allocator);
|
|
195
|
+
set.blocks_released.deinit(allocator);
|
|
196
|
+
set.blocks_reserved.deinit(allocator);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
pub fn count_reservations(set: FreeSetModel) usize {
|
|
200
|
+
return set.reservation_count;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
pub fn count_free(set: FreeSetModel) usize {
|
|
204
|
+
return set.blocks_acquired.capacity() - set.blocks_acquired.count();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
pub fn count_free_reserved(set: FreeSetModel, reservation: Reservation) usize {
|
|
208
|
+
assert(reservation.block_count > 0);
|
|
209
|
+
set.assert_reservation_active(reservation);
|
|
210
|
+
|
|
211
|
+
var count: usize = 0;
|
|
212
|
+
var iterator = set.blocks_acquired.iterator(.{ .kind = .unset });
|
|
213
|
+
while (iterator.next()) |block| {
|
|
214
|
+
if (block >= reservation.block_base and
|
|
215
|
+
block < reservation.block_base + reservation.block_count)
|
|
216
|
+
{
|
|
217
|
+
count += 1;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return count;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
pub fn count_acquired(set: FreeSetModel) usize {
|
|
224
|
+
return set.blocks_acquired.count();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
pub fn highest_address_acquired(set: FreeSetModel) ?u64 {
|
|
228
|
+
const block = set.blocks_acquired.iterator(.{
|
|
229
|
+
.direction = .reverse,
|
|
230
|
+
}).next() orelse return null;
|
|
231
|
+
return block + 1;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
pub fn reserve(set: *FreeSetModel, reserve_count: usize) ?Reservation {
|
|
235
|
+
assert(reserve_count > 0);
|
|
236
|
+
|
|
237
|
+
var blocks_found_free: usize = 0;
|
|
238
|
+
var iterator = set.blocks_acquired.iterator(.{ .kind = .unset });
|
|
239
|
+
while (iterator.next()) |block| {
|
|
240
|
+
if (block < set.blocks_reserved.count()) {
|
|
241
|
+
assert(set.blocks_reserved.isSet(block));
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
blocks_found_free += 1;
|
|
246
|
+
if (blocks_found_free == reserve_count) {
|
|
247
|
+
const block_base = set.blocks_reserved.count();
|
|
248
|
+
const block_count = block + 1 - block_base;
|
|
249
|
+
|
|
250
|
+
var i: usize = 0;
|
|
251
|
+
while (i < block_count) : (i += 1) set.blocks_reserved.set(block_base + i);
|
|
252
|
+
|
|
253
|
+
set.reservation_count += 1;
|
|
254
|
+
return Reservation{
|
|
255
|
+
.block_base = block_base,
|
|
256
|
+
.block_count = block_count,
|
|
257
|
+
.session = set.reservation_session,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
pub fn forfeit(set: *FreeSetModel, reservation: Reservation) void {
|
|
265
|
+
set.assert_reservation_active(reservation);
|
|
266
|
+
set.reservation_count -= 1;
|
|
267
|
+
|
|
268
|
+
var i: usize = 0;
|
|
269
|
+
while (i < reservation.block_count) : (i += 1) {
|
|
270
|
+
set.blocks_reserved.unset(reservation.block_base + i);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (set.reservation_count == 0) {
|
|
274
|
+
set.reservation_session +%= 1;
|
|
275
|
+
assert(set.blocks_reserved.count() == 0);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
pub fn acquire(set: *FreeSetModel, reservation: Reservation) ?u64 {
|
|
280
|
+
assert(reservation.block_count > 0);
|
|
281
|
+
assert(reservation.block_base < set.blocks_acquired.capacity());
|
|
282
|
+
assert(reservation.session == set.reservation_session);
|
|
283
|
+
set.assert_reservation_active(reservation);
|
|
284
|
+
|
|
285
|
+
var iterator = set.blocks_acquired.iterator(.{ .kind = .unset });
|
|
286
|
+
while (iterator.next()) |block| {
|
|
287
|
+
if (block >= reservation.block_base and
|
|
288
|
+
block < reservation.block_base + reservation.block_count)
|
|
289
|
+
{
|
|
290
|
+
assert(!set.blocks_acquired.isSet(block));
|
|
291
|
+
set.blocks_acquired.set(block);
|
|
292
|
+
|
|
293
|
+
const address = block + 1;
|
|
294
|
+
return address;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
pub fn is_free(set: *FreeSetModel, address: u64) bool {
|
|
301
|
+
return !set.blocks_acquired.isSet(address - 1);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
pub fn release(set: *FreeSetModel, address: u64) void {
|
|
305
|
+
const block = address - 1;
|
|
306
|
+
set.blocks_released.set(block);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
pub fn checkpoint(set: *FreeSetModel) void {
|
|
310
|
+
assert(set.blocks_reserved.count() == 0);
|
|
311
|
+
|
|
312
|
+
var iterator = set.blocks_released.iterator(.{});
|
|
313
|
+
while (iterator.next()) |block| {
|
|
314
|
+
assert(set.blocks_released.isSet(block));
|
|
315
|
+
assert(set.blocks_acquired.isSet(block));
|
|
316
|
+
|
|
317
|
+
set.blocks_released.unset(block);
|
|
318
|
+
set.blocks_acquired.unset(block);
|
|
319
|
+
}
|
|
320
|
+
assert(set.blocks_released.count() == 0);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
fn assert_reservation_active(set: FreeSetModel, reservation: Reservation) void {
|
|
324
|
+
assert(set.reservation_count > 0);
|
|
325
|
+
assert(set.reservation_session == reservation.session);
|
|
326
|
+
|
|
327
|
+
var i: usize = 0;
|
|
328
|
+
while (i < reservation.block_count) : (i += 1) {
|
|
329
|
+
assert(set.blocks_reserved.isSet(reservation.block_base + i));
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
};
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
//! Fuzz SuperBlock open()/checkpoint()/view_change().
|
|
2
|
+
//!
|
|
3
|
+
//! Invariants checked:
|
|
4
|
+
//!
|
|
5
|
+
//! - Crashing during a checkpoint() or view_change().
|
|
6
|
+
//! - open() finds a quorum, even with the interference of disk faults.
|
|
7
|
+
//! - open()'s quorum never regresses.
|
|
8
|
+
//! - Calling checkpoint() and view_change() concurrently is safe.
|
|
9
|
+
//! - VSRState will not leak before the corresponding checkpoint()/view_change().
|
|
10
|
+
//! - Trailers will not leak before the corresponding checkpoint().
|
|
11
|
+
//! - view_change_in_progress() reports the correct state.
|
|
12
|
+
//!
|
|
13
|
+
const std = @import("std");
|
|
14
|
+
const assert = std.debug.assert;
|
|
15
|
+
const log = std.log.scoped(.fuzz_vsr_superblock);
|
|
16
|
+
|
|
17
|
+
const config = @import("../config.zig");
|
|
18
|
+
const util = @import("../util.zig");
|
|
19
|
+
const vsr = @import("../vsr.zig");
|
|
20
|
+
const Storage = @import("../test/storage.zig").Storage;
|
|
21
|
+
const MessagePool = @import("../message_pool.zig").MessagePool;
|
|
22
|
+
const superblock_zone_size = @import("superblock.zig").superblock_zone_size;
|
|
23
|
+
const data_file_size_min = @import("superblock.zig").data_file_size_min;
|
|
24
|
+
const VSRState = @import("superblock.zig").SuperBlockSector.VSRState;
|
|
25
|
+
const SuperBlockType = @import("superblock.zig").SuperBlockType;
|
|
26
|
+
const SuperBlock = SuperBlockType(Storage);
|
|
27
|
+
const fuzz = @import("../test/fuzz.zig");
|
|
28
|
+
|
|
29
|
+
/// Total calls to checkpoint() + view_change().
|
|
30
|
+
const transitions_count_total = 10;
|
|
31
|
+
const cluster = 0;
|
|
32
|
+
|
|
33
|
+
pub fn main() !void {
|
|
34
|
+
const allocator = std.testing.allocator;
|
|
35
|
+
const args = try fuzz.parse_fuzz_args(allocator);
|
|
36
|
+
|
|
37
|
+
try run_fuzz(allocator, args.seed);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
fn run_fuzz(allocator: std.mem.Allocator, seed: u64) !void {
|
|
41
|
+
var prng = std.rand.DefaultPrng.init(seed);
|
|
42
|
+
const random = prng.random();
|
|
43
|
+
|
|
44
|
+
const storage_options = .{
|
|
45
|
+
.seed = random.int(u64),
|
|
46
|
+
// SuperBlock's IO is all serial, so latencies never reorder reads/writes.
|
|
47
|
+
.read_latency_min = 1,
|
|
48
|
+
.read_latency_mean = 1,
|
|
49
|
+
.write_latency_min = 1,
|
|
50
|
+
.write_latency_mean = 1,
|
|
51
|
+
// Storage will never inject more faults than the superblock is able to recover from,
|
|
52
|
+
// so a 100% fault probability is allowed.
|
|
53
|
+
.read_fault_probability = 25 + random.uintLessThan(u8, 76),
|
|
54
|
+
.write_fault_probability = 25 + random.uintLessThan(u8, 76),
|
|
55
|
+
.crash_fault_probability = 50 + random.uintLessThan(u8, 51),
|
|
56
|
+
.faulty_superblock = true,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
var storage = try Storage.init(allocator, superblock_zone_size, storage_options);
|
|
60
|
+
defer storage.deinit(allocator);
|
|
61
|
+
|
|
62
|
+
var storage_verify = try Storage.init(allocator, superblock_zone_size, storage_options);
|
|
63
|
+
defer storage_verify.deinit(allocator);
|
|
64
|
+
|
|
65
|
+
var message_pool = try MessagePool.init(allocator, .replica);
|
|
66
|
+
defer message_pool.deinit(allocator);
|
|
67
|
+
|
|
68
|
+
var superblock = try SuperBlock.init(allocator, &storage, &message_pool);
|
|
69
|
+
defer superblock.deinit(allocator);
|
|
70
|
+
|
|
71
|
+
var superblock_verify = try SuperBlock.init(allocator, &storage_verify, &message_pool);
|
|
72
|
+
defer superblock_verify.deinit(allocator);
|
|
73
|
+
|
|
74
|
+
var sequence_states = Environment.SequenceStates.init(allocator);
|
|
75
|
+
defer sequence_states.deinit();
|
|
76
|
+
|
|
77
|
+
var env = Environment{
|
|
78
|
+
.sequence_states = sequence_states,
|
|
79
|
+
.superblock = &superblock,
|
|
80
|
+
.superblock_verify = &superblock_verify,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
try env.format();
|
|
84
|
+
while (env.pending.count() > 0) env.superblock.storage.tick();
|
|
85
|
+
|
|
86
|
+
env.open();
|
|
87
|
+
while (env.pending.count() > 0) env.superblock.storage.tick();
|
|
88
|
+
|
|
89
|
+
try env.verify();
|
|
90
|
+
assert(env.pending.count() == 0);
|
|
91
|
+
assert(env.latest_sequence == 1);
|
|
92
|
+
|
|
93
|
+
var transitions: usize = 0;
|
|
94
|
+
while (transitions < transitions_count_total or env.pending.count() > 0) {
|
|
95
|
+
if (transitions < transitions_count_total) {
|
|
96
|
+
// TODO bias the RNG
|
|
97
|
+
if (env.pending.count() == 0) {
|
|
98
|
+
transitions += 1;
|
|
99
|
+
if (random.boolean()) {
|
|
100
|
+
try env.checkpoint();
|
|
101
|
+
} else {
|
|
102
|
+
try env.view_change();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (env.pending.count() == 1 and random.uintLessThan(u8, 6) == 0) {
|
|
107
|
+
transitions += 1;
|
|
108
|
+
if (env.pending.contains(.view_change)) {
|
|
109
|
+
try env.checkpoint();
|
|
110
|
+
} else {
|
|
111
|
+
try env.view_change();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
assert(env.pending.count() > 0);
|
|
117
|
+
assert(env.pending.count() <= 2);
|
|
118
|
+
try env.tick();
|
|
119
|
+
|
|
120
|
+
// Trailers are only updated on-disk by checkpoint(), never view_change().
|
|
121
|
+
// Trailers must not be mutated while a checkpoint() is in progress.
|
|
122
|
+
if (!env.pending.contains(.checkpoint) and random.boolean()) {
|
|
123
|
+
const range = env.superblock.free_set.reserve(1).?;
|
|
124
|
+
_ = env.superblock.free_set.acquire(range).?;
|
|
125
|
+
env.superblock.free_set.forfeit(range);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const Environment = struct {
|
|
131
|
+
/// Track the expected value of parameters at a particular sequence.
|
|
132
|
+
/// Indexed by sequence.
|
|
133
|
+
const SequenceStates = std.ArrayList(struct {
|
|
134
|
+
vsr_state: VSRState,
|
|
135
|
+
/// Track the expected `checksum(free_set)`.
|
|
136
|
+
/// Note that this is a checksum of the decoded free set; it is not the same as
|
|
137
|
+
/// `SuperBlockSector.free_set_checksum`.
|
|
138
|
+
free_set: u128,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
sequence_states: SequenceStates,
|
|
142
|
+
|
|
143
|
+
superblock: *SuperBlock,
|
|
144
|
+
superblock_verify: *SuperBlock,
|
|
145
|
+
|
|
146
|
+
/// Verify that the working superblock after open() never regresses.
|
|
147
|
+
latest_sequence: u64 = 0,
|
|
148
|
+
latest_checksum: u128 = 0,
|
|
149
|
+
latest_parent: u128 = 0,
|
|
150
|
+
latest_vsr_state: VSRState = std.mem.zeroInit(VSRState, .{}),
|
|
151
|
+
|
|
152
|
+
context_format: SuperBlock.Context = undefined,
|
|
153
|
+
context_open: SuperBlock.Context = undefined,
|
|
154
|
+
context_checkpoint: SuperBlock.Context = undefined,
|
|
155
|
+
context_view_change: SuperBlock.Context = undefined,
|
|
156
|
+
context_verify: SuperBlock.Context = undefined,
|
|
157
|
+
|
|
158
|
+
// Set bits indicate pending operations.
|
|
159
|
+
pending: std.enums.EnumSet(SuperBlock.Context.Caller) = .{},
|
|
160
|
+
pending_verify: bool = false,
|
|
161
|
+
|
|
162
|
+
/// After every write to `superblock`'s storage, verify that the superblock can be opened,
|
|
163
|
+
/// and the quorum never regresses.
|
|
164
|
+
fn tick(env: *Environment) !void {
|
|
165
|
+
assert(env.pending.count() <= 2);
|
|
166
|
+
assert(env.superblock.storage.reads.len + env.superblock.storage.writes.len <= 1);
|
|
167
|
+
assert(!env.pending.contains(.format));
|
|
168
|
+
assert(!env.pending.contains(.open));
|
|
169
|
+
assert(!env.pending_verify);
|
|
170
|
+
assert(env.pending.contains(.view_change) == env.superblock.view_change_in_progress());
|
|
171
|
+
|
|
172
|
+
const write = env.superblock.storage.writes.peek();
|
|
173
|
+
env.superblock.storage.tick();
|
|
174
|
+
|
|
175
|
+
if (write) |w| {
|
|
176
|
+
if (w.done_at_tick <= env.superblock.storage.ticks) try env.verify();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/// Verify that the superblock will recover safely if the replica crashes immediately after
|
|
181
|
+
/// the most recent write.
|
|
182
|
+
fn verify(env: *Environment) !void {
|
|
183
|
+
assert(!env.pending_verify);
|
|
184
|
+
|
|
185
|
+
{
|
|
186
|
+
// Reset `superblock_verify` so that it can be reused.
|
|
187
|
+
env.superblock_verify.opened = false;
|
|
188
|
+
var free_set_iterator = env.superblock_verify.free_set.blocks.iterator(.{
|
|
189
|
+
.kind = .unset,
|
|
190
|
+
});
|
|
191
|
+
while (free_set_iterator.next()) |block_bit| {
|
|
192
|
+
const block_address = block_bit + 1;
|
|
193
|
+
env.superblock_verify.free_set.release(block_address);
|
|
194
|
+
}
|
|
195
|
+
env.superblock_verify.free_set.checkpoint();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Duplicate the `superblock`'s storage so it is not modified by `superblock_verify`'s
|
|
199
|
+
// repairs. Immediately reset() it to simulate a crash (potentially injecting additional
|
|
200
|
+
// faults for pending writes) and clear the read/write queues.
|
|
201
|
+
env.superblock_verify.storage.copy(env.superblock.storage);
|
|
202
|
+
env.superblock_verify.storage.reset();
|
|
203
|
+
env.superblock_verify.open(verify_callback, &env.context_verify);
|
|
204
|
+
|
|
205
|
+
env.pending_verify = true;
|
|
206
|
+
while (env.pending_verify) env.superblock_verify.storage.tick();
|
|
207
|
+
|
|
208
|
+
assert(env.superblock_verify.working.checksum == env.superblock.working.checksum or
|
|
209
|
+
env.superblock_verify.working.checksum == env.superblock.staging.checksum);
|
|
210
|
+
|
|
211
|
+
// Verify the sequence we read from disk is monotonically increasing.
|
|
212
|
+
if (env.latest_sequence < env.superblock_verify.working.sequence) {
|
|
213
|
+
assert(env.latest_sequence + 1 == env.superblock_verify.working.sequence);
|
|
214
|
+
|
|
215
|
+
if (env.latest_checksum != 0) {
|
|
216
|
+
if (env.latest_sequence + 1 == env.superblock_verify.working.sequence) {
|
|
217
|
+
// After a checkpoint() or view_change(), the parent points to the previous
|
|
218
|
+
// working sector.
|
|
219
|
+
assert(env.superblock_verify.working.parent == env.latest_checksum);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
assert(env.latest_vsr_state.monotonic(env.superblock_verify.working.vsr_state));
|
|
224
|
+
|
|
225
|
+
const expect = env.sequence_states.items[env.superblock_verify.working.sequence];
|
|
226
|
+
assert(std.meta.eql(expect.vsr_state, env.superblock_verify.working.vsr_state));
|
|
227
|
+
assert(expect.free_set == checksum_free_set(env.superblock_verify));
|
|
228
|
+
|
|
229
|
+
env.latest_sequence = env.superblock_verify.working.sequence;
|
|
230
|
+
env.latest_checksum = env.superblock_verify.working.checksum;
|
|
231
|
+
env.latest_parent = env.superblock_verify.working.parent;
|
|
232
|
+
env.latest_vsr_state = env.superblock_verify.working.vsr_state;
|
|
233
|
+
} else {
|
|
234
|
+
assert(env.latest_sequence == env.superblock_verify.working.sequence);
|
|
235
|
+
assert(env.latest_checksum == env.superblock_verify.working.checksum);
|
|
236
|
+
assert(env.latest_parent == env.superblock_verify.working.parent);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
fn verify_callback(context: *SuperBlock.Context) void {
|
|
241
|
+
const env = @fieldParentPtr(Environment, "context_verify", context);
|
|
242
|
+
assert(env.pending_verify);
|
|
243
|
+
env.pending_verify = false;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
fn format(env: *Environment) !void {
|
|
247
|
+
assert(env.pending.count() == 0);
|
|
248
|
+
env.pending.insert(.format);
|
|
249
|
+
env.superblock.format(format_callback, &env.context_format, .{
|
|
250
|
+
.cluster = cluster,
|
|
251
|
+
.replica = 0,
|
|
252
|
+
// Include extra space (beyond what Storage actually needs) because SuperBlock will
|
|
253
|
+
// update the sector's size according to the highest FreeSet block allocated.
|
|
254
|
+
.size_max = data_file_size_min + 1000 * config.block_size,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
assert(env.sequence_states.items.len == 0);
|
|
258
|
+
try env.sequence_states.append(undefined); // skip sequence=0
|
|
259
|
+
try env.sequence_states.append(.{
|
|
260
|
+
.vsr_state = VSRState.root(cluster),
|
|
261
|
+
.free_set = checksum_free_set(env.superblock),
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
fn format_callback(context: *SuperBlock.Context) void {
|
|
266
|
+
const env = @fieldParentPtr(Environment, "context_format", context);
|
|
267
|
+
assert(env.pending.contains(.format));
|
|
268
|
+
env.pending.remove(.format);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
fn open(env: *Environment) void {
|
|
272
|
+
assert(env.pending.count() == 0);
|
|
273
|
+
env.pending.insert(.open);
|
|
274
|
+
env.superblock.open(open_callback, &env.context_open);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
fn open_callback(context: *SuperBlock.Context) void {
|
|
278
|
+
const env = @fieldParentPtr(Environment, "context_open", context);
|
|
279
|
+
assert(env.pending.contains(.open));
|
|
280
|
+
env.pending.remove(.open);
|
|
281
|
+
|
|
282
|
+
assert(env.superblock.working.sequence == 1);
|
|
283
|
+
assert(env.superblock.working.replica == 0);
|
|
284
|
+
assert(env.superblock.working.cluster == cluster);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
fn view_change(env: *Environment) !void {
|
|
288
|
+
assert(!env.pending.contains(.view_change));
|
|
289
|
+
assert(env.pending.count() < 2);
|
|
290
|
+
|
|
291
|
+
const vsr_state = .{
|
|
292
|
+
.commit_min_checksum = env.superblock.staging.vsr_state.commit_min_checksum,
|
|
293
|
+
.commit_min = env.superblock.staging.vsr_state.commit_min,
|
|
294
|
+
.commit_max = env.superblock.staging.vsr_state.commit_max + 3,
|
|
295
|
+
.view_normal = env.superblock.staging.vsr_state.view_normal + 4,
|
|
296
|
+
.view = env.superblock.staging.vsr_state.view + 5,
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
assert(env.sequence_states.items.len == env.superblock.staging.sequence + 1);
|
|
300
|
+
try env.sequence_states.append(.{
|
|
301
|
+
.vsr_state = vsr_state,
|
|
302
|
+
.free_set = env.sequence_states.items[env.sequence_states.items.len - 1].free_set,
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
env.pending.insert(.view_change);
|
|
306
|
+
env.superblock.view_change(view_change_callback, &env.context_view_change, vsr_state);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
fn view_change_callback(context: *SuperBlock.Context) void {
|
|
310
|
+
const env = @fieldParentPtr(Environment, "context_view_change", context);
|
|
311
|
+
assert(env.pending.contains(.view_change));
|
|
312
|
+
env.pending.remove(.view_change);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
fn checkpoint(env: *Environment) !void {
|
|
316
|
+
assert(!env.pending.contains(.checkpoint));
|
|
317
|
+
assert(env.pending.count() < 2);
|
|
318
|
+
|
|
319
|
+
const vsr_state = .{
|
|
320
|
+
.commit_min_checksum = env.superblock.staging.vsr_state.commit_min_checksum + 1,
|
|
321
|
+
.commit_min = env.superblock.staging.vsr_state.commit_min + 1,
|
|
322
|
+
.commit_max = env.superblock.staging.vsr_state.commit_max + 1,
|
|
323
|
+
.view_normal = env.superblock.staging.vsr_state.view_normal + 1,
|
|
324
|
+
.view = env.superblock.staging.vsr_state.view + 1,
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
assert(env.sequence_states.items.len == env.superblock.staging.sequence + 1);
|
|
328
|
+
try env.sequence_states.append(.{
|
|
329
|
+
.vsr_state = vsr_state,
|
|
330
|
+
.free_set = checksum_free_set(env.superblock),
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
env.pending.insert(.checkpoint);
|
|
334
|
+
env.superblock.checkpoint(checkpoint_callback, &env.context_checkpoint, vsr_state);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
fn checkpoint_callback(context: *SuperBlock.Context) void {
|
|
338
|
+
const env = @fieldParentPtr(Environment, "context_checkpoint", context);
|
|
339
|
+
assert(env.pending.contains(.checkpoint));
|
|
340
|
+
env.pending.remove(.checkpoint);
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
fn checksum_free_set(superblock: *const SuperBlock) u128 {
|
|
345
|
+
const mask_bits = @bitSizeOf(std.DynamicBitSetUnmanaged.MaskInt);
|
|
346
|
+
const count_bits = superblock.free_set.blocks.bit_length;
|
|
347
|
+
const count_words = util.div_ceil(count_bits, mask_bits);
|
|
348
|
+
return vsr.checksum(std.mem.sliceAsBytes(superblock.free_set.blocks.masks[0..count_words]));
|
|
349
|
+
}
|