tigerbeetle-node 0.11.7 → 0.11.9

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 (83) hide show
  1. package/dist/.client.node.sha256 +1 -1
  2. package/package.json +4 -3
  3. package/scripts/build_lib.sh +29 -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 +29 -16
  8. package/src/tigerbeetle/src/constants.zig +30 -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 +85 -103
  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 +12 -8
  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/tree.zig +31 -5
  27. package/src/tigerbeetle/src/lsm/tree_fuzz.zig +86 -114
  28. package/src/tigerbeetle/src/message_bus.zig +4 -4
  29. package/src/tigerbeetle/src/message_pool.zig +7 -10
  30. package/src/tigerbeetle/src/ring_buffer.zig +22 -12
  31. package/src/tigerbeetle/src/simulator.zig +360 -214
  32. package/src/tigerbeetle/src/state_machine/auditor.zig +5 -5
  33. package/src/tigerbeetle/src/state_machine/workload.zig +3 -3
  34. package/src/tigerbeetle/src/state_machine.zig +190 -178
  35. package/src/tigerbeetle/src/{util.zig → stdx.zig} +2 -0
  36. package/src/tigerbeetle/src/storage.zig +13 -6
  37. package/src/tigerbeetle/src/{test → testing/cluster}/message_bus.zig +3 -3
  38. package/src/tigerbeetle/src/{test → testing/cluster}/network.zig +46 -22
  39. package/src/tigerbeetle/src/testing/cluster/state_checker.zig +169 -0
  40. package/src/tigerbeetle/src/testing/cluster/storage_checker.zig +202 -0
  41. package/src/tigerbeetle/src/testing/cluster.zig +537 -0
  42. package/src/tigerbeetle/src/{test → testing}/fuzz.zig +0 -0
  43. package/src/tigerbeetle/src/testing/hash_log.zig +66 -0
  44. package/src/tigerbeetle/src/{test → testing}/id.zig +0 -0
  45. package/src/tigerbeetle/src/testing/packet_simulator.zig +365 -0
  46. package/src/tigerbeetle/src/{test → testing}/priority_queue.zig +1 -1
  47. package/src/tigerbeetle/src/testing/reply_sequence.zig +139 -0
  48. package/src/tigerbeetle/src/{test → testing}/state_machine.zig +3 -1
  49. package/src/tigerbeetle/src/testing/storage.zig +754 -0
  50. package/src/tigerbeetle/src/{test → testing}/table.zig +21 -0
  51. package/src/tigerbeetle/src/{test → testing}/time.zig +0 -0
  52. package/src/tigerbeetle/src/tigerbeetle.zig +2 -0
  53. package/src/tigerbeetle/src/tracer.zig +3 -3
  54. package/src/tigerbeetle/src/unit_tests.zig +4 -4
  55. package/src/tigerbeetle/src/vopr.zig +2 -2
  56. package/src/tigerbeetle/src/vsr/client.zig +16 -9
  57. package/src/tigerbeetle/src/vsr/clock.zig +93 -53
  58. package/src/tigerbeetle/src/vsr/journal.zig +29 -14
  59. package/src/tigerbeetle/src/vsr/journal_format_fuzz.zig +2 -2
  60. package/src/tigerbeetle/src/vsr/replica.zig +1383 -774
  61. package/src/tigerbeetle/src/vsr/replica_format.zig +2 -2
  62. package/src/tigerbeetle/src/vsr/superblock.zig +59 -43
  63. package/src/tigerbeetle/src/vsr/superblock_client_table.zig +7 -7
  64. package/src/tigerbeetle/src/vsr/superblock_free_set.zig +1 -1
  65. package/src/tigerbeetle/src/vsr/superblock_free_set_fuzz.zig +1 -1
  66. package/src/tigerbeetle/src/vsr/superblock_fuzz.zig +15 -7
  67. package/src/tigerbeetle/src/vsr/superblock_manifest.zig +38 -19
  68. package/src/tigerbeetle/src/vsr/superblock_quorums_fuzz.zig +1 -1
  69. package/src/tigerbeetle/src/vsr.zig +6 -4
  70. package/src/tigerbeetle/src/demo.zig +0 -132
  71. package/src/tigerbeetle/src/demo_01_create_accounts.zig +0 -35
  72. package/src/tigerbeetle/src/demo_02_lookup_accounts.zig +0 -7
  73. package/src/tigerbeetle/src/demo_03_create_transfers.zig +0 -37
  74. package/src/tigerbeetle/src/demo_04_create_pending_transfers.zig +0 -61
  75. package/src/tigerbeetle/src/demo_05_post_pending_transfers.zig +0 -37
  76. package/src/tigerbeetle/src/demo_06_void_pending_transfers.zig +0 -24
  77. package/src/tigerbeetle/src/demo_07_lookup_transfers.zig +0 -7
  78. package/src/tigerbeetle/src/test/cluster.zig +0 -352
  79. package/src/tigerbeetle/src/test/conductor.zig +0 -366
  80. package/src/tigerbeetle/src/test/packet_simulator.zig +0 -398
  81. package/src/tigerbeetle/src/test/state_checker.zig +0 -169
  82. package/src/tigerbeetle/src/test/storage.zig +0 -864
  83. 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.000001,
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,98 +186,278 @@ 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;
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;
321
+ }
322
+
323
+ for (simulator.cluster.clients) |*client| {
324
+ if (client.request_queue.count > 0) return false;
325
+ }
326
+ return true;
327
+ }
328
+
329
+ pub fn tick(simulator: *Simulator) void {
330
+ // TODO(Zig): Remove (see on_cluster_reply()).
331
+ simulator.cluster.context = simulator;
332
+
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
+ );
377
+ }
378
+ }
379
+ }
380
+
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
+ }
267
387
  } else {
268
- break :replicas cluster.replicas[0].quorum_view_change;
388
+ if (chance(simulator.random, simulator.options.request_idle_on_probability)) {
389
+ simulator.requests_idle = true;
390
+ }
269
391
  }
