tigerbeetle-node 0.11.12 → 0.12.0

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 (143) hide show
  1. package/README.md +212 -196
  2. package/dist/bin/aarch64-linux-gnu/client.node +0 -0
  3. package/dist/bin/aarch64-linux-musl/client.node +0 -0
  4. package/dist/bin/aarch64-macos/client.node +0 -0
  5. package/dist/bin/x86_64-linux-gnu/client.node +0 -0
  6. package/dist/bin/x86_64-linux-musl/client.node +0 -0
  7. package/dist/bin/x86_64-macos/client.node +0 -0
  8. package/dist/index.js +33 -1
  9. package/dist/index.js.map +1 -1
  10. package/package-lock.json +66 -0
  11. package/package.json +8 -17
  12. package/src/index.ts +56 -1
  13. package/src/node.zig +10 -9
  14. package/dist/.client.node.sha256 +0 -1
  15. package/scripts/build_lib.sh +0 -61
  16. package/scripts/download_node_headers.sh +0 -32
  17. package/src/tigerbeetle/scripts/benchmark.bat +0 -48
  18. package/src/tigerbeetle/scripts/benchmark.sh +0 -66
  19. package/src/tigerbeetle/scripts/confirm_image.sh +0 -44
  20. package/src/tigerbeetle/scripts/fuzz_loop.sh +0 -15
  21. package/src/tigerbeetle/scripts/fuzz_unique_errors.sh +0 -7
  22. package/src/tigerbeetle/scripts/install.bat +0 -7
  23. package/src/tigerbeetle/scripts/install.sh +0 -21
  24. package/src/tigerbeetle/scripts/install_zig.bat +0 -113
  25. package/src/tigerbeetle/scripts/install_zig.sh +0 -90
  26. package/src/tigerbeetle/scripts/lint.zig +0 -199
  27. package/src/tigerbeetle/scripts/pre-commit.sh +0 -9
  28. package/src/tigerbeetle/scripts/scripts/benchmark.bat +0 -48
  29. package/src/tigerbeetle/scripts/scripts/benchmark.sh +0 -66
  30. package/src/tigerbeetle/scripts/scripts/confirm_image.sh +0 -44
  31. package/src/tigerbeetle/scripts/scripts/fuzz_loop.sh +0 -15
  32. package/src/tigerbeetle/scripts/scripts/fuzz_unique_errors.sh +0 -7
  33. package/src/tigerbeetle/scripts/scripts/install.bat +0 -7
  34. package/src/tigerbeetle/scripts/scripts/install.sh +0 -21
  35. package/src/tigerbeetle/scripts/scripts/install_zig.bat +0 -113
  36. package/src/tigerbeetle/scripts/scripts/install_zig.sh +0 -90
  37. package/src/tigerbeetle/scripts/scripts/lint.zig +0 -199
  38. package/src/tigerbeetle/scripts/scripts/pre-commit.sh +0 -9
  39. package/src/tigerbeetle/scripts/scripts/shellcheck.sh +0 -5
  40. package/src/tigerbeetle/scripts/scripts/tests_on_alpine.sh +0 -10
  41. package/src/tigerbeetle/scripts/scripts/tests_on_ubuntu.sh +0 -14
  42. package/src/tigerbeetle/scripts/scripts/upgrade_ubuntu_kernel.sh +0 -48
  43. package/src/tigerbeetle/scripts/scripts/validate_docs.sh +0 -23
  44. package/src/tigerbeetle/scripts/scripts/vr_state_enumerate +0 -46
  45. package/src/tigerbeetle/scripts/shellcheck.sh +0 -5
  46. package/src/tigerbeetle/scripts/tests_on_alpine.sh +0 -10
  47. package/src/tigerbeetle/scripts/tests_on_ubuntu.sh +0 -14
  48. package/src/tigerbeetle/scripts/upgrade_ubuntu_kernel.sh +0 -48
  49. package/src/tigerbeetle/scripts/validate_docs.sh +0 -23
  50. package/src/tigerbeetle/scripts/vr_state_enumerate +0 -46
  51. package/src/tigerbeetle/src/benchmark.zig +0 -314
  52. package/src/tigerbeetle/src/config.zig +0 -234
  53. package/src/tigerbeetle/src/constants.zig +0 -436
  54. package/src/tigerbeetle/src/ewah.zig +0 -286
  55. package/src/tigerbeetle/src/ewah_benchmark.zig +0 -120
  56. package/src/tigerbeetle/src/ewah_fuzz.zig +0 -130
  57. package/src/tigerbeetle/src/fifo.zig +0 -120
  58. package/src/tigerbeetle/src/io/benchmark.zig +0 -213
  59. package/src/tigerbeetle/src/io/darwin.zig +0 -814
  60. package/src/tigerbeetle/src/io/linux.zig +0 -1062
  61. package/src/tigerbeetle/src/io/test.zig +0 -643
  62. package/src/tigerbeetle/src/io/windows.zig +0 -1183
  63. package/src/tigerbeetle/src/io.zig +0 -34
  64. package/src/tigerbeetle/src/iops.zig +0 -107
  65. package/src/tigerbeetle/src/lsm/README.md +0 -308
  66. package/src/tigerbeetle/src/lsm/binary_search.zig +0 -341
  67. package/src/tigerbeetle/src/lsm/bloom_filter.zig +0 -125
  68. package/src/tigerbeetle/src/lsm/compaction.zig +0 -603
  69. package/src/tigerbeetle/src/lsm/composite_key.zig +0 -77
  70. package/src/tigerbeetle/src/lsm/direction.zig +0 -11
  71. package/src/tigerbeetle/src/lsm/eytzinger.zig +0 -587
  72. package/src/tigerbeetle/src/lsm/eytzinger_benchmark.zig +0 -330
  73. package/src/tigerbeetle/src/lsm/forest.zig +0 -204
  74. package/src/tigerbeetle/src/lsm/forest_fuzz.zig +0 -401
  75. package/src/tigerbeetle/src/lsm/grid.zig +0 -573
  76. package/src/tigerbeetle/src/lsm/groove.zig +0 -972
  77. package/src/tigerbeetle/src/lsm/k_way_merge.zig +0 -474
  78. package/src/tigerbeetle/src/lsm/level_iterator.zig +0 -332
  79. package/src/tigerbeetle/src/lsm/manifest.zig +0 -617
  80. package/src/tigerbeetle/src/lsm/manifest_level.zig +0 -877
  81. package/src/tigerbeetle/src/lsm/manifest_log.zig +0 -789
  82. package/src/tigerbeetle/src/lsm/manifest_log_fuzz.zig +0 -691
  83. package/src/tigerbeetle/src/lsm/merge_iterator.zig +0 -106
  84. package/src/tigerbeetle/src/lsm/node_pool.zig +0 -235
  85. package/src/tigerbeetle/src/lsm/posted_groove.zig +0 -378
  86. package/src/tigerbeetle/src/lsm/segmented_array.zig +0 -1328
  87. package/src/tigerbeetle/src/lsm/segmented_array_benchmark.zig +0 -148
  88. package/src/tigerbeetle/src/lsm/segmented_array_fuzz.zig +0 -9
  89. package/src/tigerbeetle/src/lsm/set_associative_cache.zig +0 -850
  90. package/src/tigerbeetle/src/lsm/table.zig +0 -1031
  91. package/src/tigerbeetle/src/lsm/table_immutable.zig +0 -203
  92. package/src/tigerbeetle/src/lsm/table_iterator.zig +0 -340
  93. package/src/tigerbeetle/src/lsm/table_mutable.zig +0 -220
  94. package/src/tigerbeetle/src/lsm/test.zig +0 -438
  95. package/src/tigerbeetle/src/lsm/tree.zig +0 -1193
  96. package/src/tigerbeetle/src/lsm/tree_fuzz.zig +0 -474
  97. package/src/tigerbeetle/src/message_bus.zig +0 -1012
  98. package/src/tigerbeetle/src/message_pool.zig +0 -156
  99. package/src/tigerbeetle/src/ring_buffer.zig +0 -399
  100. package/src/tigerbeetle/src/simulator.zig +0 -569
  101. package/src/tigerbeetle/src/state_machine/auditor.zig +0 -577
  102. package/src/tigerbeetle/src/state_machine/workload.zig +0 -883
  103. package/src/tigerbeetle/src/state_machine.zig +0 -1881
  104. package/src/tigerbeetle/src/static_allocator.zig +0 -65
  105. package/src/tigerbeetle/src/stdx.zig +0 -162
  106. package/src/tigerbeetle/src/storage.zig +0 -393
  107. package/src/tigerbeetle/src/testing/cluster/message_bus.zig +0 -82
  108. package/src/tigerbeetle/src/testing/cluster/network.zig +0 -237
  109. package/src/tigerbeetle/src/testing/cluster/state_checker.zig +0 -169
  110. package/src/tigerbeetle/src/testing/cluster/storage_checker.zig +0 -202
  111. package/src/tigerbeetle/src/testing/cluster.zig +0 -443
  112. package/src/tigerbeetle/src/testing/fuzz.zig +0 -140
  113. package/src/tigerbeetle/src/testing/hash_log.zig +0 -66
  114. package/src/tigerbeetle/src/testing/id.zig +0 -99
  115. package/src/tigerbeetle/src/testing/packet_simulator.zig +0 -364
  116. package/src/tigerbeetle/src/testing/priority_queue.zig +0 -645
  117. package/src/tigerbeetle/src/testing/reply_sequence.zig +0 -139
  118. package/src/tigerbeetle/src/testing/state_machine.zig +0 -249
  119. package/src/tigerbeetle/src/testing/storage.zig +0 -757
  120. package/src/tigerbeetle/src/testing/table.zig +0 -247
  121. package/src/tigerbeetle/src/testing/time.zig +0 -84
  122. package/src/tigerbeetle/src/tigerbeetle.zig +0 -227
  123. package/src/tigerbeetle/src/time.zig +0 -112
  124. package/src/tigerbeetle/src/tracer.zig +0 -529
  125. package/src/tigerbeetle/src/unit_tests.zig +0 -42
  126. package/src/tigerbeetle/src/vopr.zig +0 -495
  127. package/src/tigerbeetle/src/vsr/README.md +0 -209
  128. package/src/tigerbeetle/src/vsr/client.zig +0 -544
  129. package/src/tigerbeetle/src/vsr/clock.zig +0 -853
  130. package/src/tigerbeetle/src/vsr/journal.zig +0 -2413
  131. package/src/tigerbeetle/src/vsr/journal_format_fuzz.zig +0 -111
  132. package/src/tigerbeetle/src/vsr/marzullo.zig +0 -309
  133. package/src/tigerbeetle/src/vsr/replica.zig +0 -6381
  134. package/src/tigerbeetle/src/vsr/replica_format.zig +0 -219
  135. package/src/tigerbeetle/src/vsr/superblock.zig +0 -1631
  136. package/src/tigerbeetle/src/vsr/superblock_client_table.zig +0 -256
  137. package/src/tigerbeetle/src/vsr/superblock_free_set.zig +0 -929
  138. package/src/tigerbeetle/src/vsr/superblock_free_set_fuzz.zig +0 -334
  139. package/src/tigerbeetle/src/vsr/superblock_fuzz.zig +0 -390
  140. package/src/tigerbeetle/src/vsr/superblock_manifest.zig +0 -615
  141. package/src/tigerbeetle/src/vsr/superblock_quorums.zig +0 -394
  142. package/src/tigerbeetle/src/vsr/superblock_quorums_fuzz.zig +0 -314
  143. package/src/tigerbeetle/src/vsr.zig +0 -1352
