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.
Files changed (85) hide show
  1. package/dist/.client.node.sha256 +1 -1
  2. package/package.json +4 -3
  3. package/scripts/build_lib.sh +41 -0
  4. package/src/node.zig +1 -1
  5. package/src/tigerbeetle/scripts/validate_docs.sh +7 -1
  6. package/src/tigerbeetle/src/benchmark.zig +3 -3
  7. package/src/tigerbeetle/src/config.zig +31 -16
  8. package/src/tigerbeetle/src/constants.zig +48 -9
  9. package/src/tigerbeetle/src/ewah.zig +5 -5
  10. package/src/tigerbeetle/src/ewah_fuzz.zig +1 -1
  11. package/src/tigerbeetle/src/lsm/binary_search.zig +1 -1
  12. package/src/tigerbeetle/src/lsm/bloom_filter.zig +1 -1
  13. package/src/tigerbeetle/src/lsm/compaction.zig +34 -21
  14. package/src/tigerbeetle/src/lsm/forest_fuzz.zig +84 -104
  15. package/src/tigerbeetle/src/lsm/grid.zig +19 -13
  16. package/src/tigerbeetle/src/lsm/manifest_log.zig +8 -10
  17. package/src/tigerbeetle/src/lsm/manifest_log_fuzz.zig +18 -13
  18. package/src/tigerbeetle/src/lsm/merge_iterator.zig +1 -1
  19. package/src/tigerbeetle/src/lsm/segmented_array.zig +17 -17
  20. package/src/tigerbeetle/src/lsm/segmented_array_fuzz.zig +1 -1
  21. package/src/tigerbeetle/src/lsm/set_associative_cache.zig +1 -1
  22. package/src/tigerbeetle/src/lsm/table.zig +8 -20
  23. package/src/tigerbeetle/src/lsm/table_immutable.zig +1 -1
  24. package/src/tigerbeetle/src/lsm/table_iterator.zig +3 -3
  25. package/src/tigerbeetle/src/lsm/table_mutable.zig +14 -2
  26. package/src/tigerbeetle/src/lsm/test.zig +5 -4
  27. package/src/tigerbeetle/src/lsm/tree.zig +1 -2
  28. package/src/tigerbeetle/src/lsm/tree_fuzz.zig +85 -115
  29. package/src/tigerbeetle/src/message_bus.zig +4 -4
  30. package/src/tigerbeetle/src/message_pool.zig +7 -10
  31. package/src/tigerbeetle/src/ring_buffer.zig +22 -12
  32. package/src/tigerbeetle/src/simulator.zig +366 -239
  33. package/src/tigerbeetle/src/state_machine/auditor.zig +5 -5
  34. package/src/tigerbeetle/src/state_machine/workload.zig +3 -3
  35. package/src/tigerbeetle/src/state_machine.zig +190 -178
  36. package/src/tigerbeetle/src/{util.zig → stdx.zig} +2 -0
  37. package/src/tigerbeetle/src/storage.zig +13 -6
  38. package/src/tigerbeetle/src/{test → testing/cluster}/message_bus.zig +3 -3
  39. package/src/tigerbeetle/src/{test → testing/cluster}/network.zig +46 -22
  40. package/src/tigerbeetle/src/testing/cluster/state_checker.zig +169 -0
  41. package/src/tigerbeetle/src/testing/cluster/storage_checker.zig +202 -0
  42. package/src/tigerbeetle/src/testing/cluster.zig +443 -0
  43. package/src/tigerbeetle/src/{test → testing}/fuzz.zig +0 -0
  44. package/src/tigerbeetle/src/testing/hash_log.zig +66 -0
  45. package/src/tigerbeetle/src/{test → testing}/id.zig +0 -0
  46. package/src/tigerbeetle/src/testing/packet_simulator.zig +365 -0
  47. package/src/tigerbeetle/src/{test → testing}/priority_queue.zig +1 -1
  48. package/src/tigerbeetle/src/testing/reply_sequence.zig +139 -0
  49. package/src/tigerbeetle/src/{test → testing}/state_machine.zig +3 -1
  50. package/src/tigerbeetle/src/testing/storage.zig +757 -0
  51. package/src/tigerbeetle/src/{test → testing}/table.zig +21 -0
  52. package/src/tigerbeetle/src/{test → testing}/time.zig +0 -0
  53. package/src/tigerbeetle/src/tigerbeetle.zig +2 -0
  54. package/src/tigerbeetle/src/tracer.zig +3 -3
  55. package/src/tigerbeetle/src/unit_tests.zig +4 -4
  56. package/src/tigerbeetle/src/vopr.zig +2 -2
  57. package/src/tigerbeetle/src/vsr/client.zig +5 -2
  58. package/src/tigerbeetle/src/vsr/clock.zig +93 -53
  59. package/src/tigerbeetle/src/vsr/journal.zig +109 -98
  60. package/src/tigerbeetle/src/vsr/journal_format_fuzz.zig +2 -2
  61. package/src/tigerbeetle/src/vsr/replica.zig +1983 -1430
  62. package/src/tigerbeetle/src/vsr/replica_format.zig +13 -13
  63. package/src/tigerbeetle/src/vsr/superblock.zig +240 -142
  64. package/src/tigerbeetle/src/vsr/superblock_client_table.zig +7 -7
  65. package/src/tigerbeetle/src/vsr/superblock_free_set.zig +1 -1
  66. package/src/tigerbeetle/src/vsr/superblock_free_set_fuzz.zig +1 -1
  67. package/src/tigerbeetle/src/vsr/superblock_fuzz.zig +49 -14
  68. package/src/tigerbeetle/src/vsr/superblock_manifest.zig +38 -19
  69. package/src/tigerbeetle/src/vsr/superblock_quorums.zig +48 -48
  70. package/src/tigerbeetle/src/vsr/superblock_quorums_fuzz.zig +51 -51
  71. package/src/tigerbeetle/src/vsr.zig +99 -33
  72. package/src/tigerbeetle/src/demo.zig +0 -132
  73. package/src/tigerbeetle/src/demo_01_create_accounts.zig +0 -35
  74. package/src/tigerbeetle/src/demo_02_lookup_accounts.zig +0 -7
  75. package/src/tigerbeetle/src/demo_03_create_transfers.zig +0 -37
  76. package/src/tigerbeetle/src/demo_04_create_pending_transfers.zig +0 -61
  77. package/src/tigerbeetle/src/demo_05_post_pending_transfers.zig +0 -37
  78. package/src/tigerbeetle/src/demo_06_void_pending_transfers.zig +0 -24
  79. package/src/tigerbeetle/src/demo_07_lookup_transfers.zig +0 -7
  80. package/src/tigerbeetle/src/test/cluster.zig +0 -352
  81. package/src/tigerbeetle/src/test/conductor.zig +0 -366
  82. package/src/tigerbeetle/src/test/packet_simulator.zig +0 -398
  83. package/src/tigerbeetle/src/test/state_checker.zig +0 -169
  84. package/src/tigerbeetle/src/test/storage.zig +0 -864
  85. 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 Client = @import("test/cluster.zig").Client;
