tigerbeetle-node 0.11.5 → 0.11.7

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 (77) hide show
  1. package/dist/.client.node.sha256 +1 -1
  2. package/dist/index.d.ts +41 -42
  3. package/dist/index.js +41 -42
  4. package/dist/index.js.map +1 -1
  5. package/package.json +2 -2
  6. package/src/index.ts +0 -1
  7. package/src/tigerbeetle/scripts/benchmark.bat +7 -3
  8. package/src/tigerbeetle/scripts/benchmark.sh +2 -3
  9. package/src/tigerbeetle/scripts/install.bat +7 -0
  10. package/src/tigerbeetle/scripts/install.sh +2 -3
  11. package/src/tigerbeetle/src/benchmark.zig +3 -3
  12. package/src/tigerbeetle/src/config.zig +24 -3
  13. package/src/tigerbeetle/src/constants.zig +8 -5
  14. package/src/tigerbeetle/src/ewah.zig +6 -5
  15. package/src/tigerbeetle/src/ewah_fuzz.zig +1 -1
  16. package/src/tigerbeetle/src/io/darwin.zig +19 -0
  17. package/src/tigerbeetle/src/io/linux.zig +8 -0
  18. package/src/tigerbeetle/src/io/windows.zig +20 -2
  19. package/src/tigerbeetle/src/iops.zig +7 -1
  20. package/src/tigerbeetle/src/lsm/compaction.zig +27 -72
  21. package/src/tigerbeetle/src/lsm/forest_fuzz.zig +10 -11
  22. package/src/tigerbeetle/src/lsm/grid.zig +267 -267
  23. package/src/tigerbeetle/src/lsm/groove.zig +3 -0
  24. package/src/tigerbeetle/src/lsm/level_iterator.zig +18 -1
  25. package/src/tigerbeetle/src/lsm/manifest.zig +29 -1
  26. package/src/tigerbeetle/src/lsm/manifest_level.zig +1 -0
  27. package/src/tigerbeetle/src/lsm/manifest_log.zig +5 -5
  28. package/src/tigerbeetle/src/lsm/manifest_log_fuzz.zig +19 -11
  29. package/src/tigerbeetle/src/lsm/merge_iterator.zig +106 -0
  30. package/src/tigerbeetle/src/lsm/posted_groove.zig +1 -0
  31. package/src/tigerbeetle/src/lsm/segmented_array.zig +1 -0
  32. package/src/tigerbeetle/src/lsm/set_associative_cache.zig +26 -70
  33. package/src/tigerbeetle/src/lsm/table.zig +56 -0
  34. package/src/tigerbeetle/src/lsm/table_iterator.zig +29 -2
  35. package/src/tigerbeetle/src/lsm/table_mutable.zig +49 -15
  36. package/src/tigerbeetle/src/lsm/test.zig +10 -7
  37. package/src/tigerbeetle/src/lsm/tree.zig +27 -6
  38. package/src/tigerbeetle/src/lsm/tree_fuzz.zig +302 -263
  39. package/src/tigerbeetle/src/message_pool.zig +2 -1
  40. package/src/tigerbeetle/src/simulator.zig +22 -84
  41. package/src/tigerbeetle/src/{test/accounting → state_machine}/auditor.zig +8 -8
  42. package/src/tigerbeetle/src/{test/accounting → state_machine}/workload.zig +108 -48
  43. package/src/tigerbeetle/src/state_machine.zig +20 -14
  44. package/src/tigerbeetle/src/storage.zig +58 -6
  45. package/src/tigerbeetle/src/test/cluster.zig +14 -11
  46. package/src/tigerbeetle/src/test/conductor.zig +2 -3
  47. package/src/tigerbeetle/src/test/id.zig +10 -0
  48. package/src/tigerbeetle/src/test/state_checker.zig +1 -1
  49. package/src/tigerbeetle/src/test/state_machine.zig +151 -46
  50. package/src/tigerbeetle/src/test/storage.zig +22 -1
  51. package/src/tigerbeetle/src/tigerbeetle.zig +0 -1
  52. package/src/tigerbeetle/src/tracer.zig +50 -28
  53. package/src/tigerbeetle/src/unit_tests.zig +11 -6
  54. package/src/tigerbeetle/src/vopr.zig +4 -4
  55. package/src/tigerbeetle/src/vsr/client.zig +5 -5
  56. package/src/tigerbeetle/src/vsr/clock.zig +2 -2
  57. package/src/tigerbeetle/src/vsr/journal.zig +647 -537
  58. package/src/tigerbeetle/src/vsr/replica.zig +333 -333
  59. package/src/tigerbeetle/src/vsr/replica_format.zig +7 -4
  60. package/src/tigerbeetle/src/vsr/superblock.zig +87 -39
  61. package/src/tigerbeetle/src/vsr/superblock_free_set.zig +114 -93
  62. package/src/tigerbeetle/src/vsr/superblock_free_set_fuzz.zig +1 -1
  63. package/src/tigerbeetle/src/vsr/superblock_fuzz.zig +11 -8
  64. package/src/tigerbeetle/src/vsr/superblock_quorums_fuzz.zig +3 -3
  65. package/src/tigerbeetle/src/vsr.zig +60 -13
  66. package/src/tigerbeetle/src/c/tb_client/context.zig +0 -304
  67. package/src/tigerbeetle/src/c/tb_client/echo_client.zig +0 -108
  68. package/src/tigerbeetle/src/c/tb_client/packet.zig +0 -80
  69. package/src/tigerbeetle/src/c/tb_client/signal.zig +0 -286
  70. package/src/tigerbeetle/src/c/tb_client/thread.zig +0 -88
  71. package/src/tigerbeetle/src/c/tb_client.h +0 -221
  72. package/src/tigerbeetle/src/c/tb_client.zig +0 -177
  73. package/src/tigerbeetle/src/c/tb_client_header.zig +0 -218
  74. package/src/tigerbeetle/src/c/tb_client_header_test.zig +0 -135
  75. package/src/tigerbeetle/src/c/test.zig +0 -371
  76. package/src/tigerbeetle/src/cli.zig +0 -375
  77. package/src/tigerbeetle/src/main.zig +0 -245
