tigerbeetle-node 0.10.0 → 0.11.1

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 (109) hide show
  1. package/README.md +302 -101
  2. package/dist/index.d.ts +70 -72
  3. package/dist/index.js +70 -72
  4. package/dist/index.js.map +1 -1
  5. package/package.json +9 -8
  6. package/scripts/download_node_headers.sh +14 -7
  7. package/src/index.ts +6 -10
  8. package/src/node.zig +6 -3
  9. package/src/tigerbeetle/scripts/benchmark.sh +4 -4
  10. package/src/tigerbeetle/scripts/confirm_image.sh +44 -0
  11. package/src/tigerbeetle/scripts/fuzz_loop.sh +15 -0
  12. package/src/tigerbeetle/scripts/fuzz_unique_errors.sh +7 -0
  13. package/src/tigerbeetle/scripts/install.sh +19 -4
  14. package/src/tigerbeetle/scripts/install_zig.bat +5 -1
  15. package/src/tigerbeetle/scripts/install_zig.sh +24 -14
  16. package/src/tigerbeetle/scripts/pre-commit.sh +9 -0
  17. package/src/tigerbeetle/scripts/shellcheck.sh +5 -0
  18. package/src/tigerbeetle/scripts/tests_on_alpine.sh +10 -0
  19. package/src/tigerbeetle/scripts/tests_on_ubuntu.sh +14 -0
  20. package/src/tigerbeetle/scripts/validate_docs.sh +17 -0
  21. package/src/tigerbeetle/src/benchmark.zig +29 -13
  22. package/src/tigerbeetle/src/c/tb_client/context.zig +248 -47
  23. package/src/tigerbeetle/src/c/tb_client/echo_client.zig +108 -0
  24. package/src/tigerbeetle/src/c/tb_client/packet.zig +2 -2
  25. package/src/tigerbeetle/src/c/tb_client/signal.zig +2 -4
  26. package/src/tigerbeetle/src/c/tb_client/thread.zig +17 -257
  27. package/src/tigerbeetle/src/c/tb_client.h +118 -84
  28. package/src/tigerbeetle/src/c/tb_client.zig +88 -23
  29. package/src/tigerbeetle/src/c/tb_client_header_test.zig +135 -0
  30. package/src/tigerbeetle/src/c/test.zig +371 -1
  31. package/src/tigerbeetle/src/cli.zig +37 -7
  32. package/src/tigerbeetle/src/config.zig +58 -17
  33. package/src/tigerbeetle/src/demo.zig +5 -2
  34. package/src/tigerbeetle/src/demo_01_create_accounts.zig +1 -1
  35. package/src/tigerbeetle/src/demo_03_create_transfers.zig +13 -0
  36. package/src/tigerbeetle/src/ewah.zig +11 -33
  37. package/src/tigerbeetle/src/ewah_benchmark.zig +8 -9
  38. package/src/tigerbeetle/src/io/linux.zig +1 -1
  39. package/src/tigerbeetle/src/lsm/README.md +308 -0
  40. package/src/tigerbeetle/src/lsm/binary_search.zig +137 -10
  41. package/src/tigerbeetle/src/lsm/bloom_filter.zig +43 -0
  42. package/src/tigerbeetle/src/lsm/compaction.zig +376 -397
  43. package/src/tigerbeetle/src/lsm/composite_key.zig +2 -0
  44. package/src/tigerbeetle/src/lsm/eytzinger.zig +1 -1
  45. package/src/tigerbeetle/src/{eytzinger_benchmark.zig → lsm/eytzinger_benchmark.zig} +34 -21
  46. package/src/tigerbeetle/src/lsm/forest.zig +21 -447
  47. package/src/tigerbeetle/src/lsm/forest_fuzz.zig +414 -0
  48. package/src/tigerbeetle/src/lsm/grid.zig +170 -76
  49. package/src/tigerbeetle/src/lsm/groove.zig +197 -133
  50. package/src/tigerbeetle/src/lsm/k_way_merge.zig +40 -18
  51. package/src/tigerbeetle/src/lsm/level_iterator.zig +28 -9
  52. package/src/tigerbeetle/src/lsm/manifest.zig +93 -180
  53. package/src/tigerbeetle/src/lsm/manifest_level.zig +161 -454
  54. package/src/tigerbeetle/src/lsm/manifest_log.zig +243 -356
  55. package/src/tigerbeetle/src/lsm/manifest_log_fuzz.zig +665 -0
  56. package/src/tigerbeetle/src/lsm/node_pool.zig +4 -0
  57. package/src/tigerbeetle/src/lsm/posted_groove.zig +65 -76
  58. package/src/tigerbeetle/src/lsm/segmented_array.zig +580 -251
  59. package/src/tigerbeetle/src/lsm/segmented_array_benchmark.zig +148 -0
  60. package/src/tigerbeetle/src/lsm/segmented_array_fuzz.zig +9 -0
  61. package/src/tigerbeetle/src/lsm/set_associative_cache.zig +62 -12
  62. package/src/tigerbeetle/src/lsm/table.zig +115 -68
  63. package/src/tigerbeetle/src/lsm/table_immutable.zig +30 -23
  64. package/src/tigerbeetle/src/lsm/table_iterator.zig +27 -17
  65. package/src/tigerbeetle/src/lsm/table_mutable.zig +63 -12
  66. package/src/tigerbeetle/src/lsm/test.zig +61 -56
  67. package/src/tigerbeetle/src/lsm/tree.zig +450 -407
  68. package/src/tigerbeetle/src/lsm/tree_fuzz.zig +461 -0
  69. package/src/tigerbeetle/src/main.zig +83 -8
  70. package/src/tigerbeetle/src/message_bus.zig +20 -9
  71. package/src/tigerbeetle/src/message_pool.zig +22 -19
  72. package/src/tigerbeetle/src/ring_buffer.zig +7 -3
  73. package/src/tigerbeetle/src/simulator.zig +179 -119
  74. package/src/tigerbeetle/src/state_machine.zig +381 -246
  75. package/src/tigerbeetle/src/static_allocator.zig +65 -0
  76. package/src/tigerbeetle/src/storage.zig +3 -7
  77. package/src/tigerbeetle/src/test/accounting/auditor.zig +577 -0
  78. package/src/tigerbeetle/src/test/accounting/workload.zig +823 -0
  79. package/src/tigerbeetle/src/test/cluster.zig +33 -81
  80. package/src/tigerbeetle/src/test/conductor.zig +366 -0
  81. package/src/tigerbeetle/src/test/fuzz.zig +121 -0
  82. package/src/tigerbeetle/src/test/id.zig +89 -0
  83. package/src/tigerbeetle/src/test/network.zig +45 -19
  84. package/src/tigerbeetle/src/test/packet_simulator.zig +40 -29
  85. package/src/tigerbeetle/src/test/priority_queue.zig +645 -0
  86. package/src/tigerbeetle/src/test/state_checker.zig +91 -69
  87. package/src/tigerbeetle/src/test/state_machine.zig +11 -35
  88. package/src/tigerbeetle/src/test/storage.zig +470 -106
  89. package/src/tigerbeetle/src/test/storage_checker.zig +204 -0
  90. package/src/tigerbeetle/src/tigerbeetle.zig +15 -16
  91. package/src/tigerbeetle/src/unit_tests.zig +13 -1
  92. package/src/tigerbeetle/src/util.zig +97 -11
  93. package/src/tigerbeetle/src/vopr.zig +495 -0
  94. package/src/tigerbeetle/src/vsr/client.zig +21 -3
  95. package/src/tigerbeetle/src/vsr/journal.zig +293 -212
  96. package/src/tigerbeetle/src/vsr/replica.zig +1086 -515
  97. package/src/tigerbeetle/src/vsr/superblock.zig +382 -637
  98. package/src/tigerbeetle/src/vsr/superblock_client_table.zig +14 -16
  99. package/src/tigerbeetle/src/vsr/superblock_free_set.zig +416 -153
  100. package/src/tigerbeetle/src/vsr/superblock_free_set_fuzz.zig +332 -0
  101. package/src/tigerbeetle/src/vsr/superblock_fuzz.zig +349 -0
  102. package/src/tigerbeetle/src/vsr/superblock_manifest.zig +62 -12
  103. package/src/tigerbeetle/src/vsr/superblock_quorums.zig +394 -0
  104. package/src/tigerbeetle/src/vsr/superblock_quorums_fuzz.zig +312 -0
  105. package/src/tigerbeetle/src/vsr.zig +94 -60
  106. package/src/tigerbeetle/scripts/vopr.bat +0 -48
  107. package/src/tigerbeetle/scripts/vopr.sh +0 -33
  108. package/src/tigerbeetle/src/benchmark_array_search.zig +0 -317
  109. package/src/tigerbeetle/src/benchmarks/perf.zig +0 -299