@@ -1,883 +0,0 @@
1
- //! The Workload drives an end-to-end test: from client requests, through consensus and the state
2
- //! machine, down to the storage engine, and back.
3
- //!
4
- //! The Workload constructs messages to create and query accounts and transfers, and validates the
5
- //! replies.
6
- //!
7
- //! Goals:
8
- //!
9
- //! * Run in a fixed amount of memory. (For long-running tests or performance testing).
10
- //! * Query and verify transfers arbitrarily far back. (To exercise the storage engine).
11
- //!
12
- //! Transfer Encoding:
13
- //!
14
- //! * `Transfer.id` is a deterministic, reversible permutation of an ascending index.
15
- //! * With the transfer's index as a seed, the Workload knows the eventual outcome of the transfer.
16
- //! * `Transfer.user_data` is a checksum of the remainder of the transfer's data
17
- //! (excluding `timestamp` and `user_data` itself). This helps `on_lookup_transfers` to
18
- //! validate its results.
19
- //!
20
- const std = @import("std");
21
- const assert = std.debug.assert;
22
- const log = std.log.scoped(.test_workload);
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");
28
- const Auditor = accounting_auditor.AccountingAuditor;
29
- const IdPermutation = @import("../testing/id.zig").IdPermutation;
30
- const fuzz = @import("../testing/fuzz.zig");
31
-
32
- // TODO(zig) This won't be necessary in Zig 0.10.
33
- const PriorityQueue = @import("../testing/priority_queue.zig").PriorityQueue;
34
-
35
- const TransferOutcome = enum {
36
- /// The transfer is guaranteed to commit.
37
- /// For example, a single-phase transfer between valid accounts without balance limits.
38
- success,
39
- /// The transfer is invalid. For example, the `ledger` field is missing.
40
- failure,
41
- /// Due to races with timeouts or other transfers, the outcome of the transfer is uncertain.
42
- /// For example, post/void-pending transfers race with their timeout.
43
- unknown,
44
- };
45
-
46
- /// A Transfer generated from the plan is guaranteed to have a matching `outcome`, but it may use a
47
- /// different Method. (For example, `method=pending` may fall back to `method=single_phase` if the
48
- /// Auditor's pending transfer queue is full).
49
- const TransferPlan = struct {
50
- /// When false, send invalid payments that are guaranteed to be rejected with an error.
51
- valid: bool,
52
-
53
- /// When `limit` is set, at least one of the following is true:
54
- ///
55
- /// * the debit account has debits_must_not_exceed_credits
56
- /// * the credit account has credits_must_not_exceed_debits
57
- ///
58
- limit: bool,
59
-
60
- method: Method,
61
-
62
- const Method = enum {
63
- single_phase,
64
- pending,
65
- post_pending,
66
- void_pending,
67
- };
68
-
69
- fn outcome(self: TransferPlan) TransferOutcome {
70
- if (!self.valid) return .failure;
71
- if (self.limit) return .unknown;
72
- return switch (self.method) {
73
- .single_phase, .pending => .success,
74
- .post_pending, .void_pending => .unknown,
75
- };
76
- }
77
- };
78
-
79
- const TransferTemplate = struct {
80
- ledger: u32,
81
- result: accounting_auditor.CreateTransferResultSet,
82
- };
83
-
84
- const TransferBatchQueue = PriorityQueue(TransferBatch, void, struct {
85
- /// Ascending order.
86
- fn compare(_: void, a: TransferBatch, b: TransferBatch) std.math.Order {
87
- assert(a.min != b.min);
88
- assert(a.max != b.max);
89
- return std.math.order(a.min, b.min);
90
- }
91
- }.compare);
92
-
93
- const TransferBatch = struct {
94
- /// Index of the first transfer in the batch.
95
- min: usize,
96
- /// Index of the last transfer in the batch.
97
- max: usize,
98
- };
99
-
100
- /// Indexes: [valid:bool][limit:bool][method]
101
- const transfer_templates = table: {
102
- const SNGL = @enumToInt(TransferPlan.Method.single_phase);
103
- const PEND = @enumToInt(TransferPlan.Method.pending);
104
- const POST = @enumToInt(TransferPlan.Method.post_pending);
105
- const VOID = @enumToInt(TransferPlan.Method.void_pending);
106
- const Result = accounting_auditor.CreateTransferResultSet;
107
- const result = Result.init;
108
-
109
- const two_phase_ok = .{
110
- .ok = true,
111
- .pending_transfer_already_posted = true,
112
- .pending_transfer_already_voided = true,
113
- .pending_transfer_expired = true,
114
- };
115
-
116
- const limits = result(.{
117
- .exceeds_credits = true,
118
- .exceeds_debits = true,
119
- });
120
-
121
- const either = struct {
122
- fn either(a: Result, b: Result) Result {
123
- var c = a;
124
- c.setUnion(b);
125
- return c;
126
- }
127
- }.either;
128
-
129
- const template = struct {
130
- fn template(ledger: u32, transfer_result: Result) TransferTemplate {
131
- return .{
132
- .ledger = ledger,
133
- .result = transfer_result,
134
- };
135
- }
136
- }.template;
137
-
138
- // [valid:bool][limit:bool][method]
139
- var templates: [2][2][std.meta.fields(TransferPlan.Method).len]TransferTemplate = undefined;
140
-
141
- // template(ledger, result)
142
- templates[0][0][SNGL] = template(0, result(.{ .ledger_must_not_be_zero = true }));
143
- templates[0][0][PEND] = template(0, result(.{ .ledger_must_not_be_zero = true }));
144
- templates[0][0][POST] = template(9, result(.{ .pending_transfer_has_different_ledger = true }));
145
- templates[0][0][VOID] = template(9, result(.{ .pending_transfer_has_different_ledger = true }));
146
-
147
- templates[0][1][SNGL] = template(0, result(.{ .ledger_must_not_be_zero = true }));
148
- templates[0][1][PEND] = template(0, result(.{ .ledger_must_not_be_zero = true }));
149
- templates[0][1][POST] = template(9, result(.{ .pending_transfer_has_different_ledger = true }));
150
- templates[0][1][VOID] = template(9, result(.{ .pending_transfer_has_different_ledger = true }));
151
-
152
- templates[1][0][SNGL] = template(1, result(.{ .ok = true }));
153
- templates[1][0][PEND] = template(1, result(.{ .ok = true }));
154
- templates[1][0][POST] = template(1, result(two_phase_ok));
155
- templates[1][0][VOID] = template(1, result(two_phase_ok));
156
-
157
- templates[1][1][SNGL] = template(1, either(limits, result(.{ .ok = true })));
158
- templates[1][1][PEND] = template(1, either(limits, result(.{ .ok = true })));
159
- templates[1][1][POST] = template(1, either(limits, result(two_phase_ok)));
160
- templates[1][1][VOID] = template(1, either(limits, result(two_phase_ok)));
161
-
162
- break :table templates;
163
- };
164
-
165
- pub fn WorkloadType(comptime AccountingStateMachine: type) type {
166
- const Operation = AccountingStateMachine.Operation;
167
-
168
- const Action = enum(u8) {
169
- create_accounts = @enumToInt(Operation.create_accounts),
170
- create_transfers = @enumToInt(Operation.create_transfers),
171
- lookup_accounts = @enumToInt(Operation.lookup_accounts),
172
- lookup_transfers = @enumToInt(Operation.lookup_transfers),
173
- };
174
-
175
- return struct {
176
- const Self = @This();
177
-
178
- pub const Options = OptionsType(AccountingStateMachine, Action);
179
-
180
- random: std.rand.Random,
181
- auditor: Auditor,
182
- options: Options,
183
-
184
- transfer_plan_seed: u64,
185
-
186
- /// Whether a `create_accounts` message has ever been sent.
187
- accounts_sent: bool = false,
188
-
189
- /// The index of the next transfer to send.
190
- transfers_sent: usize = 0,
191
-
192
- /// All transfers below this index have been delivered.
193
- /// Any transfers above this index that have been delivered are stored in
194
- /// `transfers_delivered_recently`.
195
- transfers_delivered_past: usize = 0,
196
-
197
- /// Track index ranges of `create_transfers` batches that have committed but are greater
198
- /// than or equal to `transfers_delivered_past` (which is still in-flight).
199
- transfers_delivered_recently: TransferBatchQueue,
200
-
201
- /// Track the number of pending transfers that have been sent but not committed.
202
- transfers_pending_in_flight: usize = 0,
203
-
204
- pub fn init(allocator: std.mem.Allocator, random: std.rand.Random, options: Options) !Self {
205
- assert(options.create_account_invalid_probability <= 100);
206
- assert(options.create_transfer_invalid_probability <= 100);
207
- assert(options.create_transfer_limit_probability <= 100);
208
- assert(options.create_transfer_pending_probability <= 100);
209
- assert(options.create_transfer_post_probability <= 100);
210
- assert(options.create_transfer_void_probability <= 100);
211
- assert(options.lookup_account_invalid_probability <= 100);
212
-
213
- assert(options.account_limit_probability <= 100);
214
- assert(options.linked_valid_probability <= 100);
215
- assert(options.linked_invalid_probability <= 100);
216
-
217
- assert(options.accounts_batch_size_span + options.accounts_batch_size_min <=
218
- AccountingStateMachine.constants.batch_max.create_accounts);
219
- assert(options.accounts_batch_size_span >= 1);
220
- assert(options.transfers_batch_size_span + options.transfers_batch_size_min <=
221
- AccountingStateMachine.constants.batch_max.create_transfers);
222
- assert(options.transfers_batch_size_span >= 1);
223
-
224
- var auditor = try Auditor.init(allocator, random, options.auditor_options);
225
- errdefer auditor.deinit(allocator);
226
-
227
- var transfers_delivered_recently = TransferBatchQueue.init(allocator, {});
228
- errdefer transfers_delivered_recently.deinit();
229
- try transfers_delivered_recently.ensureTotalCapacity(
230
- options.auditor_options.client_count * constants.client_request_queue_max,
231
- );
232
-
233
- for (auditor.accounts) |*account, i| {
234
- account.* = std.mem.zeroInit(tb.Account, .{
235
- .id = auditor.account_index_to_id(i),
236
- .ledger = 1,
237
- .code = 123,
238
- });
239
-
240
- if (chance(random, options.account_limit_probability)) {
241
- const b = random.boolean();
242
- account.flags.debits_must_not_exceed_credits = b;
243
- account.flags.credits_must_not_exceed_debits = !b;
244
- }
245
- }
246
-
247
- return Self{
248
- .random = random,
249
- .auditor = auditor,
250
- .options = options,
251
- .transfer_plan_seed = random.int(u64),
252
- .transfers_delivered_recently = transfers_delivered_recently,
253
- };
254
- }
255
-
256
- pub fn deinit(self: *Self, allocator: std.mem.Allocator) void {
257
- self.auditor.deinit(allocator);
258
- self.transfers_delivered_recently.deinit();
259
- }
260
-
261
- pub fn done(self: *const Self) bool {
262
- if (self.transfers_delivered_recently.len != 0) return false;
263
- return self.auditor.done();
264
- }
265
-
266
- /// A client may build multiple requests to queue up while another is in-flight.
267
- pub fn build_request(
268
- self: *Self,
269
- client_index: usize,
270
- body: []align(@alignOf(vsr.Header)) u8,
271
- ) struct {
272
- operation: Operation,
273
- size: usize,
274
- } {
275
- assert(client_index < self.auditor.options.client_count);
276
- assert(body.len == constants.message_size_max - @sizeOf(vsr.Header));
277
-
278
- const action = action: {
279
- if (!self.accounts_sent and self.random.boolean()) {
280
- // Early in the test make sure some accounts get created.
281
- self.accounts_sent = true;
282
- break :action .create_accounts;
283
- }
284
-
285
- break :action switch (sample_distribution(self.random, self.options.operations)) {
286
- .create_accounts => Action.create_accounts,
287
- .create_transfers => Action.create_transfers,
288
- .lookup_accounts => Action.lookup_accounts,
289
- .lookup_transfers => Action.lookup_transfers,
290
- };
291
- };
292
-
293
- const size = switch (action) {
294
- .create_accounts => @sizeOf(tb.Account) *
295
- self.build_create_accounts(client_index, self.batch(tb.Account, action, body)),
296
- .create_transfers => @sizeOf(tb.Transfer) *
297
- self.build_create_transfers(client_index, self.batch(tb.Transfer, action, body)),
298
- .lookup_accounts => @sizeOf(u128) *
299
- self.build_lookup_accounts(self.batch(u128, action, body)),
300
- .lookup_transfers => @sizeOf(u128) *
301
- self.build_lookup_transfers(self.batch(u128, action, body)),
302
- };
303
- assert(size <= body.len);
304
-
305
- return .{
306
- .operation = @intToEnum(Operation, @enumToInt(action)),
307
- .size = size,
308
- };
309
- }
310
-
311
- /// `on_reply` is called for replies in commit order.
312
- pub fn on_reply(
313
- self: *Self,
314
- client_index: usize,
315
- operation: vsr.Operation,
316
- timestamp: u64,
317
- request_body: []align(@alignOf(vsr.Header)) const u8,
318
- reply_body: []align(@alignOf(vsr.Header)) const u8,
319
- ) void {
320
- assert(timestamp != 0);
321
- assert(request_body.len <= constants.message_size_max - @sizeOf(vsr.Header));
322
- assert(reply_body.len <= constants.message_size_max - @sizeOf(vsr.Header));
323
-
324
- switch (operation.cast(AccountingStateMachine)) {
325
- .create_accounts => self.auditor.on_create_accounts(
326
- client_index,
327
- timestamp,
328
- std.mem.bytesAsSlice(tb.Account, request_body),
329
- std.mem.bytesAsSlice(tb.CreateAccountsResult, reply_body),
330
- ),
331
- .create_transfers => self.on_create_transfers(
332
- client_index,
333
- timestamp,
334
- std.mem.bytesAsSlice(tb.Transfer, request_body),
335
- std.mem.bytesAsSlice(tb.CreateTransfersResult, reply_body),
336
- ),
337
- .lookup_accounts => self.auditor.on_lookup_accounts(
338
- client_index,
339
- timestamp,
340
- std.mem.bytesAsSlice(u128, request_body),
341
- std.mem.bytesAsSlice(tb.Account, reply_body),
342
- ),
343
- .lookup_transfers => self.on_lookup_transfers(
344
- client_index,
345
- timestamp,
346
- std.mem.bytesAsSlice(u128, request_body),
347
- std.mem.bytesAsSlice(tb.Transfer, reply_body),
348
- ),
349
- else => unreachable,
350
- }
351
- }
352
-
353
- fn build_create_accounts(self: *Self, client_index: usize, accounts: []tb.Account) usize {
354
- const results = self.auditor.expect_create_accounts(client_index);
355
- for (accounts) |*account, i| {
356
- const account_index = self.random.uintLessThanBiased(usize, self.auditor.accounts.len);
357
- account.* = self.auditor.accounts[account_index];
358
- account.debits_pending = 0;
359
- account.debits_posted = 0;
360
- account.credits_pending = 0;
361
- account.credits_posted = 0;
362
- account.timestamp = 0;
363
- results[i] = accounting_auditor.CreateAccountResultSet{};
364
-
365
- if (chance(self.random, self.options.create_account_invalid_probability)) {
366
- account.ledger = 0;
367
- results[i].insert(.ledger_must_not_be_zero);
368
- } else {
369
- if (!self.auditor.accounts_created[account_index]) {
370
- results[i].insert(.ok);
371
- }
372
- // Even if the account doesn't exist yet, we may race another request.
373
- results[i].insert(.exists);
374
- }
375
- assert(results[i].count() > 0);
376
- }
377
- return accounts.len;
378
- }
379
-
380
- fn build_create_transfers(self: *Self, client_index: usize, transfers: []tb.Transfer) usize {
381
- const results = self.auditor.expect_create_transfers(client_index);
382
- var transfers_count: usize = transfers.len;
383
- var i: usize = 0;
384
- while (i < transfers_count) {
385
- const transfer_index = self.transfers_sent;
386
- const transfer_plan = self.transfer_index_to_plan(transfer_index);
387
- const transfer_id = self.transfer_index_to_id(transfer_index);
388
- results[i] = self.build_transfer(transfer_id, transfer_plan, &transfers[i]) orelse {
389
- // This transfer index can't be built; stop with what we have so far.
390
- // Hopefully it will be unblocked before the next `create_transfers`.
391
- transfers_count = i;
392
- break;
393
- };
394
-
395
- if (i != 0 and results[i].count() == 1 and results[i - 1].count() == 1) {
396
- // To support random `lookup_transfers`, linked transfers can't be planned.
397
- // Instead, link transfers opportunistically, when consecutive transfers can be
398
- // linked without altering any of their outcomes.
399
-
400
- if (results[i].contains(.ok) and results[i - 1].contains(.ok) and
401
- chance(self.random, self.options.linked_valid_probability))
402
- {
403
- transfers[i - 1].flags.linked = true;
404
- }
405
-
406
- if (!results[i].contains(.ok) and !results[i - 1].contains(.ok) and
407
- chance(self.random, self.options.linked_invalid_probability))
408
- {
409
- // Convert the previous transfer to a single-phase no-limit transfer, but
410
- // link it to the current transfer — it will still fail.
411
- _ = self.build_transfer(transfers[i - 1].id, .{
412
- .valid = true,
413
- .limit = false,
414
- .method = .single_phase,
415
- }, &transfers[i - 1]);
416
- transfers[i - 1].flags.linked = true;
417
- results[i - 1] = accounting_auditor.CreateTransferResultSet.init(.{
418
- .linked_event_failed = true,
419
- });
420
- }
421
- }
422
- assert(results[i].count() > 0);
423
-
424
- if (transfers[i].flags.pending) self.transfers_pending_in_flight += 1;
425
- i += 1;
426
- self.transfers_sent += 1;
427
- }
428
-
429
- // Checksum transfers only after the whole batch is ready.
430
- // The opportunistic linking backtracks to modify transfers.
431
- for (transfers[0..transfers_count]) |*transfer| {
432
- transfer.user_data = vsr.checksum(std.mem.asBytes(transfer));
433
- }
434
- assert(transfers_count == i);
435
- assert(transfers_count <= transfers.len);
436
- return transfers_count;
437
- }
438
-
439
- fn build_lookup_accounts(self: *Self, lookup_ids: []u128) usize {
440
- for (lookup_ids) |*id| {
441
- if (chance(self.random, self.options.lookup_account_invalid_probability)) {
442
- // Pick an account with valid index (rather than "random.int(u128)") because the
443
- // Auditor must decode the id to check for a matching account.
444
- id.* = self.auditor.account_index_to_id(self.random.int(usize));
445
- } else {
446
- id.* = self.auditor.accounts[self.random.uintLessThanBiased(usize, self.auditor.accounts.len)].id;
447
- }
448
- }
449
- return lookup_ids.len;
450
- }
451
-
452
- fn build_lookup_transfers(self: *const Self, lookup_ids: []u128) usize {
453
- const delivered = self.transfers_delivered_past;
454
- const lookup_window = sample_distribution(self.random, self.options.lookup_transfer);
455
- const lookup_window_start = switch (lookup_window) {
456
- // +1 to avoid an error when delivered=0.
457
- .delivered => self.random.uintLessThanBiased(usize, delivered + 1),
458
- // +1 to avoid an error when delivered=transfers_sent.
459
- .sending => self.random.intRangeLessThanBiased(
460
- usize,
461
- delivered,
462
- self.transfers_sent + 1,
463
- ),
464
- };
465
-
466
- // +1 to make the span-max inclusive.
467
- const lookup_window_size = std.math.min(
468
- fuzz.random_int_exponential(
469
- self.random,
470
- usize,
471
- self.options.lookup_transfer_span_mean,
472
- ),
473
- self.transfers_sent - lookup_window_start,
474
- );
475
- if (lookup_window_size == 0) return 0;
476
-
477
- for (lookup_ids) |*lookup_id| {
478
- lookup_id.* = self.transfer_index_to_id(
479
- lookup_window_start + self.random.uintLessThanBiased(usize, lookup_window_size),
480
- );
481
- }
482
- return lookup_ids.len;
483
- }
484
-
485
- /// The transfer built is guaranteed to match the TransferPlan's outcome.
486
- /// The transfer built is _not_ guaranteed to match the TransferPlan's method.
487
- ///
488
- /// Returns `null` if the transfer plan cannot be fulfilled (because there aren't enough
489
- /// accounts created).
490
- fn build_transfer(
491
- self: *const Self,
492
- transfer_id: u128,
493
- transfer_plan: TransferPlan,
494
- transfer: *tb.Transfer,
495
- ) ?accounting_auditor.CreateTransferResultSet {
496
- // If the specified method is unavailable, swap it.
497
- // Changing the method may narrow the TransferOutcome (unknown→success, unknown→failure)
498
- // but never broaden it (success→unknown, success→failure).
499
- const method = method: {
500
- const default = transfer_plan.method;
501
- if (default == .pending and
502
- self.auditor.pending_expiries.count() + self.transfers_pending_in_flight ==
503
- self.auditor.options.transfers_pending_max)
504
- {
505
- break :method .single_phase;
506
- }
507
-
508
- if (default == .post_pending or default == .void_pending) {
509
- if (self.auditor.pending_transfers.count() == 0) {
510
- break :method .single_phase;
511
- }
512
- }
513
- break :method default;
514
- };
515
-
516
- const index_valid = @boolToInt(transfer_plan.valid);
517
- const index_limit = @boolToInt(transfer_plan.limit);
518
- const index_method = @enumToInt(method);
519
- const transfer_template = &transfer_templates[index_valid][index_limit][index_method];
520
-
521
- const limit_debits = transfer_plan.limit and self.random.boolean();
522
- const limit_credits = transfer_plan.limit and (self.random.boolean() or !limit_debits);
523
-
524
- const debit_account = self.auditor.pick_account(.{
525
- .created = true,
526
- .debits_must_not_exceed_credits = limit_debits,
527
- .credits_must_not_exceed_debits = null,
528
- }) orelse return null;
529
-
530
- const credit_account = self.auditor.pick_account(.{
531
- .created = true,
532
- .debits_must_not_exceed_credits = null,
533
- .credits_must_not_exceed_debits = limit_credits,
534
- .exclude = debit_account.id,
535
- }) orelse return null;
536
-
537
- transfer.* = .{
538
- .id = transfer_id,
539
- .debit_account_id = debit_account.id,
540
- .credit_account_id = credit_account.id,
541
- // "user_data" will be set to a checksum of the Transfer.
542
- .user_data = 0,
543
- .reserved = 0,
544
- .pending_id = 0,
545
- .timeout = 0,
546
- .ledger = transfer_template.ledger,
547
- .code = 123,
548
- .flags = .{},
549
- // +1 to avoid `.amount_must_not_be_zero`.
550
- .amount = 1 + @as(u64, self.random.int(u8)),
551
- };
552
-
553
- switch (method) {
554
- .single_phase => {},
555
- .pending => {
556
- transfer.flags = .{ .pending = true };
557
- // Bound the timeout to ensure we never hit `overflows_timeout`.
558
- transfer.timeout = 1 + std.math.min(
559
- std.math.maxInt(u64) / 2,
560
- fuzz.random_int_exponential(
561
- self.random,
562
- u64,
563
- self.options.pending_timeout_mean,
564
- ),
565
- );
566
- },
567
- .post_pending, .void_pending => {
568
- // Don't depend on `HashMap.keyIterator()` being deterministic.
569
- // Pick a random "target" key, then post/void the id it is nearest to.
570
- const target = self.random.int(u128);
571
- var previous: ?u128 = null;
572
- var iterator = self.auditor.pending_transfers.keyIterator();
573
- while (iterator.next()) |id| {
574
- if (previous == null or
575
- std.math.max(target, id.*) - std.math.min(target, id.*) <
576
- std.math.max(target, previous.?) - std.math.min(target, previous.?))
577
- {
578
- previous = id.*;
579
- }
580
- }
581
-
582
- // If there were no pending ids, the method would have been changed.
583
- const pending_id = previous.?;
584
- const pending_transfer = self.auditor.pending_transfers.getPtr(previous.?).?;
585
- const dr = pending_transfer.debit_account_index;
586
- const cr = pending_transfer.credit_account_index;
587
- // Don't use the default '0' parameters because the StateMachine overwrites 0s
588
- // with the pending transfer's values, invalidating the post/void transfer checksum.
589
- transfer.debit_account_id = self.auditor.account_index_to_id(dr);
590
- transfer.credit_account_id = self.auditor.account_index_to_id(cr);
591
- if (method == .post_pending) {
592
- // 1+rng for minimum amount of 1. This also makes the pending amount inclusive.
593
- transfer.amount = 1 + self.random.uintLessThanBiased(u64, pending_transfer.amount);
594
- } else {
595
- transfer.amount = pending_transfer.amount;
596
- }
597
- transfer.pending_id = pending_id;
598
- transfer.flags = .{
599
- .post_pending_transfer = method == .post_pending,
600
- .void_pending_transfer = method == .void_pending,
601
- };
602
- },
603
- }
604
- assert(transfer_template.result.count() > 0);
605
- return transfer_template.result;
606
- }
607
-
608
- fn batch(self: *const Self, comptime T: type, action: Action, body: []align(@alignOf(vsr.Header)) u8) []T {
609
- const batch_min = switch (action) {
610
- .create_accounts, .lookup_accounts => self.options.accounts_batch_size_min,
611
- .create_transfers, .lookup_transfers => self.options.transfers_batch_size_min,
612
- };
613
- const batch_span = switch (action) {
614
- .create_accounts, .lookup_accounts => self.options.accounts_batch_size_span,
615
- .create_transfers, .lookup_transfers => self.options.transfers_batch_size_span,
616
- };
617
-
618
- // +1 because the span is inclusive.
619
- const batch_size = batch_min + self.random.uintLessThanBiased(usize, batch_span + 1);
620
- return std.mem.bytesAsSlice(T, body)[0..batch_size];
621
- }
622
-
623
- fn transfer_id_to_index(self: *const Self, id: u128) usize {
624
- // -1 because id=0 is not valid, so index=0→id=1.
625
- return @intCast(usize, self.options.transfer_id_permutation.decode(id)) - 1;
626
- }
627
-
628
- fn transfer_index_to_id(self: *const Self, index: usize) u128 {
629
- // +1 so that index=0 is encoded as a valid id.
630
- return self.options.transfer_id_permutation.encode(index + 1);
631
- }
632
-
633
- /// To support `lookup_transfers`, the `TransferPlan` is deterministic based on:
634
- /// * `Workload.transfer_plan_seed`, and
635
- /// * the transfer `index`.
636
- fn transfer_index_to_plan(self: *const Self, index: usize) TransferPlan {
637
- var prng = std.rand.DefaultPrng.init(self.transfer_plan_seed ^ @as(u64, index));
638
- const random = prng.random();
639
- const method: TransferPlan.Method = blk: {
640
- if (chance(random, self.options.create_transfer_pending_probability)) {
641
- break :blk .pending;
642
- }
643
- if (chance(random, self.options.create_transfer_post_probability)) {
644
- break :blk .post_pending;
645
- }
646
- if (chance(random, self.options.create_transfer_void_probability)) {
647
- break :blk .void_pending;
648
- }
649
- break :blk .single_phase;
650
- };
651
- return .{
652
- .valid = !chance(random, self.options.create_transfer_invalid_probability),
653
- .limit = chance(random, self.options.create_transfer_limit_probability),
654
- .method = method,
655
- };
656
- }
657
-
658
- fn on_create_transfers(
659
- self: *Self,
660
- client_index: usize,
661
- timestamp: u64,
662
- transfers: []const tb.Transfer,
663
- results_sparse: []const tb.CreateTransfersResult,
664
- ) void {
665
- self.auditor.on_create_transfers(client_index, timestamp, transfers, results_sparse);
666
- if (transfers.len == 0) return;
667
-
668
- const transfer_index_min = self.transfer_id_to_index(transfers[0].id);
669
- const transfer_index_max = self.transfer_id_to_index(transfers[transfers.len - 1].id);
670
- assert(transfer_index_min <= transfer_index_max);
671
-
672
- self.transfers_delivered_recently.add(.{
673
- .min = transfer_index_min,
674
- .max = transfer_index_max,
675
- }) catch unreachable;
676
-
677
- while (self.transfers_delivered_recently.peek()) |delivered| {
678
- if (self.transfers_delivered_past == delivered.min) {
679
- self.transfers_delivered_past = delivered.max + 1;
680
- _ = self.transfers_delivered_recently.remove();
681
- } else {
682
- assert(self.transfers_delivered_past < delivered.min);
683
- break;
684
- }
685
- }
686
-
687
- for (transfers) |*transfer| {
688
- if (transfer.flags.pending) self.transfers_pending_in_flight -= 1;
689
- }
690
- }
691
-
692
- fn on_lookup_transfers(
693
- self: *Self,
694
- client_index: usize,
695
- timestamp: u64,
696
- ids: []const u128,
697
- results: []const tb.Transfer,
698
- ) void {
699
- self.auditor.on_lookup_transfers(client_index, timestamp, ids, results);
700
-
701
- var transfers = accounting_auditor.IteratorForLookup(tb.Transfer).init(results);
702
- for (ids) |transfer_id| {
703
- const transfer_index = self.transfer_id_to_index(transfer_id);
704
- const transfer_outcome = self.transfer_index_to_plan(transfer_index).outcome();
705
- const result = transfers.take(transfer_id);
706
-
707
- if (result) |transfer| {
708
- // The transfer exists; verify its integrity.
709
- const checksum_actual = transfer.user_data;
710
- var check = transfer.*;
711
- check.user_data = 0;
712
- check.timestamp = 0;
713
- const checksum_expect = vsr.checksum(std.mem.asBytes(&check));
714
- assert(checksum_expect == checksum_actual);
715
- }
716
-
717
- if (transfer_index >= self.transfers_sent) {
718
- // This transfer hasn't been created yet.
719
- assert(result == null);
720
- continue;
721
- }
722
-
723
- switch (transfer_outcome) {
724
- .success => {
725
- if (transfer_index < self.transfers_delivered_past) {
726
- // The transfer was delivered; it must exist.
727
- assert(result != null);
728
- } else {
729
- for (self.transfers_delivered_recently.items) |delivered| {
730
- if (transfer_index >= delivered.min and
731
- transfer_index <= delivered.max)
732
- {
733
- // The transfer was delivered recently; it must exist.
734
- assert(result != null);
735
- break;
736
- }
737
- } else {
738
- // The `create_transfers` has not committed (it may be in-flight).
739
- assert(result == null);
740
- }
741
- }
742
- },
743
- // An invalid transfer is never persisted.
744
- .failure => assert(result == null),
745
- // Due to races and timeouts, these transfer types are not guaranteed to succeed.
746
- .unknown => {},
747
- }
748
- }
749
- }
750
- };
751
- }
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
-
848
- /// Sample from a discrete distribution.
849
- /// Use integers instead of floating-point numbers to avoid nondeterminism on different hardware.
850
- fn sample_distribution(
851
- random: std.rand.Random,
852
- distribution: anytype,
853
- ) std.meta.FieldEnum(@TypeOf(distribution)) {
854
- const SampleSpace = std.meta.FieldEnum(@TypeOf(distribution));
855
- const Indexer = std.enums.EnumIndexer(SampleSpace);
856
-
857
- var sum = sum: {
858
- var sum: usize = 0;
859
- comptime var i: usize = 0;
860
- inline while (i < Indexer.count) : (i += 1) {
861
- const key = comptime @tagName(Indexer.keyForIndex(i));
862
- sum += @field(distribution, key);
863
- }
864
- break :sum sum;
865
- };
866
-
867
- var pick = random.uintLessThanBiased(usize, sum);
868
- comptime var i: usize = 0;
869
- inline while (i < Indexer.count) : (i += 1) {
870
- const event = comptime Indexer.keyForIndex(i);
871
- const weight = @field(distribution, @tagName(event));
872
- if (pick < weight) return event;
873
- pick -= weight;
874
- }
875
-
876
- @panic("sample_discrete: empty sample space");
877
- }
878
-
879
- /// Returns true, `p` percent of the time, else false.
880
- fn chance(random: std.rand.Random, p: u8) bool {
881
- assert(p <= 100);
882
- return random.uintLessThanBiased(u8, 100) < p;
883
- }