@@ -22,7 +22,8 @@ const message_size_max_padded = constants.message_size_max + constants.sector_si
22
22
  pub const messages_max_replica = messages_max: {
23
23
  var sum: usize = 0;
24
24
 
25
- sum += constants.journal_iops_read_max + constants.journal_iops_write_max; // Journal I/O
25
+ sum += constants.journal_iops_read_max; // Journal reads
26
+ sum += constants.journal_iops_write_max; // Journal writes
26
27
  sum += constants.clients_max; // SuperBlock.client_table
27
28
  sum += 1; // Replica.loopback_queue
28
29
  sum += constants.pipeline_max; // Replica.pipeline
@@ -17,9 +17,7 @@ const StateChecker = @import("test/state_checker.zig").StateChecker;
17
17
  const StorageChecker = @import("test/storage_checker.zig").StorageChecker;
18
18
  const PartitionMode = @import("test/packet_simulator.zig").PartitionMode;
19
19
  const MessageBus = @import("test/message_bus.zig").MessageBus;
20
- const auditor = @import("test/accounting/auditor.zig");
21
- const Workload = @import("test/accounting/workload.zig").WorkloadType(StateMachine);
22
- const Conductor = @import("test/conductor.zig").ConductorType(Client, MessageBus, StateMachine, Workload);
20
+ const Conductor = @import("test/conductor.zig").ConductorType(Client, MessageBus, StateMachine);
23
21
  const IdPermutation = @import("test/id.zig").IdPermutation;
24
22
 
25
23
  /// The `log` namespace in this root file is required to implement our custom `log` function.
@@ -88,16 +86,17 @@ pub fn main() !void {
88
86
  const idle_on_probability = random.uintLessThan(u8, 20);
89
87
  const idle_off_probability = 10 + random.uintLessThan(u8, 10);
90
88
 
91
- // TODO: When block recovery and state transfer are implemented, remove this flag to allow
92
- // crashes to coexist with WAL wraps.
89
+ // The maximum number of transitions from calling `client.request()`, not including
90
+ // `register` messages.
93
91
  const requests_committed_max: usize = constants.journal_slot_count * 3;
94
92
 
95
93
  const cluster_options: ClusterOptions = .{
96
94
  .cluster = cluster_id,
97
95
  .replica_count = replica_count,
98
96
  .client_count = client_count,
99
- // TODO Compute an upper-bound for this based on requests_committed_max.
100
- .grid_size_max = 1024 * 1024 * 256,
97
+ .storage_size_limit = vsr.sector_floor(
98
+ constants.storage_size_max - random.uintLessThan(u64, constants.storage_size_max / 10),
99
+ ),
101
100
  .seed = random.int(u64),
102
101
  .on_change_state = on_replica_change_state,
103
102
  .on_compact = on_replica_compact,
@@ -132,11 +131,7 @@ pub fn main() !void {
132
131
  .write_latency_mean = 3 + random.uintLessThan(u16, 100),
133
132
  .read_fault_probability = random.uintLessThan(u8, 10),
134
133
  .write_fault_probability = random.uintLessThan(u8, 10),
135
- // TODO Allow WAL faults on crash when replica_count=1 when redundant-header-repair
136
- // is implemented after recovering with decision=fix. Otherwise we can end up with
137
- // multiple crashes faulting first a redundant headers, then a prepare, upgrading
138
- // a decision=fix to decision=vsr.
139
- .crash_fault_probability = if (replica_count == 1) 0 else 80 + random.uintLessThan(u8, 21),
134
+ .crash_fault_probability = 80 + random.uintLessThan(u8, 21),
140
135
  .faulty_superblock = true,
141
136
  },
142
137
  .health_options = .{
@@ -145,60 +140,16 @@ pub fn main() !void {
145
140
  .restart_probability = 0.0001,
146
141
  .restart_stability = random.uintLessThan(u32, 1_000),
147
142
  },
148
- .state_machine_options = .{
149
- // TODO What should these fields be set to? Can they be randomized (and with what constraints)?
150
- .lsm_forest_node_count = 4096,
151
- .cache_entries_accounts = 2048,
152
- .cache_entries_transfers = 2048,
153
- .cache_entries_posted = 2048,
154
- },
155
- };
156
-
157
- const workload_options: Workload.Options = .{
158
- .auditor_options = .{
159
- .accounts_max = 2 + random.uintLessThan(usize, 128),
160
- .account_id_permutation = random_id_permutation(random),
161
- .client_count = client_count,
162
- .transfers_pending_max = 256,
163
- .in_flight_max = Conductor.stalled_queue_capacity,
164
- },
165
- .transfer_id_permutation = random_id_permutation(random),
166
- .operations = .{
167
- .create_accounts = 1 + random.uintLessThan(usize, 10),
168
- .create_transfers = 1 + random.uintLessThan(usize, 100),
169
- .lookup_accounts = 1 + random.uintLessThan(usize, 20),
170
- .lookup_transfers = 1 + random.uintLessThan(usize, 20),
171
- },
172
- .create_account_invalid_probability = 1,
173
- .create_transfer_invalid_probability = 1,
174
- .create_transfer_limit_probability = random.uintLessThan(u8, 101),
175
- .create_transfer_pending_probability = 1 + random.uintLessThan(u8, 100),
176
- .create_transfer_post_probability = 1 + random.uintLessThan(u8, 50),
177
- .create_transfer_void_probability = 1 + random.uintLessThan(u8, 50),
178
- .lookup_account_invalid_probability = 1,
179
- .lookup_transfer = .{
180
- .delivered = 1 + random.uintLessThan(usize, 10),
181
- .sending = 1 + random.uintLessThan(usize, 10),
143
+ // TODO(dj) SimulatorType(StateMachine).init(state_machine_options)
144
+ .state_machine_options = switch (constants.state_machine) {
145
+ .testing => .{},
146
+ .accounting => .{
147
+ .lsm_forest_node_count = 4096,
148
+ .cache_entries_accounts = if (random.boolean()) 0 else 2048,
149
+ .cache_entries_transfers = 0,
150
+ .cache_entries_posted = if (random.boolean()) 0 else 2048,
151
+ },
182
152
  },
183
- .lookup_transfer_span_mean = 10 + random.uintLessThan(usize, 1000),
184
- .account_limit_probability = random.uintLessThan(u8, 80),
185
- .linked_valid_probability = random.uintLessThan(u8, 101),
186
- // 100% chance because this only applies to consecutive invalid transfers, which are rare.
187
- .linked_invalid_probability = 100,
188
- // TODO(Timeouts): When timeouts are implemented in the StateMachine, change this to the
189
- // (commented out) value so that timeouts can actually trigger.
190
- .pending_timeout_mean = std.math.maxInt(u64) / 2,
191
- // .pending_timeout_mean = 1 + random.uintLessThan(usize, 1_000_000_000 / 4),
192
- .accounts_batch_size_min = 0,
193
- .accounts_batch_size_span = 1 + random.uintLessThan(
194
- usize,
195
- StateMachine.constants.batch_max.create_accounts,
196
- ),
197
- .transfers_batch_size_min = 0,
198
- .transfers_batch_size_span = 1 + random.uintLessThan(
199
- usize,
200
- StateMachine.constants.batch_max.create_transfers,
201
- ),
202
153
  };
203
154
 
204
155
  output.info(
@@ -266,7 +217,12 @@ pub fn main() !void {
266
217
  cluster = try Cluster.create(allocator, random, cluster_options);
267
218
  defer cluster.destroy();
268
219
 
269
- var workload = try Workload.init(allocator, random, workload_options);
220
+ const workload_options = StateMachine.Workload.Options.generate(random, .{
221
+ .client_count = client_count,
222
+ .in_flight_max = Conductor.stalled_queue_capacity,
223
+ });
224
+
225
+ var workload = try StateMachine.Workload.init(allocator, random, workload_options);
270
226
  defer workload.deinit(allocator);
271
227
 
272
228
  var conductor = try Conductor.init(allocator, random, &workload, .{
@@ -313,14 +269,6 @@ pub fn main() !void {
313
269
  }
314
270
  };
315
271
 
316
- // Disable most faults at startup, so that the replicas don't get stuck in recovery mode.
317
- for (cluster.storages) |*storage, i| {
318
- storage.faulty = replica_normal_min <= i;
319
- }
320
-
321
- // The maximum number of transitions from calling `client.request()`, not including
322
- // `register` messages.
323
- // TODO When storage is supported, run more transitions than fit in the journal.
324
272
  var tick: u64 = 0;
325
273
  while (tick < ticks_max) : (tick += 1) {
326
274
  const health_options = &cluster.options.health_options;
@@ -461,16 +409,6 @@ fn random_partition_mode(random: std.rand.Random) PartitionMode {
461
409
  return @intToEnum(PartitionMode, enumAsInt);
462
410
  }
463
411
 
464
- fn random_id_permutation(random: std.rand.Random) IdPermutation {
465
- return switch (random.uintLessThan(usize, 4)) {
466
- 0 => .{ .identity = {} },
467
- 1 => .{ .inversion = {} },
468
- 2 => .{ .zigzag = {} },
469
- 3 => .{ .random = random.int(u64) },
470
- else => unreachable,
471
- };
472
- }
473
-
474
412
  pub fn parse_seed(bytes: []const u8) u64 {
475
413
  return std.fmt.parseUnsigned(u64, bytes, 10) catch |err| switch (err) {
476
414
  error.Overflow => @panic("seed exceeds a 64-bit unsigned integer"),
@@ -6,16 +6,16 @@ const std = @import("std");
6
6
  const assert = std.debug.assert;
7
7
  const log = std.log.scoped(.test_auditor);
8
8
 
9
- const constants = @import("../../constants.zig");
10
- const tb = @import("../../tigerbeetle.zig");
11
- const vsr = @import("../../vsr.zig");
12
- const RingBuffer = @import("../../ring_buffer.zig").RingBuffer;
13
- const IdPermutation = @import("../id.zig").IdPermutation;
9
+ const constants = @import("../constants.zig");
10
+ const tb = @import("../tigerbeetle.zig");
11
+ const vsr = @import("../vsr.zig");
12
+ const RingBuffer = @import("../ring_buffer.zig").RingBuffer;
13
+ const IdPermutation = @import("../test/id.zig").IdPermutation;
14
14
 
15
15
  // TODO(zig) This won't be necessary in Zig 0.10.
16
- const PriorityQueue = @import("../priority_queue.zig").PriorityQueue;
17
- const Storage = @import("../storage.zig").Storage;
18
- const StateMachine = @import("../../state_machine.zig").StateMachineType(Storage, .{
16
+ const PriorityQueue = @import("../test/priority_queue.zig").PriorityQueue;
17
+ const Storage = @import("../test/storage.zig").Storage;
18
+ const StateMachine = @import("../state_machine.zig").StateMachineType(Storage, .{
19
19
  .message_body_size_max = constants.message_body_size_max,
20
20
  });
21
21
 
@@ -21,18 +21,16 @@ const std = @import("std");
21
21
  const assert = std.debug.assert;
22
22
  const log = std.log.scoped(.test_workload);
23
23
 
24
- const constants = @import("../../constants.zig");
25
- const tb = @import("../../tigerbeetle.zig");
26
- const vsr = @import("../../vsr.zig");
27
- const accounting_auditor = @import("./auditor.zig");
24
+ const constants = @import("../constants.zig");
25
+ const tb = @import("../tigerbeetle.zig");
26
+ const vsr = @import("../vsr.zig");
27
+ const accounting_auditor = @import("auditor.zig");
28
28
  const Auditor = accounting_auditor.AccountingAuditor;
29
- const IdPermutation = @import("../id.zig").IdPermutation;
30
- const fuzz = @import("../fuzz.zig");
29
+ const IdPermutation = @import("../test/id.zig").IdPermutation;
30
+ const fuzz = @import("../test/fuzz.zig");
31
31
 
32
32
  // TODO(zig) This won't be necessary in Zig 0.10.
33
- const PriorityQueue = @import("../priority_queue.zig").PriorityQueue;
34
-
35
- // TODO Test linked create_accounts.
33
+ const PriorityQueue = @import("../test/priority_queue.zig").PriorityQueue;
36
34
 
37
35
  const TransferOutcome = enum {
38
36
  /// The transfer is guaranteed to commit.
@@ -177,44 +175,7 @@ pub fn WorkloadType(comptime AccountingStateMachine: type) type {
177
175
  return struct {
178
176
  const Self = @This();
179
177
 
180
- pub const Options = struct {
181
- auditor_options: Auditor.Options,
182
- transfer_id_permutation: IdPermutation,
183
-
184
- operations: std.enums.EnumFieldStruct(Action, usize, null),
185
-
186
- create_account_invalid_probability: u8, // ≤ 100
187
- create_transfer_invalid_probability: u8, // ≤ 100
188
- create_transfer_limit_probability: u8, // ≤ 100
189
- create_transfer_pending_probability: u8, // ≤ 100
190
- create_transfer_post_probability: u8, // ≤ 100
191
- create_transfer_void_probability: u8, // ≤ 100
192
- lookup_account_invalid_probability: u8, // ≤ 100
193
-
194
- lookup_transfer: std.enums.EnumFieldStruct(enum {
195
- /// Query a transfer that has either been committed or rejected.
196
- delivered,
197
- /// Query a transfer whose `create_transfers` is in-flight.
198
- sending,
199
- }, usize, null),
200
-
201
- // Size of timespan for querying, measured in transfers
202
- lookup_transfer_span_mean: usize,
203
-
204
- account_limit_probability: u8, // ≤ 100
205
-
206
- /// This probability is only checked for consecutive guaranteed-successful transfers.
207
- linked_valid_probability: u8,
208
- /// This probability is only checked for consecutive invalid transfers.
209
- linked_invalid_probability: u8,
210
-
211
- pending_timeout_mean: u64,
212
-
213
- accounts_batch_size_min: usize,
214
- accounts_batch_size_span: usize, // inclusive
215
- transfers_batch_size_min: usize,
216
- transfers_batch_size_span: usize, // inclusive
217
- };
178
+ pub const Options = OptionsType(AccountingStateMachine, Action);
218
179
 
219
180
  random: std.rand.Random,
220
181
  auditor: Auditor,
@@ -303,7 +264,11 @@ pub fn WorkloadType(comptime AccountingStateMachine: type) type {
303
264
  }
304
265
 
305
266
  /// A client may build multiple requests to queue up while another is in-flight.
306
- pub fn build_request(self: *Self, client_index: usize, body: []align(@alignOf(vsr.Header)) u8) struct {
267
+ pub fn build_request(
268
+ self: *Self,
269
+ client_index: usize,
270
+ body: []align(@alignOf(vsr.Header)) u8,
271
+ ) struct {
307
272
  operation: Operation,
308
273
  size: usize,
309
274
  } {
@@ -785,6 +750,101 @@ pub fn WorkloadType(comptime AccountingStateMachine: type) type {
785
750
  };
786
751
  }
787
752
 
753
+ fn OptionsType(comptime StateMachine: type, comptime Action: type) type {
754
+ return struct {
755
+ const Options = @This();
756
+
757
+ auditor_options: Auditor.Options,
758
+ transfer_id_permutation: IdPermutation,
759
+
760
+ operations: std.enums.EnumFieldStruct(Action, usize, null),
761
+
762
+ create_account_invalid_probability: u8, // ≤ 100
763
+ create_transfer_invalid_probability: u8, // ≤ 100
764
+ create_transfer_limit_probability: u8, // ≤ 100
765
+ create_transfer_pending_probability: u8, // ≤ 100
766
+ create_transfer_post_probability: u8, // ≤ 100
767
+ create_transfer_void_probability: u8, // ≤ 100
768
+ lookup_account_invalid_probability: u8, // ≤ 100
769
+
770
+ lookup_transfer: std.enums.EnumFieldStruct(enum {
771
+ /// Query a transfer that has either been committed or rejected.
772
+ delivered,
773
+ /// Query a transfer whose `create_transfers` is in-flight.
774
+ sending,
775
+ }, usize, null),
776
+
777
+ // Size of timespan for querying, measured in transfers
778
+ lookup_transfer_span_mean: usize,
779
+
780
+ account_limit_probability: u8, // ≤ 100
781
+
782
+ /// This probability is only checked for consecutive guaranteed-successful transfers.
783
+ linked_valid_probability: u8,
784
+ /// This probability is only checked for consecutive invalid transfers.
785
+ linked_invalid_probability: u8,
786
+
787
+ pending_timeout_mean: u64,
788
+
789
+ accounts_batch_size_min: usize,
790
+ accounts_batch_size_span: usize, // inclusive
791
+ transfers_batch_size_min: usize,
792
+ transfers_batch_size_span: usize, // inclusive
793
+
794
+ pub fn generate(random: std.rand.Random, options: struct {
795
+ client_count: usize,
796
+ in_flight_max: usize,
797
+ }) Options {
798
+ return .{
799
+ .auditor_options = .{
800
+ .accounts_max = 2 + random.uintLessThan(usize, 128),
801
+ .account_id_permutation = IdPermutation.generate(random),
802
+ .client_count = options.client_count,
803
+ .transfers_pending_max = 256,
804
+ .in_flight_max = options.in_flight_max,
805
+ },
806
+ .transfer_id_permutation = IdPermutation.generate(random),
807
+ .operations = .{
808
+ .create_accounts = 1 + random.uintLessThan(usize, 10),
809
+ .create_transfers = 1 + random.uintLessThan(usize, 100),
810
+ .lookup_accounts = 1 + random.uintLessThan(usize, 20),
811
+ .lookup_transfers = 1 + random.uintLessThan(usize, 20),
812
+ },
813
+ .create_account_invalid_probability = 1,
814
+ .create_transfer_invalid_probability = 1,
815
+ .create_transfer_limit_probability = random.uintLessThan(u8, 101),
816
+ .create_transfer_pending_probability = 1 + random.uintLessThan(u8, 100),
817
+ .create_transfer_post_probability = 1 + random.uintLessThan(u8, 50),
818
+ .create_transfer_void_probability = 1 + random.uintLessThan(u8, 50),
819
+ .lookup_account_invalid_probability = 1,
820
+ .lookup_transfer = .{
821
+ .delivered = 1 + random.uintLessThan(usize, 10),
822
+ .sending = 1 + random.uintLessThan(usize, 10),
823
+ },
824
+ .lookup_transfer_span_mean = 10 + random.uintLessThan(usize, 1000),
825
+ .account_limit_probability = random.uintLessThan(u8, 80),
826
+ .linked_valid_probability = random.uintLessThan(u8, 101),
827
+ // 100% chance because this only applies to consecutive invalid transfers, which are rare.
828
+ .linked_invalid_probability = 100,
829
+ // TODO(Timeouts): When timeouts are implemented in the StateMachine, change this to the
830
+ // (commented out) value so that timeouts can actually trigger.
831
+ .pending_timeout_mean = std.math.maxInt(u64) / 2,
832
+ // .pending_timeout_mean = 1 + random.uintLessThan(usize, 1_000_000_000 / 4),
833
+ .accounts_batch_size_min = 0,
834
+ .accounts_batch_size_span = 1 + random.uintLessThan(
835
+ usize,
836
+ StateMachine.constants.batch_max.create_accounts,
837
+ ),
838
+ .transfers_batch_size_min = 0,
839
+ .transfers_batch_size_span = 1 + random.uintLessThan(
840
+ usize,
841
+ StateMachine.constants.batch_max.create_transfers,
842
+ ),
843
+ };
844
+ }
845
+ };
846
+ }
847
+
788
848
  /// Sample from a discrete distribution.
789
849
  /// Use integers instead of floating-point numbers to avoid nondeterminism on different hardware.
790
850
  fn sample_distribution(
@@ -8,6 +8,7 @@ const tracer = @import("tracer.zig");
8
8
 
9
9
  const tb = @import("tigerbeetle.zig");
10
10
  const snapshot_latest = @import("lsm/tree.zig").snapshot_latest;
11
+ const WorkloadType = @import("state_machine/workload.zig").WorkloadType;
11
12
 
12
13
  const Account = tb.Account;
13
14
  const AccountFlags = tb.AccountFlags;
@@ -26,7 +27,6 @@ pub fn StateMachineType(comptime Storage: type, comptime constants_: struct {
26
27
  }) type {
27
28
  return struct {
28
29
  const StateMachine = @This();
29
-
30
30
  const Grid = @import("lsm/grid.zig").GridType(Storage);
31
31
  const GrooveType = @import("lsm/groove.zig").GrooveType;
32
32
  const ForestType = @import("lsm/forest.zig").ForestType;
@@ -49,6 +49,8 @@ pub fn StateMachineType(comptime Storage: type, comptime constants_: struct {
49
49
  );
50
50
  const PostedGroove = @import("lsm/posted_groove.zig").PostedGrooveType(Storage);
51
51
 
52
+ pub const Workload = WorkloadType(StateMachine);
53
+
52
54
  pub const Forest = ForestType(Storage, .{
53
55
  .accounts = AccountsGroove,
54
56
  .transfers = TransfersGroove,
@@ -180,13 +182,13 @@ pub fn StateMachineType(comptime Storage: type, comptime constants_: struct {
180
182
  /// Returns the header's timestamp.
181
183
  pub fn prepare(self: *StateMachine, operation: Operation, input: []u8) u64 {
182
184
  switch (operation) {
185
+ .reserved => unreachable,
183
186
  .root => unreachable,
184
187
  .register => {},
185
188
  .create_accounts => self.prepare_timestamps(.create_accounts, input),
186
189
  .create_transfers => self.prepare_timestamps(.create_transfers, input),
187
190
  .lookup_accounts => {},
188
191
  .lookup_transfers => {},
189
- else => unreachable,
190
192
  }
191
193
  return self.prepare_timestamp;
192
194
  }
@@ -660,10 +662,7 @@ pub fn StateMachineType(comptime Storage: type, comptime constants_: struct {
660
662
  if (t.credit_account_id == t.debit_account_id) return .accounts_must_be_different;
661
663
 
662
664
  if (t.pending_id != 0) return .pending_id_must_be_zero;
663
- if (t.flags.pending) {
664
- // Otherwise, reserved amounts may never be released.
665
- if (t.timeout == 0) return .pending_transfer_must_timeout;
666
- } else {
665
+ if (!t.flags.pending) {
667
666
  if (t.timeout != 0) return .timeout_reserved_for_pending_transfer;
668
667
  }
669
668
 
@@ -822,8 +821,9 @@ pub fn StateMachineType(comptime Storage: type, comptime constants_: struct {
822
821
  }
823
822
 
824
823
  assert(p.timestamp < t.timestamp);
825
- assert(p.timeout > 0);
826
- if (p.timestamp + p.timeout <= t.timestamp) return .pending_transfer_expired;
824
+ if (p.timeout > 0) {
825
+ if (p.timestamp + p.timeout <= t.timestamp) return .pending_transfer_expired;
826
+ }
827
827
 
828
828
  self.forest.grooves.transfers.put_no_clobber(&Transfer{
829
829
  .id = t.id,
@@ -1113,6 +1113,7 @@ test "sum_overflows" {
1113
1113
  const TestContext = struct {
1114
1114
  const Storage = @import("test/storage.zig").Storage;
1115
1115
  const MessagePool = @import("message_pool.zig").MessagePool;
1116
+ const data_file_size_min = @import("vsr/superblock.zig").data_file_size_min;
1116
1117
  const SuperBlock = @import("vsr/superblock.zig").SuperBlockType(Storage);
1117
1118
  const Grid = @import("lsm/grid.zig").GridType(Storage);
1118
1119
  const StateMachine = StateMachineType(Storage, .{
@@ -1147,7 +1148,11 @@ const TestContext = struct {
1147
1148
  };
1148
1149
  errdefer ctx.message_pool.deinit(allocator);
1149
1150
 
1150
- ctx.superblock = try SuperBlock.init(allocator, &ctx.storage, &ctx.message_pool);
1151
+ ctx.superblock = try SuperBlock.init(allocator, .{
1152
+ .storage = &ctx.storage,
1153
+ .storage_size_limit = data_file_size_min,
1154
+ .message_pool = &ctx.message_pool,
1155
+ });
1151
1156
  errdefer ctx.superblock.deinit(allocator);
1152
1157
 
1153
1158
  // Pretend that the superblock is open so that the Forest can initialize.
@@ -1622,7 +1627,6 @@ test "create_transfers/lookup_transfers" {
1622
1627
  \\ transfer T1 A8 -0 _ _ T1 _ L0 C0 _ PEN _ _ _ 0 credit_account_id_must_not_be_int_max
1623
1628
  \\ transfer T1 A8 A8 _ _ T1 _ L0 C0 _ PEN _ _ _ 0 accounts_must_be_different
1624
1629
  \\ transfer T1 A8 A9 _ _ T1 _ L0 C0 _ PEN _ _ _ 0 pending_id_must_be_zero
1625
- \\ transfer T1 A8 A9 _ _ _ _ L0 C0 _ PEN _ _ _ 0 pending_transfer_must_timeout
1626
1630
  \\ transfer T1 A8 A9 _ _ _ -0 L0 C0 _ _ _ _ _ 0 timeout_reserved_for_pending_transfer
1627
1631
  \\ transfer T1 A8 A9 _ _ _ -0 L0 C0 _ PEN _ _ _ 0 ledger_must_not_be_zero
1628
1632
  \\ transfer T1 A8 A9 _ _ _ -0 L9 C0 _ PEN _ _ _ 0 code_must_not_be_zero
@@ -1680,11 +1684,12 @@ test "create/lookup 2-phase transfers" {
1680
1684
  \\ transfer T3 A1 A2 _ _ _ 50 L1 C1 _ PEN _ _ _ 15 ok
1681
1685
  \\ transfer T4 A1 A2 _ _ _ 1 L1 C1 _ PEN _ _ _ 15 ok
1682
1686
  \\ transfer T5 A1 A2 U9 _ _ 50 L1 C1 _ PEN _ _ _ 7 ok
1687
+ \\ transfer T6 A1 A2 _ _ _ 0 L1 C1 _ PEN _ _ _ 1 ok
1683
1688
  \\ commit create_transfers
1684
1689
 
1685
1690
  // Check balances before resolving.
1686
- \\ lookup_account A1 52 15 0 0
1687
- \\ lookup_account A2 0 0 52 15
1691
+ \\ lookup_account A1 53 15 0 0
1692
+ \\ lookup_account A2 0 0 53 15
1688
1693
  \\ commit lookup_accounts
1689
1694
 
1690
1695
  // Second phase.
@@ -1715,11 +1720,12 @@ test "create/lookup 2-phase transfers" {
1715
1720
  \\ transfer T102 A1 A2 U1 _ T3 _ L1 C1 _ _ POS _ _ 13 pending_transfer_already_voided
1716
1721
  \\ transfer T102 A1 A2 U1 _ T4 _ L1 C1 _ _ _ VOI _ 15 pending_transfer_expired
1717
1722
  \\ transfer T105 A0 A0 U0 _ T5 _ L0 C0 _ _ POS _ _ _ ok
1723
+ \\ transfer T106 A0 A0 U0 _ T6 _ L1 C1 _ _ POS _ _ 0 ok
1718
1724
  \\ commit create_transfers
1719
1725
 
1720
1726
  // Check balances after resolving.
1721
- \\ lookup_account A1 15 35 0 0
1722
- \\ lookup_account A2 0 0 15 35
1727
+ \\ lookup_account A1 15 36 0 0
1728
+ \\ lookup_account A2 0 0 15 36
1723
1729
  \\ commit lookup_accounts
1724
1730
  );
1725
1731
  }
@@ -5,8 +5,8 @@ const assert = std.debug.assert;
5
5
  const log = std.log.scoped(.storage);
6
6
 
7
7
  const IO = @import("io.zig").IO;
8
+ const FIFO = @import("fifo.zig").FIFO;
8
9
  const constants = @import("constants.zig");
9
- const fatal = @import("cli.zig").fatal;
10
10
  const vsr = @import("vsr.zig");
11
11
 
12
12
  pub const Storage = struct {
@@ -68,9 +68,17 @@ pub const Storage = struct {
68
68
  offset: u64,
69
69
  };
70
70
 
71
+ pub const NextTick = struct {
72
+ next: ?*NextTick = null,
73
+ callback: fn (next_tick: *NextTick) void,
74
+ };
75
+
71
76
  io: *IO,
72
77
  fd: os.fd_t,
73
78
 
79
+ next_tick_queue: FIFO(NextTick) = .{},
80
+ next_tick_completion: IO.Completion = undefined,
81
+
74
82
  pub fn init(io: *IO, fd: os.fd_t) !Storage {
75
83
  return Storage{
76
84
  .io = io,
@@ -79,6 +87,7 @@ pub const Storage = struct {
79
87
  }
80
88
 
81
89
  pub fn deinit(storage: *Storage) void {
90
+ assert(storage.next_tick_queue.empty());
82
91
  assert(storage.fd != IO.INVALID_FILE);
83
92
  storage.fd = IO.INVALID_FILE;
84
93
  }
@@ -90,6 +99,43 @@ pub const Storage = struct {
90
99
  };
91
100
  }
92
101
 
102
+ pub fn on_next_tick(
103
+ storage: *Storage,
104
+ callback: fn (next_tick: *Storage.NextTick) void,
105
+ next_tick: *Storage.NextTick,
106
+ ) void {
107
+ next_tick.* = .{ .callback = callback };
108
+
109
+ const was_empty = storage.next_tick_queue.empty();
110
+ storage.next_tick_queue.push(next_tick);
111
+
112
+ if (was_empty) {
113
+ storage.io.timeout(
114
+ *Storage,
115
+ storage,
116
+ timeout_callback,
117
+ &storage.next_tick_completion,
118
+ 0, // 0ns timeout means to resolve as soon as possible - like a yield
119
+ );
120
+ }
121
+ }
122
+
123
+ fn timeout_callback(
124
+ storage: *Storage,
125
+ completion: *IO.Completion,
126
+ result: IO.TimeoutError!void,
127
+ ) void {
128
+ assert(completion == &storage.next_tick_completion);
129
+ _ = result catch |e| switch (e) {
130
+ error.Canceled => unreachable,
131
+ error.Unexpected => unreachable,
132
+ };
133
+
134
+ var queue = storage.next_tick_queue;
135
+ storage.next_tick_queue = .{};
136
+ while (queue.pop()) |next_tick| next_tick.callback(next_tick);
137
+ }
138
+
93
139
  pub fn read_sectors(
94
140
  self: *Storage,
95
141
  callback: fn (read: *Storage.Read) void,
@@ -113,18 +159,24 @@ pub const Storage = struct {
113
159
  .target_max = buffer.len,
114
160
  };
115
161
 
116
- self.start_read(read, 0);
162
+ self.start_read(read, null);
117
163
  assert(read.target().len > 0);
118
164
  }
119
165
 
120
- fn start_read(self: *Storage, read: *Storage.Read, bytes_read: usize) void {
121
- assert(bytes_read <= read.target().len);
166
+ fn start_read(self: *Storage, read: *Storage.Read, bytes_read: ?usize) void {
167
+ const bytes = bytes_read orelse 0;
168
+ assert(bytes <= read.target().len);
122
169
 
123
- read.offset += bytes_read;
124
- read.buffer = read.buffer[bytes_read..];
170
+ read.offset += bytes;
171
+ read.buffer = read.buffer[bytes..];
125
172
 
126
173
  const target = read.target();
127
174
  if (target.len == 0) {
175
+ // Resolving the read inline means start_read() must not have been called from
176
+ // read_sectors(). If it was, this is a synchronous callback resolution and should
177
+ // be reported.
178
+ assert(bytes_read != null);
179
+
128
180
  read.callback(read);
129
181
  return;
130
182
  }