@@ -3,11 +3,13 @@ const assert = std.debug.assert;
3
3
  const mem = std.mem;
4
4
 
5
5
  const config = @import("../config.zig");
6
+ const vsr = @import("../vsr.zig");
6
7
 
7
8
  const Cluster = @import("cluster.zig").Cluster;
8
9
  const Network = @import("network.zig").Network;
9
10
  const Storage = @import("storage.zig").Storage;
10
- const StateMachine = @import("state_machine.zig").StateMachineType(Storage);
11
+ const Client = @import("cluster.zig").Client;
12
+ const Replica = @import("cluster.zig").Replica;
11
13
 
12
14
  const message_pool = @import("../message_pool.zig");
13
15
  const MessagePool = message_pool.MessagePool;
@@ -15,47 +17,50 @@ const Message = MessagePool.Message;
15
17
 
16
18
  const RingBuffer = @import("../ring_buffer.zig").RingBuffer;
17
19
 
18
- const RequestQueue = RingBuffer(u128, config.client_request_queue_max, .array);
19
20
  const StateTransitions = std.AutoHashMap(u128, u64);
20
21
 
21
22
  const log = std.log.scoped(.state_checker);
22
23
 
23
24
  pub const StateChecker = struct {
24
- /// Indexed by client index as used by Cluster.
25
- client_requests: [config.clients_max]RequestQueue =
26
- [_]RequestQueue{.{}} ** config.clients_max,
27
-
28
25
  /// Indexed by replica index.
29
- state_machine_states: [config.replicas_max]u128,
26
+ replica_states: [config.replicas_max]u128 = [_]u128{0} ** config.replicas_max,
30
27
 
28
+ /// Keyed by committed `message.header.checksum`.
29
+ ///
30
+ /// The first state is always `root_prepare.checksum`, since the root prepare doesn't
31
+ /// commit normally.
31
32
  history: StateTransitions,
32
33
 
33
- /// The highest cannonical state reached by the cluster.
34
- state: u128,
34
+ root: u128,
35
35
 
36
- /// The number of times the cannonical state has been advanced.
37
- transitions: u64 = 0,
36
+ // TODO When StateChecker is owned by the Simulation, use @fieldParentPtr to get these.
37
+ replicas: []const Replica,
38
+ clients: []const Client,
38
39
 
39
- pub fn init(allocator: mem.Allocator, cluster: *Cluster) !StateChecker {
40
- const state = cluster.replicas[0].state_machine.state;
40
+ /// The highest canonical state reached by the cluster.
41
+ state: u128 = 0,
41
42
 
42
- var state_machine_states: [config.replicas_max]u128 = undefined;
43
- for (cluster.replicas) |*replica, i| {
44
- const state_machine = &replica.state_machine;
45
- assert(state_machine.state == state);
46
- state_machine_states[i] = state_machine.state;
47
- }
43
+ /// The number of times the canonical state has been advanced.
44
+ requests_committed: u64 = 0,
48
45
 
46
+ pub fn init(
47
+ allocator: mem.Allocator,
48
+ cluster: u32,
49
+ replicas: []const Replica,
50
+ clients: []const Client,
51
+ ) !StateChecker {
49
52
  var history = StateTransitions.init(allocator);
50
53
  errdefer history.deinit();
51
54
 
55
+ const root_checksum = vsr.Header.root_prepare(cluster).checksum;
56
+
52
57
  var state_checker = StateChecker{
53
- .state_machine_states = state_machine_states,
54
58
  .history = history,
55
- .state = state,
59
+ .root = root_checksum,
60
+ .replicas = replicas,
61
+ .clients = clients,
56
62
  };
57
-
58
- try state_checker.history.putNoClobber(state, state_checker.transitions);
63
+ try state_checker.history.putNoClobber(root_checksum, state_checker.requests_committed);
59
64
 
60
65
  return state_checker;
61
66
  }
@@ -64,14 +69,24 @@ pub const StateChecker = struct {
64
69
  state_checker.history.deinit();
65
70
  }
66
71
 
67
- pub fn check_state(state_checker: *StateChecker, replica: u8) void {
68
- const cluster = @fieldParentPtr(Cluster, "state_checker", state_checker);
72
+ pub fn check_state(state_checker: *StateChecker, replica_index: u8) !void {
73
+ const replica = state_checker.replicas[replica_index];
74
+ const commit_header = header: {
75
+ if (replica.journal.recovered) {
76
+ const commit_header = replica.journal.header_with_op(replica.commit_min);
77
+ assert(commit_header != null or replica.commit_min == replica.op_checkpoint);
78
+ break :header replica.journal.header_with_op(replica.commit_min);
79
+ } else {
80
+ // Still recovering.
81
+ break :header null;
82
+ }
83
+ };
69
84
 
70
- const a = state_checker.state_machine_states[replica];
71
- const b = cluster.replicas[replica].state_machine.state;
85
+ const a = state_checker.replica_states[replica_index];
86
+ const b = if (commit_header) |h| h.checksum else state_checker.root;
72
87
 
73
88
  if (b == a) return;
74
- state_checker.state_machine_states[replica] = b;
89
+ state_checker.replica_states[replica_index] = b;
75
90
 
76
91
  // If some other replica has already reached this state, then it will be in the history:
77
92
  if (state_checker.history.get(b)) |transition| {
@@ -81,65 +96,72 @@ pub const StateChecker = struct {
81
96
  // transitioned may not regress.
82
97
  log.info(
83
98
  "{d:0>4}/{d:0>4} {x:0>32} > {x:0>32} {}",
84
- .{ transition, state_checker.transitions, a, b, replica },
99
+ .{ transition, state_checker.requests_committed, a, b, replica_index },
85
100
  );
86
101
  return;
87
102
  }
88
103
 
104
+ if (commit_header == null) return;
105
+ assert(commit_header.?.parent == a);
106
+ assert(commit_header.?.op > 0);
107
+ assert(commit_header.?.command == .prepare);
108
+ assert(commit_header.?.operation != .reserved);
109
+
89
110
  // The replica has transitioned to state `b` that is not yet in the history.
90
- // Check if this is a valid new state based on all currently inflight client requests.
91
- for (state_checker.client_requests) |*queue| {
92
- if (queue.head_ptr()) |input| {
93
- if (b == StateMachine.hash(state_checker.state, std.mem.asBytes(input))) {
94
- const transitions_executed = state_checker.history.get(a).?;
95
- if (transitions_executed < state_checker.transitions) {
96
- @panic("replica skipped interim transitions");
97
- } else {
98
- assert(transitions_executed == state_checker.transitions);
99
- }
100
-
101
- state_checker.state = b;
102
- state_checker.transitions += 1;
103
-
104
- log.info(" {d:0>4} {x:0>32} > {x:0>32} {}", .{
105
- state_checker.transitions,
106
- a,
107
- b,
108
- replica,
109
- });
110
-
111
- state_checker.history.putNoClobber(b, state_checker.transitions) catch {
112
- @panic("state checker unable to allocate memory for history.put()");
113
- };
114
-
115
- // As soon as we reach a valid state we must pop the inflight request.
116
- // We cannot wait until the client receives the reply because that would allow
117
- // the inflight request to be used to reach other states in the interim.
118
- // We must therefore use our own queue rather than the clients' queues.
119
- _ = queue.pop();
120
- return;
121
- }
122
- }
111
+ // Check if this is a valid new state based on the originating client's inflight request.
112
+ const client = for (state_checker.clients) |*client| {
113
+ if (client.id == commit_header.?.client) break client;
114
+ } else unreachable;
115
+
116
+ if (client.request_queue.empty()) {
117
+ return error.ReplicaTransitionedToInvalidState;
123
118
  }
124
119
 
125
- @panic("replica transitioned to an invalid state");
120
+ const request = client.request_queue.head_ptr_const().?;
121
+ assert(request.message.header.client == commit_header.?.client);
122
+ assert(request.message.header.request == commit_header.?.request);
123
+ assert(request.message.header.command == .request);
124
+ assert(request.message.header.operation == commit_header.?.operation);
125
+ assert(request.message.header.size == commit_header.?.size);
126
+ // `checksum_body` will not match; the leader's StateMachine updated the timestamps in the
127
+ // prepare body's accounts/transfers.
128
+
129
+ const transitions_executed = state_checker.history.get(a).?;
130
+ if (transitions_executed < state_checker.requests_committed) {
131
+ return error.ReplicaSkippedInterimTransitions;
132
+ } else {
133
+ assert(transitions_executed == state_checker.requests_committed);
134
+ }
135
+
136
+ state_checker.state = b;
137
+ state_checker.requests_committed += 1;
138
+ assert(state_checker.requests_committed == commit_header.?.op);
139
+
140
+ log.info(" {d:0>4} {x:0>32} > {x:0>32} {}", .{
141
+ state_checker.requests_committed,
142
+ a,
143
+ b,
144
+ replica_index,
145
+ });
146
+
147
+ state_checker.history.putNoClobber(b, state_checker.requests_committed) catch {
148
+ @panic("state checker unable to allocate memory for history.put()");
149
+ };
126
150
  }
127
151
 
128
152
  pub fn convergence(state_checker: *StateChecker) bool {
129
- const cluster = @fieldParentPtr(Cluster, "state_checker", state_checker);
130
-
131
- const a = state_checker.state_machine_states[0];
132
- for (state_checker.state_machine_states[1..cluster.options.replica_count]) |b| {
153
+ const a = state_checker.replica_states[0];
154
+ for (state_checker.replica_states[1..state_checker.replicas[0].replica_count]) |b| {
133
155
  if (b != a) return false;
134
156
  }
135
157
 
136
158
  const transitions_executed = state_checker.history.get(a).?;
137
- if (transitions_executed < state_checker.transitions) {
159
+ if (transitions_executed < state_checker.requests_committed) {
138
160
  // Cluster reached convergence but on a regressed state.
139
161
  // A replica reached the transition limit, crashed, then repaired.
140
162
  return false;
141
163
  } else {
142
- assert(transitions_executed == state_checker.transitions);
164
+ assert(transitions_executed == state_checker.requests_committed);
143
165
  }
144
166
 
145
167
  return true;
@@ -15,7 +15,7 @@ pub fn StateMachineType(comptime Storage: type) type {
15
15
  root,
16
16
  register,
17
17
 
18
- hash,
18
+ random,
19
19
  };
20
20
 
21
21
  /// Minimum/mean number of ticks to perform the specified operation.
@@ -28,7 +28,6 @@ pub fn StateMachineType(comptime Storage: type) type {
28
28
  };
29
29
 
30
30
  options: Options,
31
- state: u128,
32
31
  prng: std.rand.DefaultPrng,
33
32
  prepare_timestamp: u64 = 0,
34
33
  commit_timestamp: u64 = 0,
@@ -38,7 +37,6 @@ pub fn StateMachineType(comptime Storage: type) type {
38
37
 
39
38
  pub fn init(_: std.mem.Allocator, _: *Grid, options: Options) !StateMachine {
40
39
  return StateMachine{
41
- .state = hash(0, std.mem.asBytes(&options.seed)),
42
40
  .options = options,
43
41
  .prng = std.rand.DefaultPrng.init(options.seed),
44
42
  };
@@ -46,6 +44,10 @@ pub fn StateMachineType(comptime Storage: type) type {
46
44
 
47
45
  pub fn deinit(_: *StateMachine, _: std.mem.Allocator) void {}
48
46
 
47
+ // TODO This is dead code — tick() has been removed from the StateMachine
48
+ // interface. If we start using the test StateMachine again, tick will need
49
+ // to be called explicitly from the simulator to ensure async operations can
50
+ // finish.
49
51
  pub fn tick(state_machine: *StateMachine) void {
50
52
  if (state_machine.callback) |callback| {
51
53
  if (state_machine.callback_ticks == 0) {
@@ -98,32 +100,16 @@ pub fn StateMachineType(comptime Storage: type) type {
98
100
  input: []const u8,
99
101
  output: []u8,
100
102
  ) usize {
101
- _ = op;
103
+ _ = state_machine;
104
+ _ = client;
105
+ _ = input;
106
+ _ = output;
107
+ assert(op != 0);
102
108
 
103
109
  switch (operation) {
104
110
  .reserved, .root => unreachable,
105
111
  .register => return 0,
106
-
107
- // TODO: instead of always using the first 32 bytes of the output
108
- // buffer, get tricky and use a random but deterministic slice
109
- // of it, filling the rest with 0s.
110
- .hash => {
111
- // Fold the input into our current state, creating a hash chain.
112
- // Hash the input with the client ID since small inputs may collide across clients.
113
- const client_input = hash(client, input);
114
- const new_state = hash(state_machine.state, std.mem.asBytes(&client_input));
115
-
116
- log.debug("state={x} input={x} input.len={} new state={x}", .{
117
- state_machine.state,
118
- client_input,
119
- input.len,
120
- new_state,
121
- });
122
-
123
- state_machine.state = new_state;
124
- std.mem.copy(u8, output, std.mem.asBytes(&state_machine.state));
125
- return @sizeOf(@TypeOf(state_machine.state));
126
- },
112
+ .random => return 0,
127
113
  }
128
114
  }
129
115
 
@@ -142,23 +128,13 @@ pub fn StateMachineType(comptime Storage: type) type {
142
128
  pub fn checkpoint(
143
129
  state_machine: *StateMachine,
144
130
  callback: fn (*StateMachine) void,
145
- op: u64,
146
131
  ) void {
147
- _ = op;
148
132
  assert(state_machine.callback == null);
149
133
  assert(state_machine.callback_ticks == 0);
150
134
  state_machine.callback = callback;
151
135
  state_machine.callback_ticks = state_machine.latency(state_machine.options.checkpoint_mean);
152
136
  }
153
137
 
154
- pub fn hash(state: u128, input: []const u8) u128 {
155
- var key: [32]u8 = [_]u8{0} ** 32;
156
- std.mem.copy(u8, key[0..16], std.mem.asBytes(&state));
157
- var target: [32]u8 = undefined;
158
- std.crypto.hash.Blake3.hash(input, &target, .{ .key = key });
159
- return @bitCast(u128, target[0..16].*);
160
- }
161
-
162
138
  fn latency(state_machine: *StateMachine, mean: u64) u64 {
163
139
  return state_machine.prng.random().uintLessThan(u64, 2 * mean);
164
140
  }