270
- };
271
392
 
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;
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;
414
+ }
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);
444
+ }
277
445
 
278
- for (cluster.storages) |*storage, replica| {
279
- if (cluster.replicas[replica].journal.status == .recovered) {
446
+ fn tick_crash(simulator: *Simulator) void {
447
+ // The maximum number of replicas that can crash, with the cluster still able to recover.
448
+ var crashes = blk: {
449
+ // The minimum number of healthy replicas required for a crashed replica to be able to
450
+ // recover. A cluster of 1 can crash safely (as long as there is no disk corruption)
451
+ // since it does not run the recovery protocol.
452
+ var replica_normal_min = if (simulator.options.cluster.replica_count == 1)
453
+ 0
454
+ else
455
+ vsr.quorums(simulator.options.cluster.replica_count).view_change;
456
+ break :blk simulator.cluster.replica_normal_count() -| replica_normal_min;
457
+ };
458
+
459
+ for (simulator.cluster.storages) |*storage, replica| {
460
+ if (simulator.cluster.replicas[replica].journal.status == .recovered) {
280
461
  // TODO Remove this workaround when VSR recovery protocol is disabled.
281
462
  // When only the minimum number of replicas are healthy (no more crashes allowed),
282
463
  // disable storage faults on all healthy replicas.
@@ -286,7 +467,7 @@ pub fn main() !void {
286
467
  // not have the prepare. The two healthy replicas can never complete a view change,
287
468
  // because two replicas are not enough to nack, and the unhealthy replica cannot
288
469
  // complete the VSR recovery protocol either.
289
- if (cluster.health[replica] == .up and crashes == 0) {
470
+ if (simulator.cluster.replica_health[replica] == .up and crashes == 0) {
290
471
  if (storage.faulty) {
291
472
  log_simulator.debug("{}: disable storage faults", .{replica});
292
473
  storage.faulty = false;
@@ -302,73 +483,57 @@ pub fn main() !void {
302
483
  }
303
484
  }
304
485
 
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
- };
486
+ for (simulator.cluster.replicas) |*replica| {
487
+ simulator.replica_stability[replica.replica] -|= 1;
488
+ const stability = simulator.replica_stability[replica.replica];
489
+ if (stability > 0) continue;
315
490
 
316
- if (ticks.* != 0) continue;
491
+ switch (simulator.cluster.replica_health[replica.replica]) {
492
+ .up => {
317
493
  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;
494
+ const replica_writes = simulator.cluster.storages[replica.replica].writes.count();
495
+ const crash_probability = simulator.options.replica_crash_probability *
496
+ @as(f64, if (replica_writes == 0) 1.0 else 10.0);
497
+ if (!chance_f64(simulator.random, crash_probability)) continue;
498
+
499
+ const replica_crashed = simulator.cluster.crash_replica(replica.replica) catch |err| {
500
+ log_simulator.err("{}: crash replica: unable to open after crash (err={})", .{
501
+ replica.replica,
502
+ err,
503
+ });
504
+ unreachable;
505
+ };
506
+ if (replica_crashed) {
507
+ log_simulator.debug("{}: crash replica", .{replica.replica});
508
+ crashes -= 1;
509
+ simulator.replica_stability[replica.replica] =
510
+ simulator.options.replica_crash_stability;
322
511
  }
323
-
324
- if (!try cluster.crash_replica(replica.replica)) continue;
325
- log_simulator.debug("{}: crash replica", .{replica.replica});
326
- crashes -= 1;
327
512
  },
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();
513
+ .down => {
333
514
  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 };
515
+ if (chance_f64(simulator.random, simulator.options.replica_restart_probability)) {
516
+ simulator.cluster.restart_replica(replica.replica);
336
517
  log_simulator.debug("{}: restart replica", .{replica.replica});
518
+ simulator.replica_stability[replica.replica] =
519
+ simulator.options.replica_restart_stability;
337
520
  }
338
521
  },
339
522
  }
340
523
  }
341
-
342
- cluster.network.packet_simulator.tick(cluster.health);
343
- conductor.tick();
344
-
345
- if (state_checker.convergence() and conductor.done() and
346
- cluster.replica_up_count() == replica_count)
347
- {
348
- break;
349
- }
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", .{});
353
524
  }
354
-
355
- assert(state_checker.convergence());
356
- assert(conductor.done());
357
-
358
- output.info("\n PASSED ({} ticks)", .{tick});
359
- }
360
-
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,
366
525
  };
367
526
 
368
527
  /// 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 {
528
+ fn fatal(failure: Failure, comptime fmt_string: []const u8, args: anytype) noreturn {
370
529
  output.err(fmt_string, args);
371
- std.os.exit(@enumToInt(exit_code));
530
+ std.os.exit(@enumToInt(failure));
531
+ }
532
+
533
+ /// Returns true, `p` percent of the time, else false.
534
+ fn chance(random: std.rand.Random, p: u8) bool {
535
+ assert(p <= 100);
536
+ return random.uintLessThanBiased(u8, 100) < p;
372
537
  }
373
538
 
374
539
  /// Returns true, `p` percent of the time, else false.
@@ -383,29 +548,10 @@ fn args_next(args: *std.process.ArgIterator, allocator: std.mem.Allocator) ?[:0]
383
548
  return err_or_bytes catch @panic("Unable to extract next value from args");
384
549
  }
385
550
 
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
551
+ /// Returns a random partitioning mode.
405
552
  fn random_partition_mode(random: std.rand.Random) PartitionMode {
406
553
  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;
554
+ var enumAsInt = random.uintAtMost(typeInfo.tag_type, typeInfo.fields.len - 1);
409
555
  return @intToEnum(PartitionMode, enumAsInt);
410
556
  }
411
557