12
- const Cluster = @import("test/cluster.zig").Cluster;
13
- const ClusterOptions = @import("test/cluster.zig").ClusterOptions;
14
- const Replica = @import("test/cluster.zig").Replica;
15
- const StateMachine = @import("test/cluster.zig").StateMachine;
16
- const StateChecker = @import("test/state_checker.zig").StateChecker;
17
- const StorageChecker = @import("test/storage_checker.zig").StorageChecker;
18
- const PartitionMode = @import("test/packet_simulator.zig").PartitionMode;
19
- const MessageBus = @import("test/message_bus.zig").MessageBus;
20
- const Conductor = @import("test/conductor.zig").ConductorType(Client, MessageBus, StateMachine);
21
- const IdPermutation = @import("test/id.zig").IdPermutation;
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: ClusterOptions = .{
94
- .cluster = cluster_id,
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
- .on_change_state = on_replica_change_state,
102
- .on_compact = on_replica_compact,
103
- .on_checkpoint = on_replica_checkpoint,
104
- .network_options = .{
105
- .packet_simulator_options = .{
106
- .replica_count = replica_count,
107
- .client_count = client_count,
108
- .node_count = node_count,
109
-
110
- .seed = random.int(u64),
111
- .one_way_delay_mean = 3 + random.uintLessThan(u16, 10),
112
- .one_way_delay_min = random.uintLessThan(u16, 3),
113
- .packet_loss_probability = random.uintLessThan(u8, 30),
114
- .path_maximum_capacity = 2 + random.uintLessThan(u8, 19),
115
- .path_clog_duration_mean = random.uintLessThan(u16, 500),
116
- .path_clog_probability = random.uintLessThan(u8, 2),
117
- .packet_replay_probability = random.uintLessThan(u8, 50),
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
- .storage_options = .{
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
- .health_options = .{
138
- .crash_probability = 0.000001,
139
- .crash_stability = random.uintLessThan(u32, 1_000),
140
- .restart_probability = 0.0001,
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
- // TODO(dj) SimulatorType(StateMachine).init(state_machine_options)
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
- idle_on_probability,
192
- idle_off_probability,
193
- cluster_options.network_options.packet_simulator_options.one_way_delay_mean,
194
- cluster_options.network_options.packet_simulator_options.one_way_delay_min,
195
- cluster_options.network_options.packet_simulator_options.packet_loss_probability,
196
- cluster_options.network_options.packet_simulator_options.path_maximum_capacity,
197
- cluster_options.network_options.packet_simulator_options.path_clog_duration_mean,
198
- cluster_options.network_options.packet_simulator_options.path_clog_probability,
199
- cluster_options.network_options.packet_simulator_options.packet_replay_probability,
200
- cluster_options.network_options.packet_simulator_options.partition_mode,
201
- cluster_options.network_options.packet_simulator_options.partition_probability,
202
- cluster_options.network_options.packet_simulator_options.unpartition_probability,
203
- cluster_options.network_options.packet_simulator_options.partition_stability,
204
- cluster_options.network_options.packet_simulator_options.unpartition_stability,
205
- cluster_options.storage_options.read_latency_min,
206
- cluster_options.storage_options.read_latency_mean,
207
- cluster_options.storage_options.write_latency_min,
208
- cluster_options.storage_options.write_latency_mean,
209
- cluster_options.storage_options.read_fault_probability,
210
- cluster_options.storage_options.write_fault_probability,
211
- cluster_options.health_options.crash_probability * 100,
212
- cluster_options.health_options.crash_stability,
213
- cluster_options.health_options.restart_probability * 100,
214
- cluster_options.health_options.restart_stability,
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
- cluster = try Cluster.create(allocator, random, cluster_options);
218
- defer cluster.destroy();
218
+ var simulator = try Simulator.init(allocator, random, simulator_options);
219
+ defer simulator.deinit(allocator);
219
220
 
220
- const workload_options = StateMachine.Workload.Options.generate(random, .{
221
- .client_count = client_count,
222
- .in_flight_max = Conductor.stalled_queue_capacity,
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
- var workload = try StateMachine.Workload.init(allocator, random, workload_options);
226
- defer workload.deinit(allocator);
232
+ output.info("\n PASSED ({} ticks)", .{tick});
233
+ }
227
234
 
228
- var conductor = try Conductor.init(allocator, random, &workload, .{
229
- .cluster = cluster_id,
230
- .replica_count = replica_count,
231
- .client_count = client_count,
232
- .message_bus_options = .{ .network = &cluster.network },
233
- .requests_max = requests_committed_max,
234
- .request_probability = request_probability,
235
- .idle_on_probability = idle_on_probability,
236
- .idle_off_probability = idle_off_probability,
237
- });
238
- defer conductor.deinit(allocator);
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
- for (conductor.clients) |*client| {
241
- cluster.network.link(client.message_bus.process, &client.message_bus);
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
- state_checker = try allocator.create(StateChecker);
245
- defer allocator.destroy(state_checker);
246
-
247
- state_checker.* = try StateChecker.init(
248
- allocator,
249
- cluster_id,
250
- cluster.replicas,
251
- conductor.clients,
252
- );
253
- defer state_checker.deinit();
254
-
255
- storage_checker = try allocator.create(StorageChecker);
256
- defer allocator.destroy(storage_checker);
257
-
258
- storage_checker.* = StorageChecker.init(allocator);
259
- defer storage_checker.deinit();
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
- var tick: u64 = 0;
273
- while (tick < ticks_max) : (tick += 1) {
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
- for (cluster.replicas) |*replica, index| {
306
- switch (cluster.health[replica.replica]) {
307
- .up => |*ticks| {
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
- if (!try cluster.crash_replica(replica.replica)) continue;
325
- log_simulator.debug("{}: crash replica", .{replica.replica});
326
- crashes -= 1;
327
- },
328
- .down => |*ticks| {
329
- ticks.* -|= 1;
330
- // Keep ticking the time so that it won't have diverged too far to synchronize
331
- // when the replica restarts.
332
- replica.clock.time.tick();
333
- assert(replica.status == .recovering);
334
- if (ticks.* == 0 and chance_f64(random, health_options.restart_probability)) {
335
- cluster.health[replica.replica] = .{ .up = health_options.restart_stability };
336
- log_simulator.debug("{}: restart replica", .{replica.replica});
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
- cluster.network.packet_simulator.tick(cluster.health);
343
- conductor.tick();
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 (state_checker.convergence() and conductor.done() and
346
- cluster.replica_up_count() == replica_count)
347
- {
348
- break;
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
- } else {
351
- output.err("you can reproduce this failure with seed={}", .{seed});
352
- fatal(.liveness, "unable to complete requests_committed_max before ticks_max", .{});
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
- assert(state_checker.convergence());
356
- assert(conductor.done());
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
- output.info("\n PASSED ({} ticks)", .{tick});
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
- pub const ExitCode = enum(u8) {
362
- ok = 0,
363
- crash = 127, // Any assertion crash will be given an exit code of 127 by default.
364
- liveness = 128,
365
- correctness = 129,
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(exit_code: ExitCode, comptime fmt_string: []const u8, args: anytype) noreturn {
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(exit_code));
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
- fn on_replica_change_state(replica: *const Replica) void {
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 - 2);
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