tigerbeetle-node 0.11.8 → 0.11.10
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 -1
- package/package.json +4 -3
- package/scripts/build_lib.sh +41 -0
- package/src/node.zig +1 -1
- package/src/tigerbeetle/scripts/validate_docs.sh +7 -1
- package/src/tigerbeetle/src/benchmark.zig +3 -3
- package/src/tigerbeetle/src/config.zig +31 -16
- package/src/tigerbeetle/src/constants.zig +48 -9
- package/src/tigerbeetle/src/ewah.zig +5 -5
- package/src/tigerbeetle/src/ewah_fuzz.zig +1 -1
- package/src/tigerbeetle/src/lsm/binary_search.zig +1 -1
- package/src/tigerbeetle/src/lsm/bloom_filter.zig +1 -1
- package/src/tigerbeetle/src/lsm/compaction.zig +34 -21
- package/src/tigerbeetle/src/lsm/forest_fuzz.zig +84 -104
- package/src/tigerbeetle/src/lsm/grid.zig +19 -13
- package/src/tigerbeetle/src/lsm/manifest_log.zig +8 -10
- package/src/tigerbeetle/src/lsm/manifest_log_fuzz.zig +18 -13
- package/src/tigerbeetle/src/lsm/merge_iterator.zig +1 -1
- package/src/tigerbeetle/src/lsm/segmented_array.zig +17 -17
- package/src/tigerbeetle/src/lsm/segmented_array_fuzz.zig +1 -1
- package/src/tigerbeetle/src/lsm/set_associative_cache.zig +1 -1
- package/src/tigerbeetle/src/lsm/table.zig +8 -20
- package/src/tigerbeetle/src/lsm/table_immutable.zig +1 -1
- package/src/tigerbeetle/src/lsm/table_iterator.zig +3 -3
- package/src/tigerbeetle/src/lsm/table_mutable.zig +14 -2
- package/src/tigerbeetle/src/lsm/test.zig +5 -4
- package/src/tigerbeetle/src/lsm/tree.zig +1 -2
- package/src/tigerbeetle/src/lsm/tree_fuzz.zig +85 -115
- package/src/tigerbeetle/src/message_bus.zig +4 -4
- package/src/tigerbeetle/src/message_pool.zig +7 -10
- package/src/tigerbeetle/src/ring_buffer.zig +22 -12
- package/src/tigerbeetle/src/simulator.zig +366 -239
- package/src/tigerbeetle/src/state_machine/auditor.zig +5 -5
- package/src/tigerbeetle/src/state_machine/workload.zig +3 -3
- package/src/tigerbeetle/src/state_machine.zig +190 -178
- package/src/tigerbeetle/src/{util.zig → stdx.zig} +2 -0
- package/src/tigerbeetle/src/storage.zig +13 -6
- package/src/tigerbeetle/src/{test → testing/cluster}/message_bus.zig +3 -3
- package/src/tigerbeetle/src/{test → testing/cluster}/network.zig +46 -22
- package/src/tigerbeetle/src/testing/cluster/state_checker.zig +169 -0
- package/src/tigerbeetle/src/testing/cluster/storage_checker.zig +202 -0
- package/src/tigerbeetle/src/testing/cluster.zig +443 -0
- package/src/tigerbeetle/src/{test → testing}/fuzz.zig +0 -0
- package/src/tigerbeetle/src/testing/hash_log.zig +66 -0
- package/src/tigerbeetle/src/{test → testing}/id.zig +0 -0
- package/src/tigerbeetle/src/testing/packet_simulator.zig +365 -0
- package/src/tigerbeetle/src/{test → testing}/priority_queue.zig +1 -1
- package/src/tigerbeetle/src/testing/reply_sequence.zig +139 -0
- package/src/tigerbeetle/src/{test → testing}/state_machine.zig +3 -1
- package/src/tigerbeetle/src/testing/storage.zig +757 -0
- package/src/tigerbeetle/src/{test → testing}/table.zig +21 -0
- package/src/tigerbeetle/src/{test → testing}/time.zig +0 -0
- package/src/tigerbeetle/src/tigerbeetle.zig +2 -0
- package/src/tigerbeetle/src/tracer.zig +3 -3
- package/src/tigerbeetle/src/unit_tests.zig +4 -4
- package/src/tigerbeetle/src/vopr.zig +2 -2
- package/src/tigerbeetle/src/vsr/client.zig +5 -2
- package/src/tigerbeetle/src/vsr/clock.zig +93 -53
- package/src/tigerbeetle/src/vsr/journal.zig +109 -98
- package/src/tigerbeetle/src/vsr/journal_format_fuzz.zig +2 -2
- package/src/tigerbeetle/src/vsr/replica.zig +1983 -1430
- package/src/tigerbeetle/src/vsr/replica_format.zig +13 -13
- package/src/tigerbeetle/src/vsr/superblock.zig +240 -142
- package/src/tigerbeetle/src/vsr/superblock_client_table.zig +7 -7
- package/src/tigerbeetle/src/vsr/superblock_free_set.zig +1 -1
- package/src/tigerbeetle/src/vsr/superblock_free_set_fuzz.zig +1 -1
- package/src/tigerbeetle/src/vsr/superblock_fuzz.zig +49 -14
- package/src/tigerbeetle/src/vsr/superblock_manifest.zig +38 -19
- package/src/tigerbeetle/src/vsr/superblock_quorums.zig +48 -48
- package/src/tigerbeetle/src/vsr/superblock_quorums_fuzz.zig +51 -51
- package/src/tigerbeetle/src/vsr.zig +99 -33
- package/src/tigerbeetle/src/demo.zig +0 -132
- package/src/tigerbeetle/src/demo_01_create_accounts.zig +0 -35
- package/src/tigerbeetle/src/demo_02_lookup_accounts.zig +0 -7
- package/src/tigerbeetle/src/demo_03_create_transfers.zig +0 -37
- package/src/tigerbeetle/src/demo_04_create_pending_transfers.zig +0 -61
- package/src/tigerbeetle/src/demo_05_post_pending_transfers.zig +0 -37
- package/src/tigerbeetle/src/demo_06_void_pending_transfers.zig +0 -24
- package/src/tigerbeetle/src/demo_07_lookup_transfers.zig +0 -7
- package/src/tigerbeetle/src/test/cluster.zig +0 -352
- package/src/tigerbeetle/src/test/conductor.zig +0 -366
- package/src/tigerbeetle/src/test/packet_simulator.zig +0 -398
- package/src/tigerbeetle/src/test/state_checker.zig +0 -169
- package/src/tigerbeetle/src/test/storage.zig +0 -864
- package/src/tigerbeetle/src/test/storage_checker.zig +0 -204
|
@@ -8,17 +8,21 @@ const constants = @import("constants.zig");
|
|
|
8
8
|
const vsr = @import("vsr.zig");
|
|
9
9
|
const Header = vsr.Header;
|
|
10
10
|
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
const
|
|
11
|
+
const state_machine = @import("vsr_simulator_options").state_machine;
|
|
12
|
+
const StateMachineType = switch (state_machine) {
|
|
13
|
+
.accounting => @import("state_machine.zig").StateMachineType,
|
|
14
|
+
.testing => @import("testing/state_machine.zig").StateMachineType,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const Client = @import("testing/cluster.zig").Client;
|
|
18
|
+
const Cluster = @import("testing/cluster.zig").ClusterType(StateMachineType);
|
|
19
|
+
const Replica = @import("testing/cluster.zig").Replica;
|
|
20
|
+
const StateMachine = Cluster.StateMachine;
|
|
21
|
+
const Failure = @import("testing/cluster.zig").Failure;
|
|
22
|
+
const PartitionMode = @import("testing/packet_simulator.zig").PartitionMode;
|
|
23
|
+
const ReplySequence = @import("testing/reply_sequence.zig").ReplySequence;
|
|
24
|
+
const IdPermutation = @import("testing/id.zig").IdPermutation;
|
|
25
|
+
const Message = @import("message_pool.zig").MessagePool.Message;
|
|
22
26
|
|
|
23
27
|
/// The `log` namespace in this root file is required to implement our custom `log` function.
|
|
24
28
|
const output = std.log.scoped(.state_checker);
|
|
@@ -36,10 +40,6 @@ pub const log_level: std.log.Level = if (log_state_transitions_only) .info else
|
|
|
36
40
|
|
|
37
41
|
const cluster_id = 0;
|
|
38
42
|
|
|
39
|
-
var cluster: *Cluster = undefined;
|
|
40
|
-
var state_checker: *StateChecker = undefined;
|
|
41
|
-
var storage_checker: *StorageChecker = undefined;
|
|
42
|
-
|
|
43
43
|
pub fn main() !void {
|
|
44
44
|
// This must be initialized at runtime as stderr is not comptime known on e.g. Windows.
|
|
45
45
|
log_buffer.unbuffered_writer = std.io.getStdErr().writer();
|
|
@@ -79,51 +79,35 @@ pub fn main() !void {
|
|
|
79
79
|
|
|
80
80
|
const replica_count = 1 + random.uintLessThan(u8, constants.replicas_max);
|
|
81
81
|
const client_count = 1 + random.uintLessThan(u8, constants.clients_max);
|
|
82
|
-
const node_count = replica_count + client_count;
|
|
83
|
-
|
|
84
|
-
const ticks_max = 50_000_000;
|
|
85
|
-
const request_probability = 1 + random.uintLessThan(u8, 99);
|
|
86
|
-
const idle_on_probability = random.uintLessThan(u8, 20);
|
|
87
|
-
const idle_off_probability = 10 + random.uintLessThan(u8, 10);
|
|
88
|
-
|
|
89
|
-
// The maximum number of transitions from calling `client.request()`, not including
|
|
90
|
-
// `register` messages.
|
|
91
|
-
const requests_committed_max: usize = constants.journal_slot_count * 3;
|
|
92
82
|
|
|
93
|
-
const cluster_options
|
|
94
|
-
.
|
|
83
|
+
const cluster_options = Cluster.Options{
|
|
84
|
+
.cluster_id = cluster_id,
|
|
95
85
|
.replica_count = replica_count,
|
|
96
86
|
.client_count = client_count,
|
|
97
87
|
.storage_size_limit = vsr.sector_floor(
|
|
98
88
|
constants.storage_size_max - random.uintLessThan(u64, constants.storage_size_max / 10),
|
|
99
89
|
),
|
|
100
90
|
.seed = random.int(u64),
|
|
101
|
-
.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
.partition_mode = random_partition_mode(random),
|
|
120
|
-
.partition_probability = random.uintLessThan(u8, 3),
|
|
121
|
-
.unpartition_probability = 1 + random.uintLessThan(u8, 10),
|
|
122
|
-
.partition_stability = 100 + random.uintLessThan(u32, 100),
|
|
123
|
-
.unpartition_stability = random.uintLessThan(u32, 20),
|
|
124
|
-
},
|
|
91
|
+
.network = .{
|
|
92
|
+
.replica_count = replica_count,
|
|
93
|
+
.client_count = client_count,
|
|
94
|
+
|
|
95
|
+
.seed = random.int(u64),
|
|
96
|
+
.one_way_delay_mean = 3 + random.uintLessThan(u16, 10),
|
|
97
|
+
.one_way_delay_min = random.uintLessThan(u16, 3),
|
|
98
|
+
.packet_loss_probability = random.uintLessThan(u8, 30),
|
|
99
|
+
.path_maximum_capacity = 2 + random.uintLessThan(u8, 19),
|
|
100
|
+
.path_clog_duration_mean = random.uintLessThan(u16, 500),
|
|
101
|
+
.path_clog_probability = random.uintLessThan(u8, 2),
|
|
102
|
+
.packet_replay_probability = random.uintLessThan(u8, 50),
|
|
103
|
+
|
|
104
|
+
.partition_mode = random_partition_mode(random),
|
|
105
|
+
.partition_probability = random.uintLessThan(u8, 3),
|
|
106
|
+
.unpartition_probability = 1 + random.uintLessThan(u8, 10),
|
|
107
|
+
.partition_stability = 100 + random.uintLessThan(u32, 100),
|
|
108
|
+
.unpartition_stability = random.uintLessThan(u32, 20),
|
|
125
109
|
},
|
|
126
|
-
.
|
|
110
|
+
.storage = .{
|
|
127
111
|
.seed = random.int(u64),
|
|
128
112
|
.read_latency_min = random.uintLessThan(u16, 3),
|
|
129
113
|
.read_latency_mean = 3 + random.uintLessThan(u16, 10),
|
|
@@ -132,16 +116,13 @@ pub fn main() !void {
|
|
|
132
116
|
.read_fault_probability = random.uintLessThan(u8, 10),
|
|
133
117
|
.write_fault_probability = random.uintLessThan(u8, 10),
|
|
134
118
|
.crash_fault_probability = 80 + random.uintLessThan(u8, 21),
|
|
135
|
-
.faulty_superblock = true,
|
|
136
119
|
},
|
|
137
|
-
.
|
|
138
|
-
.
|
|
139
|
-
.
|
|
140
|
-
.
|
|
141
|
-
.restart_stability = random.uintLessThan(u32, 1_000),
|
|
120
|
+
.storage_fault_atlas = .{
|
|
121
|
+
.faulty_superblock = true,
|
|
122
|
+
.faulty_wal_headers = replica_count > 1,
|
|
123
|
+
.faulty_wal_prepares = replica_count > 1,
|
|
142
124
|
},
|
|
143
|
-
|
|
144
|
-
.state_machine_options = switch (constants.state_machine) {
|
|
125
|
+
.state_machine = switch (state_machine) {
|
|
145
126
|
.testing => .{},
|
|
146
127
|
.accounting => .{
|
|
147
128
|
.lsm_forest_node_count = 4096,
|
|
@@ -152,6 +133,26 @@ pub fn main() !void {
|
|
|
152
133
|
},
|
|
153
134
|
};
|
|
154
135
|
|
|
136
|
+
const workload_options = StateMachine.Workload.Options.generate(random, .{
|
|
137
|
+
.client_count = client_count,
|
|
138
|
+
// TODO(DJ) Once Workload no longer needs in_flight_max, make stalled_queue_capacity private.
|
|
139
|
+
// Also maybe make it dynamic (computed from the client_count instead of clients_max).
|
|
140
|
+
.in_flight_max = ReplySequence.stalled_queue_capacity,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const simulator_options = Simulator.Options{
|
|
144
|
+
.cluster = cluster_options,
|
|
145
|
+
.workload = workload_options,
|
|
146
|
+
.replica_crash_probability = 0.00002,
|
|
147
|
+
.replica_crash_stability = random.uintLessThan(u32, 1_000),
|
|
148
|
+
.replica_restart_probability = 0.0001,
|
|
149
|
+
.replica_restart_stability = random.uintLessThan(u32, 1_000),
|
|
150
|
+
.requests_max = constants.journal_slot_count * 3,
|
|
151
|
+
.request_probability = 1 + random.uintLessThan(u8, 99),
|
|
152
|
+
.request_idle_on_probability = random.uintLessThan(u8, 20),
|
|
153
|
+
.request_idle_off_probability = 10 + random.uintLessThan(u8, 10),
|
|
154
|
+
};
|
|
155
|
+
|
|
155
156
|
output.info(
|
|
156
157
|
\\
|
|
157
158
|
\\ SEED={}
|
|
@@ -185,190 +186,335 @@ pub fn main() !void {
|
|
|
185
186
|
\\ restart_stability={} ticks
|
|
186
187
|
, .{
|
|
187
188
|
seed,
|
|
188
|
-
replica_count,
|
|
189
|
-
client_count,
|
|
190
|
-
request_probability,
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
cluster_options.
|
|
194
|
-
cluster_options.
|
|
195
|
-
cluster_options.
|
|
196
|
-
cluster_options.
|
|
197
|
-
cluster_options.
|
|
198
|
-
cluster_options.
|
|
199
|
-
cluster_options.
|
|
200
|
-
cluster_options.
|
|
201
|
-
cluster_options.
|
|
202
|
-
cluster_options.
|
|
203
|
-
cluster_options.
|
|
204
|
-
cluster_options.
|
|
205
|
-
cluster_options.
|
|
206
|
-
cluster_options.
|
|
207
|
-
cluster_options.
|
|
208
|
-
cluster_options.
|
|
209
|
-
cluster_options.
|
|
210
|
-
cluster_options.
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
189
|
+
cluster_options.replica_count,
|
|
190
|
+
cluster_options.client_count,
|
|
191
|
+
simulator_options.request_probability,
|
|
192
|
+
simulator_options.request_idle_on_probability,
|
|
193
|
+
simulator_options.request_idle_off_probability,
|
|
194
|
+
cluster_options.network.one_way_delay_mean,
|
|
195
|
+
cluster_options.network.one_way_delay_min,
|
|
196
|
+
cluster_options.network.packet_loss_probability,
|
|
197
|
+
cluster_options.network.path_maximum_capacity,
|
|
198
|
+
cluster_options.network.path_clog_duration_mean,
|
|
199
|
+
cluster_options.network.path_clog_probability,
|
|
200
|
+
cluster_options.network.packet_replay_probability,
|
|
201
|
+
cluster_options.network.partition_mode,
|
|
202
|
+
cluster_options.network.partition_probability,
|
|
203
|
+
cluster_options.network.unpartition_probability,
|
|
204
|
+
cluster_options.network.partition_stability,
|
|
205
|
+
cluster_options.network.unpartition_stability,
|
|
206
|
+
cluster_options.storage.read_latency_min,
|
|
207
|
+
cluster_options.storage.read_latency_mean,
|
|
208
|
+
cluster_options.storage.write_latency_min,
|
|
209
|
+
cluster_options.storage.write_latency_mean,
|
|
210
|
+
cluster_options.storage.read_fault_probability,
|
|
211
|
+
cluster_options.storage.write_fault_probability,
|
|
212
|
+
simulator_options.replica_crash_probability * 100,
|
|
213
|
+
simulator_options.replica_crash_stability,
|
|
214
|
+
simulator_options.replica_restart_probability * 100,
|
|
215
|
+
simulator_options.replica_restart_stability,
|
|
215
216
|
});
|
|
216
217
|
|
|
217
|
-
|
|
218
|
-
defer
|
|
218
|
+
var simulator = try Simulator.init(allocator, random, simulator_options);
|
|
219
|
+
defer simulator.deinit(allocator);
|
|
219
220
|
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
221
|
+
const ticks_max = 50_000_000;
|
|
222
|
+
var tick: u64 = 0;
|
|
223
|
+
while (tick < ticks_max) : (tick += 1) {
|
|
224
|
+
simulator.tick();
|
|
225
|
+
if (simulator.done()) break;
|
|
226
|
+
} else {
|
|
227
|
+
output.err("you can reproduce this failure with seed={}", .{seed});
|
|
228
|
+
fatal(.liveness, "unable to complete requests_committed_max before ticks_max", .{});
|
|
229
|
+
}
|
|
230
|
+
assert(simulator.done());
|
|
224
231
|
|
|
225
|
-
|
|
226
|
-
|
|
232
|
+
output.info("\n PASSED ({} ticks)", .{tick});
|
|
233
|
+
}
|
|
227
234
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
.
|
|
231
|
-
.
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
235
|
+
pub const Simulator = struct {
|
|
236
|
+
pub const Options = struct {
|
|
237
|
+
cluster: Cluster.Options,
|
|
238
|
+
workload: StateMachine.Workload.Options,
|
|
239
|
+
|
|
240
|
+
/// Probability per tick that a crash will occur.
|
|
241
|
+
replica_crash_probability: f64,
|
|
242
|
+
/// Minimum duration of a crash.
|
|
243
|
+
replica_crash_stability: u32,
|
|
244
|
+
/// Probability per tick that a crashed replica will recovery.
|
|
245
|
+
replica_restart_probability: f64,
|
|
246
|
+
/// Minimum time a replica is up until it is crashed again.
|
|
247
|
+
replica_restart_stability: u32,
|
|
248
|
+
|
|
249
|
+
/// The total number of requests to send. Does not count `register` messages.
|
|
250
|
+
requests_max: usize,
|
|
251
|
+
request_probability: u8, // percent
|
|
252
|
+
request_idle_on_probability: u8, // percent
|
|
253
|
+
request_idle_off_probability: u8, // percent
|
|
254
|
+
};
|
|
239
255
|
|
|
240
|
-
|
|
241
|
-
|
|
256
|
+
random: std.rand.Random,
|
|
257
|
+
options: Options,
|
|
258
|
+
cluster: *Cluster,
|
|
259
|
+
workload: StateMachine.Workload,
|
|
260
|
+
|
|
261
|
+
/// Protect a replica from fast successive crash/restarts.
|
|
262
|
+
replica_stability: []usize,
|
|
263
|
+
reply_sequence: ReplySequence,
|
|
264
|
+
|
|
265
|
+
/// Total number of requests sent, including those that have not been delivered.
|
|
266
|
+
/// Does not include `register` messages.
|
|
267
|
+
requests_sent: usize = 0,
|
|
268
|
+
requests_idle: bool = false,
|
|
269
|
+
|
|
270
|
+
pub fn init(allocator: std.mem.Allocator, random: std.rand.Random, options: Options) !Simulator {
|
|
271
|
+
assert(options.replica_crash_probability < 1.0);
|
|
272
|
+
assert(options.replica_crash_probability >= 0.0);
|
|
273
|
+
assert(options.replica_restart_probability < 1.0);
|
|
274
|
+
assert(options.replica_restart_probability >= 0.0);
|
|
275
|
+
assert(options.requests_max > 0);
|
|
276
|
+
assert(options.request_probability > 0);
|
|
277
|
+
assert(options.request_probability <= 100);
|
|
278
|
+
assert(options.request_idle_on_probability <= 100);
|
|
279
|
+
assert(options.request_idle_off_probability > 0);
|
|
280
|
+
assert(options.request_idle_off_probability <= 100);
|
|
281
|
+
|
|
282
|
+
var cluster = try Cluster.init(allocator, on_cluster_reply, options.cluster);
|
|
283
|
+
errdefer cluster.deinit();
|
|
284
|
+
|
|
285
|
+
var workload = try StateMachine.Workload.init(allocator, random, options.workload);
|
|
286
|
+
errdefer workload.deinit(allocator);
|
|
287
|
+
|
|
288
|
+
var replica_stability = try allocator.alloc(usize, options.cluster.replica_count);
|
|
289
|
+
errdefer allocator.free(replica_stability);
|
|
290
|
+
std.mem.set(usize, replica_stability, 0);
|
|
291
|
+
|
|
292
|
+
var reply_sequence = try ReplySequence.init(allocator);
|
|
293
|
+
errdefer reply_sequence.deinit(allocator);
|
|
294
|
+
|
|
295
|
+
return Simulator{
|
|
296
|
+
.random = random,
|
|
297
|
+
.options = options,
|
|
298
|
+
.cluster = cluster,
|
|
299
|
+
.workload = workload,
|
|
300
|
+
.replica_stability = replica_stability,
|
|
301
|
+
.reply_sequence = reply_sequence,
|
|
302
|
+
};
|
|
242
303
|
}
|
|
243
304
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
// The minimum number of healthy replicas required for a crashed replica to be able to recover.
|
|
262
|
-
const replica_normal_min = replicas: {
|
|
263
|
-
if (replica_count == 1) {
|
|
264
|
-
// A cluster of 1 can crash safely (as long as there is no disk corruption) since it
|
|
265
|
-
// does not run the recovery protocol.
|
|
266
|
-
break :replicas 0;
|
|
267
|
-
} else {
|
|
268
|
-
break :replicas cluster.replicas[0].quorum_view_change;
|
|
305
|
+
pub fn deinit(simulator: *Simulator, allocator: std.mem.Allocator) void {
|
|
306
|
+
allocator.free(simulator.replica_stability);
|
|
307
|
+
simulator.reply_sequence.deinit(allocator);
|
|
308
|
+
simulator.workload.deinit(allocator);
|
|
309
|
+
simulator.cluster.deinit();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
pub fn done(simulator: *Simulator) bool {
|
|
313
|
+
assert(simulator.requests_sent <= simulator.options.requests_max);
|
|
314
|
+
|
|
315
|
+
if (!simulator.cluster.state_checker.convergence()) return false;
|
|
316
|
+
if (!simulator.reply_sequence.empty()) return false;
|
|
317
|
+
if (simulator.requests_sent < simulator.options.requests_max) return false;
|
|
318
|
+
|
|
319
|
+
for (simulator.cluster.replica_health) |health| {
|
|
320
|
+
if (health == .down) return false;
|
|
269
321
|
}
|
|
270
|
-
};
|
|
271
322
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
const health_options = &cluster.options.health_options;
|
|
275
|
-
// The maximum number of replicas that can crash, with the cluster still able to recover.
|
|
276
|
-
var crashes = cluster.replica_normal_count() -| replica_normal_min;
|
|
277
|
-
|
|
278
|
-
for (cluster.storages) |*storage, replica| {
|
|
279
|
-
if (cluster.replicas[replica].journal.status == .recovered) {
|
|
280
|
-
// TODO Remove this workaround when VSR recovery protocol is disabled.
|
|
281
|
-
// When only the minimum number of replicas are healthy (no more crashes allowed),
|
|
282
|
-
// disable storage faults on all healthy replicas.
|
|
283
|
-
//
|
|
284
|
-
// This is a workaround to avoid the deadlock that occurs when (for example) in a
|
|
285
|
-
// cluster of 3 replicas, one is down, another has a corrupt prepare, and the last does
|
|
286
|
-
// not have the prepare. The two healthy replicas can never complete a view change,
|
|
287
|
-
// because two replicas are not enough to nack, and the unhealthy replica cannot
|
|
288
|
-
// complete the VSR recovery protocol either.
|
|
289
|
-
if (cluster.health[replica] == .up and crashes == 0) {
|
|
290
|
-
if (storage.faulty) {
|
|
291
|
-
log_simulator.debug("{}: disable storage faults", .{replica});
|
|
292
|
-
storage.faulty = false;
|
|
293
|
-
}
|
|
294
|
-
} else {
|
|
295
|
-
// When a journal recovers for the first time, enable its storage faults.
|
|
296
|
-
// Future crashes will recover in the presence of faults.
|
|
297
|
-
if (!storage.faulty) {
|
|
298
|
-
log_simulator.debug("{}: enable storage faults", .{replica});
|
|
299
|
-
storage.faulty = true;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
}
|
|
323
|
+
for (simulator.cluster.clients) |*client| {
|
|
324
|
+
if (client.request_queue.count > 0) return false;
|
|
303
325
|
}
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
304
328
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
ticks.* -|= 1;
|
|
309
|
-
replica.tick();
|
|
310
|
-
cluster.storages[index].tick();
|
|
311
|
-
|
|
312
|
-
state_checker.check_state(replica.replica) catch |err| {
|
|
313
|
-
fatal(.correctness, "state checker error: {}", .{err});
|
|
314
|
-
};
|
|
315
|
-
|
|
316
|
-
if (ticks.* != 0) continue;
|
|
317
|
-
if (crashes == 0) continue;
|
|
318
|
-
if (cluster.storages[replica.replica].writes.count() == 0) {
|
|
319
|
-
if (!chance_f64(random, health_options.crash_probability)) continue;
|
|
320
|
-
} else {
|
|
321
|
-
if (!chance_f64(random, health_options.crash_probability * 10.0)) continue;
|
|
322
|
-
}
|
|
329
|
+
pub fn tick(simulator: *Simulator) void {
|
|
330
|
+
// TODO(Zig): Remove (see on_cluster_reply()).
|
|
331
|
+
simulator.cluster.context = simulator;
|
|
323
332
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
333
|
+
simulator.cluster.tick();
|
|
334
|
+
simulator.tick_requests();
|
|
335
|
+
simulator.tick_crash();
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
fn on_cluster_reply(
|
|
339
|
+
cluster: *Cluster,
|
|
340
|
+
reply_client: usize,
|
|
341
|
+
request: *Message,
|
|
342
|
+
reply: *Message,
|
|
343
|
+
) void {
|
|
344
|
+
// TODO(Zig) Use @returnAddress to initialzie the cluster, then this can just use @fieldParentPtr().
|
|
345
|
+
const simulator = @ptrCast(*Simulator, @alignCast(@alignOf(Simulator), cluster.context.?));
|
|
346
|
+
simulator.reply_sequence.insert(reply_client, request, reply);
|
|
347
|
+
|
|
348
|
+
while (simulator.reply_sequence.peek()) |commit| {
|
|
349
|
+
defer simulator.reply_sequence.next();
|
|
350
|
+
|
|
351
|
+
const commit_client = simulator.cluster.clients[commit.client_index];
|
|
352
|
+
assert(commit.reply.references == 1);
|
|
353
|
+
assert(commit.reply.header.command == .reply);
|
|
354
|
+
assert(commit.reply.header.client == commit_client.id);
|
|
355
|
+
assert(commit.reply.header.request == commit.request.header.request);
|
|
356
|
+
assert(commit.reply.header.operation == commit.request.header.operation);
|
|
357
|
+
|
|
358
|
+
assert(commit.request.references == 1);
|
|
359
|
+
assert(commit.request.header.command == .request);
|
|
360
|
+
assert(commit.request.header.client == commit_client.id);
|
|
361
|
+
|
|
362
|
+
log_simulator.debug("consume_stalled_replies: op={} operation={} client={} request={}", .{
|
|
363
|
+
commit.reply.header.op,
|
|
364
|
+
commit.reply.header.operation,
|
|
365
|
+
commit.request.header.client,
|
|
366
|
+
commit.request.header.request,
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
if (commit.request.header.operation != .register) {
|
|
370
|
+
simulator.workload.on_reply(
|
|
371
|
+
commit.client_index,
|
|
372
|
+
commit.reply.header.operation,
|
|
373
|
+
commit.reply.header.timestamp,
|
|
374
|
+
commit.request.body(),
|
|
375
|
+
commit.reply.body(),
|
|
376
|
+
);
|
|
339
377
|
}
|
|
340
378
|
}
|
|
379
|
+
}
|
|
341
380
|
|
|
342
|
-
|
|
343
|
-
|
|
381
|
+
/// Maybe send a request from one of the cluster's clients.
|
|
382
|
+
fn tick_requests(simulator: *Simulator) void {
|
|
383
|
+
if (simulator.requests_idle) {
|
|
384
|
+
if (chance(simulator.random, simulator.options.request_idle_off_probability)) {
|
|
385
|
+
simulator.requests_idle = false;
|
|
386
|
+
}
|
|
387
|
+
} else {
|
|
388
|
+
if (chance(simulator.random, simulator.options.request_idle_on_probability)) {
|
|
389
|
+
simulator.requests_idle = true;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
344
392
|
|
|
345
|
-
if (
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
393
|
+
if (simulator.requests_idle) return;
|
|
394
|
+
if (simulator.requests_sent == simulator.options.requests_max) return;
|
|
395
|
+
if (!chance(simulator.random, simulator.options.request_probability)) return;
|
|
396
|
+
|
|
397
|
+
const client_index =
|
|
398
|
+
simulator.random.uintLessThan(usize, simulator.options.cluster.client_count);
|
|
399
|
+
var client = &simulator.cluster.clients[client_index];
|
|
400
|
+
|
|
401
|
+
// Make sure that there is capacity in the client's request queue so that we never trigger
|
|
402
|
+
// error.TooManyOutstandingRequests.
|
|
403
|
+
if (client.request_queue.count + 1 > constants.client_request_queue_max) return;
|
|
404
|
+
|
|
405
|
+
// Messages aren't added to the ReplySequence until a reply arrives.
|
|
406
|
+
// Before sending a new message, make sure there will definitely be room for it.
|
|
407
|
+
var reserved: usize = 0;
|
|
408
|
+
for (simulator.cluster.clients) |*c| {
|
|
409
|
+
// Count the number of clients that are still waiting for a `register` to complete,
|
|
410
|
+
// since they may start one at any time.
|
|
411
|
+
reserved += @boolToInt(c.session == 0);
|
|
412
|
+
// Count the number of requests queued.
|
|
413
|
+
reserved += c.request_queue.count;
|
|
349
414
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
415
|
+
// +1 for the potential request — is there room in the sequencer's queue?
|
|
416
|
+
if (reserved + 1 > simulator.reply_sequence.free()) return;
|
|
417
|
+
|
|
418
|
+
var request_message = client.get_message();
|
|
419
|
+
defer client.unref(request_message);
|
|
420
|
+
|
|
421
|
+
const request_metadata = simulator.workload.build_request(
|
|
422
|
+
client_index,
|
|
423
|
+
@alignCast(
|
|
424
|
+
@alignOf(vsr.Header),
|
|
425
|
+
request_message.buffer[@sizeOf(vsr.Header)..constants.message_size_max],
|
|
426
|
+
),
|
|
427
|
+
);
|
|
428
|
+
assert(request_metadata.size <= constants.message_size_max - @sizeOf(vsr.Header));
|
|
429
|
+
|
|
430
|
+
simulator.cluster.request(
|
|
431
|
+
client_index,
|
|
432
|
+
request_metadata.operation,
|
|
433
|
+
request_message,
|
|
434
|
+
request_metadata.size,
|
|
435
|
+
);
|
|
436
|
+
// Since we already checked the client's request queue for free space, `client.request()`
|
|
437
|
+
// should always queue the request.
|
|
438
|
+
assert(request_message == client.request_queue.tail_ptr().?.message);
|
|
439
|
+
assert(request_message.header.size == @sizeOf(vsr.Header) + request_metadata.size);
|
|
440
|
+
assert(request_message.header.operation.cast(StateMachine) == request_metadata.operation);
|
|
441
|
+
|
|
442
|
+
simulator.requests_sent += 1;
|
|
443
|
+
assert(simulator.requests_sent <= simulator.options.requests_max);
|
|
353
444
|
}
|
|
354
445
|
|
|
355
|
-
|
|
356
|
-
|
|
446
|
+
fn tick_crash(simulator: *Simulator) void {
|
|
447
|
+
const recoverable_count_min =
|
|
448
|
+
vsr.quorums(simulator.options.cluster.replica_count).view_change;
|
|
449
|
+
var recoverable_count: usize = 0;
|
|
450
|
+
for (simulator.cluster.replicas) |*replica| {
|
|
451
|
+
recoverable_count += @boolToInt(replica.status != .recovering_head);
|
|
452
|
+
}
|
|
357
453
|
|
|
358
|
-
|
|
359
|
-
|
|
454
|
+
for (simulator.cluster.replicas) |*replica| {
|
|
455
|
+
simulator.replica_stability[replica.replica] -|= 1;
|
|
456
|
+
const stability = simulator.replica_stability[replica.replica];
|
|
457
|
+
if (stability > 0) continue;
|
|
458
|
+
|
|
459
|
+
switch (simulator.cluster.replica_health[replica.replica]) {
|
|
460
|
+
.up => {
|
|
461
|
+
const storage = &simulator.cluster.storages[replica.replica];
|
|
462
|
+
const replica_writes = storage.writes.count();
|
|
463
|
+
const crash_probability = simulator.options.replica_crash_probability *
|
|
464
|
+
@as(f64, if (replica_writes == 0) 1.0 else 10.0);
|
|
465
|
+
if (!chance_f64(simulator.random, crash_probability)) continue;
|
|
466
|
+
|
|
467
|
+
const fault = recoverable_count > recoverable_count_min;
|
|
468
|
+
replica.superblock.storage.faulty = fault;
|
|
469
|
+
|
|
470
|
+
if (!fault) {
|
|
471
|
+
// The journal writes redundant headers of faulty ops as zeroes to ensure
|
|
472
|
+
// that they remain faulty after a crash/recover. Since that fault cannot
|
|
473
|
+
// be disabled by `storage.faulty`, we must manually repair it here to
|
|
474
|
+
// ensure a cluster cannot become stuck in status=recovering_head.
|
|
475
|
+
// See recover_slots() for more detail.
|
|
476
|
+
const offset = vsr.Zone.wal_headers.offset(0);
|
|
477
|
+
const size = vsr.Zone.wal_headers.size().?;
|
|
478
|
+
const headers_bytes = storage.memory[offset..][0..size];
|
|
479
|
+
const headers = mem.bytesAsSlice(vsr.Header, headers_bytes);
|
|
480
|
+
for (headers) |*h, slot| {
|
|
481
|
+
if (h.checksum == 0) h.* = storage.wal_prepares()[slot].header;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
360
484
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
485
|
+
log_simulator.debug("{}: crash replica (faults={})", .{ replica.replica, fault });
|
|
486
|
+
simulator.cluster.crash_replica(replica.replica) catch unreachable;
|
|
487
|
+
replica.superblock.storage.faulty = true;
|
|
488
|
+
|
|
489
|
+
recoverable_count -= @boolToInt(replica.status == .recovering_head);
|
|
490
|
+
assert(replica.status != .recovering_head or fault);
|
|
491
|
+
|
|
492
|
+
simulator.replica_stability[replica.replica] =
|
|
493
|
+
simulator.options.replica_crash_stability;
|
|
494
|
+
},
|
|
495
|
+
.down => {
|
|
496
|
+
if (chance_f64(simulator.random, simulator.options.replica_restart_probability)) {
|
|
497
|
+
simulator.cluster.restart_replica(replica.replica);
|
|
498
|
+
log_simulator.debug("{}: restart replica", .{replica.replica});
|
|
499
|
+
simulator.replica_stability[replica.replica] =
|
|
500
|
+
simulator.options.replica_restart_stability;
|
|
501
|
+
}
|
|
502
|
+
},
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
366
506
|
};
|
|
367
507
|
|
|
368
508
|
/// Print an error message and then exit with an exit code.
|
|
369
|
-
fn fatal(
|
|
509
|
+
fn fatal(failure: Failure, comptime fmt_string: []const u8, args: anytype) noreturn {
|
|
370
510
|
output.err(fmt_string, args);
|
|
371
|
-
std.os.exit(@enumToInt(
|
|
511
|
+
std.os.exit(@enumToInt(failure));
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/// Returns true, `p` percent of the time, else false.
|
|
515
|
+
fn chance(random: std.rand.Random, p: u8) bool {
|
|
516
|
+
assert(p <= 100);
|
|
517
|
+
return random.uintLessThanBiased(u8, 100) < p;
|
|
372
518
|
}
|
|
373
519
|
|
|
374
520
|
/// Returns true, `p` percent of the time, else false.
|
|
@@ -383,29 +529,10 @@ fn args_next(args: *std.process.ArgIterator, allocator: std.mem.Allocator) ?[:0]
|
|
|
383
529
|
return err_or_bytes catch @panic("Unable to extract next value from args");
|
|
384
530
|
}
|
|
385
531
|
|
|
386
|
-
|
|
387
|
-
state_checker.check_state(replica.replica) catch |err| {
|
|
388
|
-
fatal(.correctness, "state checker error: {}", .{err});
|
|
389
|
-
};
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
fn on_replica_compact(replica: *const Replica) void {
|
|
393
|
-
storage_checker.replica_compact(replica) catch |err| {
|
|
394
|
-
fatal(.correctness, "storage checker error: {}", .{err});
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
fn on_replica_checkpoint(replica: *const Replica) void {
|
|
399
|
-
storage_checker.replica_checkpoint(replica) catch |err| {
|
|
400
|
-
fatal(.correctness, "storage checker error: {}", .{err});
|
|
401
|
-
};
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
/// Returns a random partitioning mode, excluding .custom
|
|
532
|
+
/// Returns a random partitioning mode.
|
|
405
533
|
fn random_partition_mode(random: std.rand.Random) PartitionMode {
|
|
406
534
|
const typeInfo = @typeInfo(PartitionMode).Enum;
|
|
407
|
-
var enumAsInt = random.uintAtMost(typeInfo.tag_type, typeInfo.fields.len -
|
|
408
|
-
if (enumAsInt >= @enumToInt(PartitionMode.custom)) enumAsInt += 1;
|
|
535
|
+
var enumAsInt = random.uintAtMost(typeInfo.tag_type, typeInfo.fields.len - 1);
|
|
409
536
|
return @intToEnum(PartitionMode, enumAsInt);
|
|
410
537
|
}
|
|
411
538
|
|