tigerbeetle 0.0.34 → 0.0.37
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -0
- data/ext/tb_client/extconf.rb +13 -13
- data/ext/tb_client/tigerbeetle/LICENSE +177 -0
- data/ext/tb_client/tigerbeetle/build.zig +2327 -0
- data/ext/tb_client/tigerbeetle/src/aof.zig +1000 -0
- data/ext/tb_client/tigerbeetle/src/build_multiversion.zig +808 -0
- data/ext/tb_client/tigerbeetle/src/cdc/amqp/protocol.zig +1283 -0
- data/ext/tb_client/tigerbeetle/src/cdc/amqp/spec.zig +1704 -0
- data/ext/tb_client/tigerbeetle/src/cdc/amqp/types.zig +341 -0
- data/ext/tb_client/tigerbeetle/src/cdc/amqp.zig +1450 -0
- data/ext/tb_client/tigerbeetle/src/cdc/runner.zig +1659 -0
- data/ext/tb_client/tigerbeetle/src/clients/c/samples/main.c +406 -0
- data/ext/tb_client/tigerbeetle/src/clients/c/tb_client/context.zig +1084 -0
- data/ext/tb_client/tigerbeetle/src/clients/c/tb_client/echo_client.zig +286 -0
- data/ext/tb_client/tigerbeetle/src/clients/c/tb_client/packet.zig +158 -0
- data/ext/tb_client/tigerbeetle/src/clients/c/tb_client/signal.zig +229 -0
- data/ext/tb_client/tigerbeetle/src/clients/c/tb_client/signal_fuzz.zig +110 -0
- data/ext/tb_client/tigerbeetle/src/clients/c/tb_client.h +386 -0
- data/ext/tb_client/tigerbeetle/src/clients/c/tb_client.zig +34 -0
- data/ext/tb_client/tigerbeetle/src/clients/c/tb_client_exports.zig +281 -0
- data/ext/tb_client/tigerbeetle/src/clients/c/tb_client_header.zig +312 -0
- data/ext/tb_client/tigerbeetle/src/clients/c/tb_client_header_test.zig +138 -0
- data/ext/tb_client/tigerbeetle/src/clients/c/test.zig +466 -0
- data/ext/tb_client/tigerbeetle/src/clients/docs_samples.zig +157 -0
- data/ext/tb_client/tigerbeetle/src/clients/docs_types.zig +90 -0
- data/ext/tb_client/tigerbeetle/src/clients/dotnet/ci.zig +203 -0
- data/ext/tb_client/tigerbeetle/src/clients/dotnet/docs.zig +79 -0
- data/ext/tb_client/tigerbeetle/src/clients/dotnet/dotnet_bindings.zig +542 -0
- data/ext/tb_client/tigerbeetle/src/clients/go/ci.zig +109 -0
- data/ext/tb_client/tigerbeetle/src/clients/go/docs.zig +86 -0
- data/ext/tb_client/tigerbeetle/src/clients/go/go_bindings.zig +370 -0
- data/ext/tb_client/tigerbeetle/src/clients/go/pkg/native/tb_client.h +386 -0
- data/ext/tb_client/tigerbeetle/src/clients/java/ci.zig +167 -0
- data/ext/tb_client/tigerbeetle/src/clients/java/docs.zig +126 -0
- data/ext/tb_client/tigerbeetle/src/clients/java/java_bindings.zig +996 -0
- data/ext/tb_client/tigerbeetle/src/clients/java/src/client.zig +748 -0
- data/ext/tb_client/tigerbeetle/src/clients/java/src/jni.zig +3238 -0
- data/ext/tb_client/tigerbeetle/src/clients/java/src/jni_tests.zig +1718 -0
- data/ext/tb_client/tigerbeetle/src/clients/java/src/jni_thread_cleaner.zig +190 -0
- data/ext/tb_client/tigerbeetle/src/clients/node/ci.zig +104 -0
- data/ext/tb_client/tigerbeetle/src/clients/node/docs.zig +75 -0
- data/ext/tb_client/tigerbeetle/src/clients/node/node.zig +522 -0
- data/ext/tb_client/tigerbeetle/src/clients/node/node_bindings.zig +267 -0
- data/ext/tb_client/tigerbeetle/src/clients/node/src/c.zig +3 -0
- data/ext/tb_client/tigerbeetle/src/clients/node/src/translate.zig +379 -0
- data/ext/tb_client/tigerbeetle/src/clients/python/ci.zig +131 -0
- data/ext/tb_client/tigerbeetle/src/clients/python/docs.zig +63 -0
- data/ext/tb_client/tigerbeetle/src/clients/python/python_bindings.zig +588 -0
- data/ext/tb_client/tigerbeetle/src/clients/rust/assets/tb_client.h +386 -0
- data/ext/tb_client/tigerbeetle/src/clients/rust/ci.zig +73 -0
- data/ext/tb_client/tigerbeetle/src/clients/rust/docs.zig +106 -0
- data/ext/tb_client/tigerbeetle/src/clients/rust/rust_bindings.zig +305 -0
- data/ext/tb_client/tigerbeetle/src/config.zig +296 -0
- data/ext/tb_client/tigerbeetle/src/constants.zig +790 -0
- data/ext/tb_client/tigerbeetle/src/copyhound.zig +202 -0
- data/ext/tb_client/tigerbeetle/src/counting_allocator.zig +72 -0
- data/ext/tb_client/tigerbeetle/src/direction.zig +11 -0
- data/ext/tb_client/tigerbeetle/src/docs_website/build.zig +158 -0
- data/ext/tb_client/tigerbeetle/src/docs_website/src/content.zig +156 -0
- data/ext/tb_client/tigerbeetle/src/docs_website/src/docs.zig +252 -0
- data/ext/tb_client/tigerbeetle/src/docs_website/src/file_checker.zig +313 -0
- data/ext/tb_client/tigerbeetle/src/docs_website/src/html.zig +87 -0
- data/ext/tb_client/tigerbeetle/src/docs_website/src/page_writer.zig +63 -0
- data/ext/tb_client/tigerbeetle/src/docs_website/src/redirects.zig +47 -0
- data/ext/tb_client/tigerbeetle/src/docs_website/src/search_index_writer.zig +28 -0
- data/ext/tb_client/tigerbeetle/src/docs_website/src/service_worker_writer.zig +61 -0
- data/ext/tb_client/tigerbeetle/src/docs_website/src/single_page_writer.zig +169 -0
- data/ext/tb_client/tigerbeetle/src/docs_website/src/website.zig +46 -0
- data/ext/tb_client/tigerbeetle/src/ewah.zig +445 -0
- data/ext/tb_client/tigerbeetle/src/ewah_benchmark.zig +128 -0
- data/ext/tb_client/tigerbeetle/src/ewah_fuzz.zig +171 -0
- data/ext/tb_client/tigerbeetle/src/fuzz_tests.zig +179 -0
- data/ext/tb_client/tigerbeetle/src/integration_tests.zig +662 -0
- data/ext/tb_client/tigerbeetle/src/io/common.zig +155 -0
- data/ext/tb_client/tigerbeetle/src/io/darwin.zig +1093 -0
- data/ext/tb_client/tigerbeetle/src/io/linux.zig +1880 -0
- data/ext/tb_client/tigerbeetle/src/io/test.zig +1005 -0
- data/ext/tb_client/tigerbeetle/src/io/windows.zig +1598 -0
- data/ext/tb_client/tigerbeetle/src/io.zig +34 -0
- data/ext/tb_client/tigerbeetle/src/iops.zig +134 -0
- data/ext/tb_client/tigerbeetle/src/list.zig +236 -0
- data/ext/tb_client/tigerbeetle/src/lsm/binary_search.zig +848 -0
- data/ext/tb_client/tigerbeetle/src/lsm/binary_search_benchmark.zig +179 -0
- data/ext/tb_client/tigerbeetle/src/lsm/cache_map.zig +424 -0
- data/ext/tb_client/tigerbeetle/src/lsm/cache_map_fuzz.zig +420 -0
- data/ext/tb_client/tigerbeetle/src/lsm/compaction.zig +2117 -0
- data/ext/tb_client/tigerbeetle/src/lsm/composite_key.zig +182 -0
- data/ext/tb_client/tigerbeetle/src/lsm/forest.zig +1119 -0
- data/ext/tb_client/tigerbeetle/src/lsm/forest_fuzz.zig +1102 -0
- data/ext/tb_client/tigerbeetle/src/lsm/forest_table_iterator.zig +200 -0
- data/ext/tb_client/tigerbeetle/src/lsm/groove.zig +1495 -0
- data/ext/tb_client/tigerbeetle/src/lsm/k_way_merge.zig +739 -0
- data/ext/tb_client/tigerbeetle/src/lsm/k_way_merge_benchmark.zig +166 -0
- data/ext/tb_client/tigerbeetle/src/lsm/manifest.zig +754 -0
- data/ext/tb_client/tigerbeetle/src/lsm/manifest_level.zig +1294 -0
- data/ext/tb_client/tigerbeetle/src/lsm/manifest_level_fuzz.zig +510 -0
- data/ext/tb_client/tigerbeetle/src/lsm/manifest_log.zig +1263 -0
- data/ext/tb_client/tigerbeetle/src/lsm/manifest_log_fuzz.zig +628 -0
- data/ext/tb_client/tigerbeetle/src/lsm/node_pool.zig +247 -0
- data/ext/tb_client/tigerbeetle/src/lsm/scan_buffer.zig +116 -0
- data/ext/tb_client/tigerbeetle/src/lsm/scan_builder.zig +543 -0
- data/ext/tb_client/tigerbeetle/src/lsm/scan_fuzz.zig +938 -0
- data/ext/tb_client/tigerbeetle/src/lsm/scan_lookup.zig +293 -0
- data/ext/tb_client/tigerbeetle/src/lsm/scan_merge.zig +362 -0
- data/ext/tb_client/tigerbeetle/src/lsm/scan_range.zig +99 -0
- data/ext/tb_client/tigerbeetle/src/lsm/scan_state.zig +17 -0
- data/ext/tb_client/tigerbeetle/src/lsm/scan_tree.zig +1036 -0
- data/ext/tb_client/tigerbeetle/src/lsm/schema.zig +617 -0
- data/ext/tb_client/tigerbeetle/src/lsm/scratch_memory.zig +84 -0
- data/ext/tb_client/tigerbeetle/src/lsm/segmented_array.zig +1500 -0
- data/ext/tb_client/tigerbeetle/src/lsm/segmented_array_benchmark.zig +149 -0
- data/ext/tb_client/tigerbeetle/src/lsm/segmented_array_fuzz.zig +7 -0
- data/ext/tb_client/tigerbeetle/src/lsm/set_associative_cache.zig +865 -0
- data/ext/tb_client/tigerbeetle/src/lsm/table.zig +607 -0
- data/ext/tb_client/tigerbeetle/src/lsm/table_memory.zig +843 -0
- data/ext/tb_client/tigerbeetle/src/lsm/table_value_iterator.zig +105 -0
- data/ext/tb_client/tigerbeetle/src/lsm/timestamp_range.zig +40 -0
- data/ext/tb_client/tigerbeetle/src/lsm/tree.zig +630 -0
- data/ext/tb_client/tigerbeetle/src/lsm/tree_fuzz.zig +933 -0
- data/ext/tb_client/tigerbeetle/src/lsm/zig_zag_merge.zig +557 -0
- data/ext/tb_client/tigerbeetle/src/message_buffer.zig +469 -0
- data/ext/tb_client/tigerbeetle/src/message_bus.zig +1214 -0
- data/ext/tb_client/tigerbeetle/src/message_bus_fuzz.zig +936 -0
- data/ext/tb_client/tigerbeetle/src/message_pool.zig +343 -0
- data/ext/tb_client/tigerbeetle/src/multiversion.zig +2195 -0
- data/ext/tb_client/tigerbeetle/src/queue.zig +390 -0
- data/ext/tb_client/tigerbeetle/src/repl/completion.zig +201 -0
- data/ext/tb_client/tigerbeetle/src/repl/parser.zig +1356 -0
- data/ext/tb_client/tigerbeetle/src/repl/terminal.zig +496 -0
- data/ext/tb_client/tigerbeetle/src/repl.zig +1034 -0
- data/ext/tb_client/tigerbeetle/src/scripts/amqp.zig +973 -0
- data/ext/tb_client/tigerbeetle/src/scripts/cfo.zig +1866 -0
- data/ext/tb_client/tigerbeetle/src/scripts/changelog.zig +304 -0
- data/ext/tb_client/tigerbeetle/src/scripts/ci.zig +227 -0
- data/ext/tb_client/tigerbeetle/src/scripts/client_readmes.zig +658 -0
- data/ext/tb_client/tigerbeetle/src/scripts/devhub.zig +466 -0
- data/ext/tb_client/tigerbeetle/src/scripts/release.zig +1058 -0
- data/ext/tb_client/tigerbeetle/src/scripts.zig +105 -0
- data/ext/tb_client/tigerbeetle/src/shell.zig +1195 -0
- data/ext/tb_client/tigerbeetle/src/stack.zig +260 -0
- data/ext/tb_client/tigerbeetle/src/state_machine/auditor.zig +911 -0
- data/ext/tb_client/tigerbeetle/src/state_machine/workload.zig +2079 -0
- data/ext/tb_client/tigerbeetle/src/state_machine.zig +4872 -0
- data/ext/tb_client/tigerbeetle/src/state_machine_fuzz.zig +288 -0
- data/ext/tb_client/tigerbeetle/src/state_machine_tests.zig +3128 -0
- data/ext/tb_client/tigerbeetle/src/static_allocator.zig +82 -0
- data/ext/tb_client/tigerbeetle/src/stdx/bit_set.zig +157 -0
- data/ext/tb_client/tigerbeetle/src/stdx/bounded_array.zig +292 -0
- data/ext/tb_client/tigerbeetle/src/stdx/debug.zig +65 -0
- data/ext/tb_client/tigerbeetle/src/stdx/flags.zig +1414 -0
- data/ext/tb_client/tigerbeetle/src/stdx/mlock.zig +92 -0
- data/ext/tb_client/tigerbeetle/src/stdx/prng.zig +677 -0
- data/ext/tb_client/tigerbeetle/src/stdx/radix.zig +336 -0
- data/ext/tb_client/tigerbeetle/src/stdx/ring_buffer.zig +511 -0
- data/ext/tb_client/tigerbeetle/src/stdx/sort_test.zig +112 -0
- data/ext/tb_client/tigerbeetle/src/stdx/stdx.zig +1160 -0
- data/ext/tb_client/tigerbeetle/src/stdx/testing/low_level_hash_vectors.zig +142 -0
- data/ext/tb_client/tigerbeetle/src/stdx/testing/snaptest.zig +361 -0
- data/ext/tb_client/tigerbeetle/src/stdx/time_units.zig +275 -0
- data/ext/tb_client/tigerbeetle/src/stdx/unshare.zig +295 -0
- data/ext/tb_client/tigerbeetle/src/stdx/vendored/aegis.zig +436 -0
- data/ext/tb_client/tigerbeetle/src/stdx/windows.zig +48 -0
- data/ext/tb_client/tigerbeetle/src/stdx/zipfian.zig +402 -0
- data/ext/tb_client/tigerbeetle/src/storage.zig +489 -0
- data/ext/tb_client/tigerbeetle/src/storage_fuzz.zig +180 -0
- data/ext/tb_client/tigerbeetle/src/testing/bench.zig +146 -0
- data/ext/tb_client/tigerbeetle/src/testing/cluster/grid_checker.zig +53 -0
- data/ext/tb_client/tigerbeetle/src/testing/cluster/journal_checker.zig +61 -0
- data/ext/tb_client/tigerbeetle/src/testing/cluster/manifest_checker.zig +76 -0
- data/ext/tb_client/tigerbeetle/src/testing/cluster/message_bus.zig +110 -0
- data/ext/tb_client/tigerbeetle/src/testing/cluster/network.zig +412 -0
- data/ext/tb_client/tigerbeetle/src/testing/cluster/state_checker.zig +331 -0
- data/ext/tb_client/tigerbeetle/src/testing/cluster/storage_checker.zig +458 -0
- data/ext/tb_client/tigerbeetle/src/testing/cluster.zig +1198 -0
- data/ext/tb_client/tigerbeetle/src/testing/exhaustigen.zig +128 -0
- data/ext/tb_client/tigerbeetle/src/testing/fixtures.zig +181 -0
- data/ext/tb_client/tigerbeetle/src/testing/fuzz.zig +144 -0
- data/ext/tb_client/tigerbeetle/src/testing/id.zig +97 -0
- data/ext/tb_client/tigerbeetle/src/testing/io.zig +317 -0
- data/ext/tb_client/tigerbeetle/src/testing/marks.zig +126 -0
- data/ext/tb_client/tigerbeetle/src/testing/packet_simulator.zig +533 -0
- data/ext/tb_client/tigerbeetle/src/testing/reply_sequence.zig +154 -0
- data/ext/tb_client/tigerbeetle/src/testing/state_machine.zig +389 -0
- data/ext/tb_client/tigerbeetle/src/testing/storage.zig +1247 -0
- data/ext/tb_client/tigerbeetle/src/testing/table.zig +249 -0
- data/ext/tb_client/tigerbeetle/src/testing/time.zig +98 -0
- data/ext/tb_client/tigerbeetle/src/testing/tmp_tigerbeetle.zig +212 -0
- data/ext/tb_client/tigerbeetle/src/testing/vortex/constants.zig +26 -0
- data/ext/tb_client/tigerbeetle/src/testing/vortex/faulty_network.zig +580 -0
- data/ext/tb_client/tigerbeetle/src/testing/vortex/java_driver/ci.zig +39 -0
- data/ext/tb_client/tigerbeetle/src/testing/vortex/logged_process.zig +214 -0
- data/ext/tb_client/tigerbeetle/src/testing/vortex/rust_driver/ci.zig +34 -0
- data/ext/tb_client/tigerbeetle/src/testing/vortex/supervisor.zig +766 -0
- data/ext/tb_client/tigerbeetle/src/testing/vortex/workload.zig +543 -0
- data/ext/tb_client/tigerbeetle/src/testing/vortex/zig_driver.zig +181 -0
- data/ext/tb_client/tigerbeetle/src/tidy.zig +1448 -0
- data/ext/tb_client/tigerbeetle/src/tigerbeetle/benchmark_driver.zig +227 -0
- data/ext/tb_client/tigerbeetle/src/tigerbeetle/benchmark_load.zig +1069 -0
- data/ext/tb_client/tigerbeetle/src/tigerbeetle/cli.zig +1422 -0
- data/ext/tb_client/tigerbeetle/src/tigerbeetle/inspect.zig +1658 -0
- data/ext/tb_client/tigerbeetle/src/tigerbeetle/inspect_integrity.zig +518 -0
- data/ext/tb_client/tigerbeetle/src/tigerbeetle/libtb_client.zig +36 -0
- data/ext/tb_client/tigerbeetle/src/tigerbeetle/main.zig +646 -0
- data/ext/tb_client/tigerbeetle/src/tigerbeetle.zig +958 -0
- data/ext/tb_client/tigerbeetle/src/time.zig +236 -0
- data/ext/tb_client/tigerbeetle/src/trace/event.zig +745 -0
- data/ext/tb_client/tigerbeetle/src/trace/statsd.zig +462 -0
- data/ext/tb_client/tigerbeetle/src/trace.zig +556 -0
- data/ext/tb_client/tigerbeetle/src/unit_tests.zig +321 -0
- data/ext/tb_client/tigerbeetle/src/vopr.zig +1785 -0
- data/ext/tb_client/tigerbeetle/src/vortex.zig +101 -0
- data/ext/tb_client/tigerbeetle/src/vsr/checkpoint_trailer.zig +473 -0
- data/ext/tb_client/tigerbeetle/src/vsr/checksum.zig +208 -0
- data/ext/tb_client/tigerbeetle/src/vsr/checksum_benchmark.zig +43 -0
- data/ext/tb_client/tigerbeetle/src/vsr/client.zig +768 -0
- data/ext/tb_client/tigerbeetle/src/vsr/client_replies.zig +532 -0
- data/ext/tb_client/tigerbeetle/src/vsr/client_sessions.zig +338 -0
- data/ext/tb_client/tigerbeetle/src/vsr/clock.zig +1019 -0
- data/ext/tb_client/tigerbeetle/src/vsr/fault_detector.zig +279 -0
- data/ext/tb_client/tigerbeetle/src/vsr/free_set.zig +1381 -0
- data/ext/tb_client/tigerbeetle/src/vsr/free_set_fuzz.zig +315 -0
- data/ext/tb_client/tigerbeetle/src/vsr/grid.zig +1460 -0
- data/ext/tb_client/tigerbeetle/src/vsr/grid_blocks_missing.zig +757 -0
- data/ext/tb_client/tigerbeetle/src/vsr/grid_scrubber.zig +797 -0
- data/ext/tb_client/tigerbeetle/src/vsr/journal.zig +2586 -0
- data/ext/tb_client/tigerbeetle/src/vsr/marzullo.zig +308 -0
- data/ext/tb_client/tigerbeetle/src/vsr/message_header.zig +1777 -0
- data/ext/tb_client/tigerbeetle/src/vsr/multi_batch.zig +715 -0
- data/ext/tb_client/tigerbeetle/src/vsr/multi_batch_fuzz.zig +185 -0
- data/ext/tb_client/tigerbeetle/src/vsr/repair_budget.zig +333 -0
- data/ext/tb_client/tigerbeetle/src/vsr/replica.zig +12355 -0
- data/ext/tb_client/tigerbeetle/src/vsr/replica_format.zig +416 -0
- data/ext/tb_client/tigerbeetle/src/vsr/replica_reformat.zig +165 -0
- data/ext/tb_client/tigerbeetle/src/vsr/replica_test.zig +2910 -0
- data/ext/tb_client/tigerbeetle/src/vsr/routing.zig +1075 -0
- data/ext/tb_client/tigerbeetle/src/vsr/superblock.zig +1603 -0
- data/ext/tb_client/tigerbeetle/src/vsr/superblock_fuzz.zig +484 -0
- data/ext/tb_client/tigerbeetle/src/vsr/superblock_quorums.zig +405 -0
- data/ext/tb_client/tigerbeetle/src/vsr/superblock_quorums_fuzz.zig +355 -0
- data/ext/tb_client/tigerbeetle/src/vsr/sync.zig +29 -0
- data/ext/tb_client/tigerbeetle/src/vsr.zig +1727 -0
- data/lib/tb_client/shared_lib.rb +12 -5
- data/lib/tigerbeetle/client.rb +1 -1
- data/lib/tigerbeetle/platforms.rb +9 -0
- data/lib/tigerbeetle/version.rb +2 -2
- data/tigerbeetle.gemspec +22 -5
- metadata +242 -3
- data/ext/tb_client/pkg.tar.gz +0 -0
|
@@ -0,0 +1,2079 @@
|
|
|
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
|
+
|
|
23
|
+
const stdx = @import("stdx");
|
|
24
|
+
const maybe = stdx.maybe;
|
|
25
|
+
const Ratio = stdx.PRNG.Ratio;
|
|
26
|
+
const ratio = stdx.PRNG.ratio;
|
|
27
|
+
|
|
28
|
+
const constants = @import("../constants.zig");
|
|
29
|
+
const tb = @import("../tigerbeetle.zig");
|
|
30
|
+
const vsr = @import("../vsr.zig");
|
|
31
|
+
const accounting_auditor = @import("auditor.zig");
|
|
32
|
+
const Auditor = accounting_auditor.AccountingAuditor;
|
|
33
|
+
const IteratorForCreateType = accounting_auditor.IteratorForCreateType;
|
|
34
|
+
const IdPermutation = @import("../testing/id.zig").IdPermutation;
|
|
35
|
+
const TimestampRange = @import("../lsm/timestamp_range.zig").TimestampRange;
|
|
36
|
+
const fuzz = @import("../testing/fuzz.zig");
|
|
37
|
+
|
|
38
|
+
const PriorityQueue = std.PriorityQueue;
|
|
39
|
+
|
|
40
|
+
const TransferOutcome = enum {
|
|
41
|
+
/// The transfer is guaranteed to commit.
|
|
42
|
+
/// For example, a single-phase transfer between valid accounts without balance limits.
|
|
43
|
+
success,
|
|
44
|
+
/// The transfer is invalid. For example, the `ledger` field is missing.
|
|
45
|
+
failure,
|
|
46
|
+
/// Due to races with timeouts or other transfers, the outcome of the transfer is uncertain.
|
|
47
|
+
/// For example, post/void-pending transfers race with their timeout.
|
|
48
|
+
unknown,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/// A Transfer generated from the plan is guaranteed to have a matching `outcome`, but it may use a
|
|
52
|
+
/// different Method. (For example, `method=pending` may fall back to `method=single_phase` if the
|
|
53
|
+
/// Auditor's pending transfer queue is full).
|
|
54
|
+
const TransferPlan = struct {
|
|
55
|
+
/// When false, send invalid payments that are guaranteed to be rejected with an error.
|
|
56
|
+
valid: bool,
|
|
57
|
+
|
|
58
|
+
/// When `limit` is set, at least one of the following is true:
|
|
59
|
+
///
|
|
60
|
+
/// * the debit account has debits_must_not_exceed_credits
|
|
61
|
+
/// * the credit account has credits_must_not_exceed_debits
|
|
62
|
+
///
|
|
63
|
+
limit: bool,
|
|
64
|
+
|
|
65
|
+
method: Method,
|
|
66
|
+
|
|
67
|
+
const Method = enum {
|
|
68
|
+
single_phase,
|
|
69
|
+
pending,
|
|
70
|
+
post_pending,
|
|
71
|
+
void_pending,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
fn outcome(self: TransferPlan) TransferOutcome {
|
|
75
|
+
if (!self.valid) return .failure;
|
|
76
|
+
if (self.limit) return .unknown;
|
|
77
|
+
return switch (self.method) {
|
|
78
|
+
.single_phase, .pending => .success,
|
|
79
|
+
.post_pending, .void_pending => .unknown,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const TransferTemplate = struct {
|
|
85
|
+
ledger: u32,
|
|
86
|
+
result: accounting_auditor.CreateTransferResultSet,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const TransferBatchQueue = PriorityQueue(TransferBatch, void, struct {
|
|
90
|
+
/// Ascending order.
|
|
91
|
+
fn compare(_: void, a: TransferBatch, b: TransferBatch) std.math.Order {
|
|
92
|
+
assert(a.min != b.min);
|
|
93
|
+
assert(a.max != b.max);
|
|
94
|
+
return std.math.order(a.min, b.min);
|
|
95
|
+
}
|
|
96
|
+
}.compare);
|
|
97
|
+
|
|
98
|
+
const TransferBatch = struct {
|
|
99
|
+
/// Index of the first transfer in the batch.
|
|
100
|
+
min: usize,
|
|
101
|
+
/// Index of the last transfer in the batch.
|
|
102
|
+
max: usize,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/// Indexes: [valid:bool][limit:bool][method]
|
|
106
|
+
const transfer_templates = table: {
|
|
107
|
+
@setEvalBranchQuota(4_000);
|
|
108
|
+
|
|
109
|
+
const SNGL = @intFromEnum(TransferPlan.Method.single_phase);
|
|
110
|
+
const PEND = @intFromEnum(TransferPlan.Method.pending);
|
|
111
|
+
const POST = @intFromEnum(TransferPlan.Method.post_pending);
|
|
112
|
+
const VOID = @intFromEnum(TransferPlan.Method.void_pending);
|
|
113
|
+
const Result = accounting_auditor.CreateTransferResultSet;
|
|
114
|
+
const result = Result.init;
|
|
115
|
+
|
|
116
|
+
const InitValues = std.enums.EnumFieldStruct(
|
|
117
|
+
tb.CreateTransferResult.Ordered,
|
|
118
|
+
bool,
|
|
119
|
+
false,
|
|
120
|
+
);
|
|
121
|
+
const two_phase_ok: InitValues = .{
|
|
122
|
+
.ok = true,
|
|
123
|
+
.pending_transfer_already_posted = true,
|
|
124
|
+
.pending_transfer_already_voided = true,
|
|
125
|
+
.pending_transfer_expired = true,
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const limits = result(.{
|
|
129
|
+
.exceeds_credits = true,
|
|
130
|
+
.exceeds_debits = true,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const either = struct {
|
|
134
|
+
fn either(a: Result, b: Result) Result {
|
|
135
|
+
var c = a;
|
|
136
|
+
c.setUnion(b);
|
|
137
|
+
return c;
|
|
138
|
+
}
|
|
139
|
+
}.either;
|
|
140
|
+
|
|
141
|
+
const template = struct {
|
|
142
|
+
fn template(ledger: u32, transfer_result: Result) TransferTemplate {
|
|
143
|
+
return .{
|
|
144
|
+
.ledger = ledger,
|
|
145
|
+
.result = transfer_result,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}.template;
|
|
149
|
+
|
|
150
|
+
// [valid:bool][limit:bool][method]
|
|
151
|
+
var templates: [2][2][std.meta.fields(TransferPlan.Method).len]TransferTemplate = undefined;
|
|
152
|
+
|
|
153
|
+
// template(ledger, result)
|
|
154
|
+
templates[0][0][SNGL] = template(0, result(.{ .ledger_must_not_be_zero = true }));
|
|
155
|
+
templates[0][0][PEND] = template(0, result(.{ .ledger_must_not_be_zero = true }));
|
|
156
|
+
templates[0][0][POST] = template(9, result(.{ .pending_transfer_has_different_ledger = true }));
|
|
157
|
+
templates[0][0][VOID] = template(9, result(.{ .pending_transfer_has_different_ledger = true }));
|
|
158
|
+
|
|
159
|
+
templates[0][1][SNGL] = template(0, result(.{ .ledger_must_not_be_zero = true }));
|
|
160
|
+
templates[0][1][PEND] = template(0, result(.{ .ledger_must_not_be_zero = true }));
|
|
161
|
+
templates[0][1][POST] = template(9, result(.{ .pending_transfer_has_different_ledger = true }));
|
|
162
|
+
templates[0][1][VOID] = template(9, result(.{ .pending_transfer_has_different_ledger = true }));
|
|
163
|
+
|
|
164
|
+
templates[1][0][SNGL] = template(1, result(.{ .ok = true }));
|
|
165
|
+
templates[1][0][PEND] = template(1, result(.{ .ok = true }));
|
|
166
|
+
templates[1][0][POST] = template(1, result(two_phase_ok));
|
|
167
|
+
templates[1][0][VOID] = template(1, result(two_phase_ok));
|
|
168
|
+
|
|
169
|
+
templates[1][1][SNGL] = template(1, either(limits, result(.{ .ok = true })));
|
|
170
|
+
templates[1][1][PEND] = template(1, either(limits, result(.{ .ok = true })));
|
|
171
|
+
templates[1][1][POST] = template(1, either(limits, result(two_phase_ok)));
|
|
172
|
+
templates[1][1][VOID] = template(1, either(limits, result(two_phase_ok)));
|
|
173
|
+
|
|
174
|
+
break :table templates;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
pub fn WorkloadType(comptime AccountingStateMachine: type) type {
|
|
178
|
+
const Operation = AccountingStateMachine.Operation;
|
|
179
|
+
|
|
180
|
+
const Action = enum(u8) {
|
|
181
|
+
create_accounts = @intFromEnum(Operation.create_accounts),
|
|
182
|
+
create_transfers = @intFromEnum(Operation.create_transfers),
|
|
183
|
+
lookup_accounts = @intFromEnum(Operation.lookup_accounts),
|
|
184
|
+
lookup_transfers = @intFromEnum(Operation.lookup_transfers),
|
|
185
|
+
get_account_transfers = @intFromEnum(Operation.get_account_transfers),
|
|
186
|
+
get_account_balances = @intFromEnum(Operation.get_account_balances),
|
|
187
|
+
query_accounts = @intFromEnum(Operation.query_accounts),
|
|
188
|
+
query_transfers = @intFromEnum(Operation.query_transfers),
|
|
189
|
+
get_change_events = @intFromEnum(Operation.get_change_events),
|
|
190
|
+
|
|
191
|
+
deprecated_create_accounts_unbatched = @intFromEnum(
|
|
192
|
+
Operation.deprecated_create_accounts_unbatched,
|
|
193
|
+
),
|
|
194
|
+
deprecated_create_transfers_unbatched = @intFromEnum(
|
|
195
|
+
Operation.deprecated_create_transfers_unbatched,
|
|
196
|
+
),
|
|
197
|
+
deprecated_lookup_accounts_unbatched = @intFromEnum(
|
|
198
|
+
Operation.deprecated_lookup_accounts_unbatched,
|
|
199
|
+
),
|
|
200
|
+
deprecated_lookup_transfers_unbatched = @intFromEnum(
|
|
201
|
+
Operation.deprecated_lookup_transfers_unbatched,
|
|
202
|
+
),
|
|
203
|
+
deprecated_get_account_transfers_unbatched = @intFromEnum(
|
|
204
|
+
Operation.deprecated_get_account_transfers_unbatched,
|
|
205
|
+
),
|
|
206
|
+
deprecated_get_account_balances_unbatched = @intFromEnum(
|
|
207
|
+
Operation.deprecated_get_account_balances_unbatched,
|
|
208
|
+
),
|
|
209
|
+
deprecated_query_accounts_unbatched = @intFromEnum(
|
|
210
|
+
Operation.deprecated_query_accounts_unbatched,
|
|
211
|
+
),
|
|
212
|
+
deprecated_query_transfers_unbatched = @intFromEnum(
|
|
213
|
+
Operation.deprecated_query_transfers_unbatched,
|
|
214
|
+
),
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const Lookup = enum {
|
|
218
|
+
/// Query a transfer that has either been committed or rejected.
|
|
219
|
+
delivered,
|
|
220
|
+
/// Query a transfer whose `create_transfers` is in-flight.
|
|
221
|
+
sending,
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
return struct {
|
|
225
|
+
const Workload = @This();
|
|
226
|
+
|
|
227
|
+
pub const Options = OptionsType(AccountingStateMachine, Action, Lookup);
|
|
228
|
+
|
|
229
|
+
prng: *stdx.PRNG,
|
|
230
|
+
auditor: Auditor,
|
|
231
|
+
options: Options,
|
|
232
|
+
|
|
233
|
+
transfer_plan_seed: u64,
|
|
234
|
+
|
|
235
|
+
/// Whether a `create_accounts` message has ever been sent.
|
|
236
|
+
accounts_sent: bool = false,
|
|
237
|
+
|
|
238
|
+
/// The index of the next transfer to send.
|
|
239
|
+
transfers_sent: usize = 0,
|
|
240
|
+
|
|
241
|
+
/// All transfers below this index have been delivered.
|
|
242
|
+
/// Any transfers above this index that have been delivered are stored in
|
|
243
|
+
/// `transfers_delivered_recently`.
|
|
244
|
+
transfers_delivered_past: usize = 0,
|
|
245
|
+
|
|
246
|
+
/// Track index ranges of `create_transfers` batches that have committed but are greater
|
|
247
|
+
/// than or equal to `transfers_delivered_past` (which is still in-flight).
|
|
248
|
+
transfers_delivered_recently: TransferBatchQueue,
|
|
249
|
+
|
|
250
|
+
/// Track the number of pending transfers that have been sent but not committed.
|
|
251
|
+
transfers_pending_in_flight: usize = 0,
|
|
252
|
+
|
|
253
|
+
/// Transfers that succeeded and must result in `exists` when retried.
|
|
254
|
+
transfers_retry_exists: std.ArrayListUnmanaged(tb.Transfer),
|
|
255
|
+
|
|
256
|
+
/// IDs of transfers that failed with transient codes
|
|
257
|
+
/// and must result in `id_already_failed` when retried.
|
|
258
|
+
transfers_retry_failed: std.AutoArrayHashMapUnmanaged(u128, void),
|
|
259
|
+
|
|
260
|
+
pub fn init(
|
|
261
|
+
allocator: std.mem.Allocator,
|
|
262
|
+
prng: *stdx.PRNG,
|
|
263
|
+
options: Options,
|
|
264
|
+
) !Workload {
|
|
265
|
+
assert(options.accounts_batch_size_span + options.accounts_batch_size_min <=
|
|
266
|
+
AccountingStateMachine.batch_max.create_accounts);
|
|
267
|
+
assert(options.accounts_batch_size_span >= 1);
|
|
268
|
+
assert(options.transfers_batch_size_span + options.transfers_batch_size_min <=
|
|
269
|
+
AccountingStateMachine.batch_max.create_transfers);
|
|
270
|
+
assert(options.transfers_batch_size_span >= 1);
|
|
271
|
+
|
|
272
|
+
var auditor = try Auditor.init(allocator, prng, options.auditor_options);
|
|
273
|
+
errdefer auditor.deinit(allocator);
|
|
274
|
+
|
|
275
|
+
var transfers_delivered_recently = TransferBatchQueue.init(allocator, {});
|
|
276
|
+
errdefer transfers_delivered_recently.deinit();
|
|
277
|
+
try transfers_delivered_recently.ensureTotalCapacity(
|
|
278
|
+
options.auditor_options.client_count * constants.client_request_queue_max,
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
for (auditor.accounts, 0..) |*account, i| {
|
|
282
|
+
const query_intersection =
|
|
283
|
+
auditor.query_intersections[prng.index(auditor.query_intersections)];
|
|
284
|
+
|
|
285
|
+
account.* = std.mem.zeroInit(tb.Account, .{
|
|
286
|
+
.id = auditor.account_index_to_id(i),
|
|
287
|
+
.user_data_64 = query_intersection.user_data_64,
|
|
288
|
+
.user_data_32 = query_intersection.user_data_32,
|
|
289
|
+
.code = query_intersection.code,
|
|
290
|
+
.ledger = 1,
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
if (prng.chance(options.account_limit_probability)) {
|
|
294
|
+
const b = prng.boolean();
|
|
295
|
+
account.flags.debits_must_not_exceed_credits = b;
|
|
296
|
+
account.flags.credits_must_not_exceed_debits = !b;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
account.flags.history = prng.chance(options.account_history_probability);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
var transfers_retry_failed: std.AutoArrayHashMapUnmanaged(u128, void) = .{};
|
|
303
|
+
try transfers_retry_failed.ensureTotalCapacity(
|
|
304
|
+
allocator,
|
|
305
|
+
options.transfers_retry_failed_max,
|
|
306
|
+
);
|
|
307
|
+
errdefer transfers_retry_failed.deinit(allocator);
|
|
308
|
+
|
|
309
|
+
var transfers_retry_exists: std.ArrayListUnmanaged(tb.Transfer) = try .initCapacity(
|
|
310
|
+
allocator,
|
|
311
|
+
options.transfers_retry_exists_max,
|
|
312
|
+
);
|
|
313
|
+
errdefer transfers_retry_exists.deinit(allocator);
|
|
314
|
+
|
|
315
|
+
return .{
|
|
316
|
+
.prng = prng,
|
|
317
|
+
.auditor = auditor,
|
|
318
|
+
.options = options,
|
|
319
|
+
.transfer_plan_seed = prng.int(u64),
|
|
320
|
+
.transfers_delivered_recently = transfers_delivered_recently,
|
|
321
|
+
.transfers_retry_failed = transfers_retry_failed,
|
|
322
|
+
.transfers_retry_exists = transfers_retry_exists,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
pub fn deinit(self: *Workload, allocator: std.mem.Allocator) void {
|
|
327
|
+
self.auditor.deinit(allocator);
|
|
328
|
+
self.transfers_delivered_recently.deinit();
|
|
329
|
+
self.transfers_retry_failed.deinit(allocator);
|
|
330
|
+
self.transfers_retry_exists.deinit(allocator);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
pub fn done(self: *const Workload) bool {
|
|
334
|
+
if (self.transfers_delivered_recently.len != 0) return false;
|
|
335
|
+
return self.auditor.done();
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/// A client may build multiple requests to queue up while another is in-flight.
|
|
339
|
+
pub fn build_request(
|
|
340
|
+
self: *Workload,
|
|
341
|
+
client_index: usize,
|
|
342
|
+
body_buffer: []align(@alignOf(vsr.Header)) u8,
|
|
343
|
+
) struct {
|
|
344
|
+
operation: Operation,
|
|
345
|
+
size: usize,
|
|
346
|
+
} {
|
|
347
|
+
assert(client_index < self.auditor.options.client_count);
|
|
348
|
+
assert(body_buffer.len == constants.message_body_size_max);
|
|
349
|
+
|
|
350
|
+
const action = action: {
|
|
351
|
+
if (!self.accounts_sent and self.prng.boolean()) {
|
|
352
|
+
// Early in the test make sure some accounts get created.
|
|
353
|
+
self.accounts_sent = true;
|
|
354
|
+
break :action .create_accounts;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
break :action self.prng.enum_weighted(Action, self.options.operations);
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
const operation: Operation = @enumFromInt(@intFromEnum(action));
|
|
361
|
+
const event_size: u32 = operation.event_size();
|
|
362
|
+
const event_max: u32 = operation.event_max(self.options.batch_size_limit);
|
|
363
|
+
assert(event_max > 0);
|
|
364
|
+
assert(body_buffer.len >= event_size * event_max);
|
|
365
|
+
|
|
366
|
+
const result_size: u32 = operation.result_size();
|
|
367
|
+
const result_max = operation.result_max(self.options.batch_size_limit);
|
|
368
|
+
assert(result_max > 0);
|
|
369
|
+
assert(constants.message_body_size_max >=
|
|
370
|
+
result_size * result_max);
|
|
371
|
+
|
|
372
|
+
if (!operation.is_multi_batch()) {
|
|
373
|
+
const size = self.build_request_batch(
|
|
374
|
+
client_index,
|
|
375
|
+
action,
|
|
376
|
+
body_buffer,
|
|
377
|
+
event_max,
|
|
378
|
+
);
|
|
379
|
+
assert(size <= body_buffer.len);
|
|
380
|
+
return .{
|
|
381
|
+
.operation = operation,
|
|
382
|
+
.size = size,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
assert(operation.is_multi_batch());
|
|
386
|
+
|
|
387
|
+
var body_encoder = vsr.multi_batch.MultiBatchEncoder.init(
|
|
388
|
+
body_buffer[0..self.options.batch_size_limit],
|
|
389
|
+
.{
|
|
390
|
+
.element_size = event_size,
|
|
391
|
+
},
|
|
392
|
+
);
|
|
393
|
+
var event_count: u32 = 0;
|
|
394
|
+
var result_count: u32 = 0;
|
|
395
|
+
for (0..self.options.multi_batch_per_request_limit) |_| {
|
|
396
|
+
const writable = body_encoder.writable() orelse break;
|
|
397
|
+
if (writable.len == 0) break;
|
|
398
|
+
|
|
399
|
+
const event_count_remain: u32 =
|
|
400
|
+
if (operation.is_batchable())
|
|
401
|
+
event_max - event_count
|
|
402
|
+
else
|
|
403
|
+
1;
|
|
404
|
+
const batch_size = self.build_request_batch(
|
|
405
|
+
client_index,
|
|
406
|
+
action,
|
|
407
|
+
writable,
|
|
408
|
+
event_count_remain,
|
|
409
|
+
);
|
|
410
|
+
assert(batch_size <= writable.len);
|
|
411
|
+
|
|
412
|
+
// Checking if the expected result will fit in the multi-batch reply.
|
|
413
|
+
const reply_trailer_size: u32 = vsr.multi_batch.trailer_total_size(.{
|
|
414
|
+
.element_size = result_size,
|
|
415
|
+
.batch_count = body_encoder.batch_count + 1,
|
|
416
|
+
});
|
|
417
|
+
const result_count_expected: u32 =
|
|
418
|
+
operation.result_count_expected(writable[0..batch_size]);
|
|
419
|
+
const reply_message_size: u32 =
|
|
420
|
+
((result_count + result_count_expected) * result_size) + reply_trailer_size;
|
|
421
|
+
if (reply_message_size > constants.message_body_size_max) {
|
|
422
|
+
// For operations that produce 1:1 result per event
|
|
423
|
+
// (e.g., `create_*` and `lookup_*`), this was already validated
|
|
424
|
+
// when checking if `event_count` fits within the multi-batch request.
|
|
425
|
+
assert(!operation.is_batchable());
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
assert(result_count + result_count_expected <= result_max);
|
|
429
|
+
|
|
430
|
+
body_encoder.add(@intCast(batch_size));
|
|
431
|
+
event_count += @intCast(@divExact(batch_size, event_size));
|
|
432
|
+
assert(event_count <= event_max);
|
|
433
|
+
|
|
434
|
+
result_count += result_count_expected;
|
|
435
|
+
assert(result_count <= result_max);
|
|
436
|
+
|
|
437
|
+
// Maybe single-batch request.
|
|
438
|
+
if (body_encoder.batch_count == 1 and self.prng.boolean()) break;
|
|
439
|
+
}
|
|
440
|
+
maybe(event_count == 0);
|
|
441
|
+
assert(result_count == 0 or event_count > 0);
|
|
442
|
+
assert(body_encoder.batch_count > 0);
|
|
443
|
+
assert(body_encoder.batch_count <= self.options.multi_batch_per_request_limit);
|
|
444
|
+
|
|
445
|
+
const bytes_written = body_encoder.finish();
|
|
446
|
+
assert(bytes_written <= self.options.batch_size_limit);
|
|
447
|
+
|
|
448
|
+
return .{
|
|
449
|
+
.operation = operation,
|
|
450
|
+
.size = bytes_written,
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
fn build_request_batch(
|
|
455
|
+
self: *Workload,
|
|
456
|
+
client_index: usize,
|
|
457
|
+
action: Action,
|
|
458
|
+
body: []u8,
|
|
459
|
+
batch_limit: u32,
|
|
460
|
+
) usize {
|
|
461
|
+
switch (action) {
|
|
462
|
+
inline else => |action_comptime| {
|
|
463
|
+
const operation_comptime = comptime std.enums.nameCast(
|
|
464
|
+
Operation,
|
|
465
|
+
action_comptime,
|
|
466
|
+
);
|
|
467
|
+
const Event = operation_comptime.EventType();
|
|
468
|
+
const event_size: u32 = operation_comptime.event_size();
|
|
469
|
+
const batchable: []Event = self.batch(
|
|
470
|
+
Event,
|
|
471
|
+
action_comptime,
|
|
472
|
+
body,
|
|
473
|
+
batch_limit,
|
|
474
|
+
);
|
|
475
|
+
assert(batchable.len <= batch_limit);
|
|
476
|
+
|
|
477
|
+
const count = switch (action_comptime) {
|
|
478
|
+
.create_accounts,
|
|
479
|
+
.deprecated_create_accounts_unbatched,
|
|
480
|
+
=> self.build_create_accounts(
|
|
481
|
+
client_index,
|
|
482
|
+
batchable,
|
|
483
|
+
),
|
|
484
|
+
.create_transfers,
|
|
485
|
+
.deprecated_create_transfers_unbatched,
|
|
486
|
+
=> self.build_create_transfers(
|
|
487
|
+
client_index,
|
|
488
|
+
batchable,
|
|
489
|
+
),
|
|
490
|
+
.lookup_accounts,
|
|
491
|
+
.deprecated_lookup_accounts_unbatched,
|
|
492
|
+
=> self.build_lookup_accounts(batchable),
|
|
493
|
+
.lookup_transfers,
|
|
494
|
+
.deprecated_lookup_transfers_unbatched,
|
|
495
|
+
=> self.build_lookup_transfers(batchable),
|
|
496
|
+
.get_account_transfers,
|
|
497
|
+
.get_account_balances,
|
|
498
|
+
.deprecated_get_account_transfers_unbatched,
|
|
499
|
+
.deprecated_get_account_balances_unbatched,
|
|
500
|
+
=> self.build_get_account_filter(
|
|
501
|
+
client_index,
|
|
502
|
+
action_comptime,
|
|
503
|
+
batchable,
|
|
504
|
+
),
|
|
505
|
+
.query_accounts,
|
|
506
|
+
.query_transfers,
|
|
507
|
+
.deprecated_query_accounts_unbatched,
|
|
508
|
+
.deprecated_query_transfers_unbatched,
|
|
509
|
+
=> self.build_query_filter(
|
|
510
|
+
client_index,
|
|
511
|
+
action_comptime,
|
|
512
|
+
batchable,
|
|
513
|
+
),
|
|
514
|
+
.get_change_events => self.build_get_change_events_filter(
|
|
515
|
+
client_index,
|
|
516
|
+
batchable,
|
|
517
|
+
),
|
|
518
|
+
};
|
|
519
|
+
assert(count <= batchable.len);
|
|
520
|
+
assert(count <= batch_limit);
|
|
521
|
+
|
|
522
|
+
const batch_size: usize = count * event_size;
|
|
523
|
+
assert(batch_size <= body.len);
|
|
524
|
+
maybe(batch_size == 0);
|
|
525
|
+
return batch_size;
|
|
526
|
+
},
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/// `on_reply` is called for replies in commit order.
|
|
531
|
+
pub fn on_reply(
|
|
532
|
+
self: *Workload,
|
|
533
|
+
client_index: usize,
|
|
534
|
+
operation: Operation,
|
|
535
|
+
timestamp: u64,
|
|
536
|
+
request_body: []const u8,
|
|
537
|
+
reply_body: []const u8,
|
|
538
|
+
) void {
|
|
539
|
+
assert(timestamp != 0);
|
|
540
|
+
assert(request_body.len <= constants.message_body_size_max);
|
|
541
|
+
assert(reply_body.len <= constants.message_body_size_max);
|
|
542
|
+
|
|
543
|
+
if (!operation.is_multi_batch()) {
|
|
544
|
+
return self.on_reply_batch(
|
|
545
|
+
client_index,
|
|
546
|
+
operation,
|
|
547
|
+
timestamp,
|
|
548
|
+
request_body,
|
|
549
|
+
reply_body,
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
assert(operation.is_multi_batch());
|
|
553
|
+
|
|
554
|
+
const event_size: u32 = operation.event_size();
|
|
555
|
+
const result_size: u32 = operation.result_size();
|
|
556
|
+
var body_decoder = vsr.multi_batch.MultiBatchDecoder.init(request_body, .{
|
|
557
|
+
.element_size = event_size,
|
|
558
|
+
}) catch unreachable;
|
|
559
|
+
assert(body_decoder.batch_count() > 0);
|
|
560
|
+
var reply_decoder = vsr.multi_batch.MultiBatchDecoder.init(reply_body, .{
|
|
561
|
+
.element_size = result_size,
|
|
562
|
+
}) catch unreachable;
|
|
563
|
+
assert(reply_decoder.batch_count() > 0);
|
|
564
|
+
assert(body_decoder.batch_count() == reply_decoder.batch_count());
|
|
565
|
+
|
|
566
|
+
const prepare_nanoseconds = struct {
|
|
567
|
+
fn prepare_nanoseconds(
|
|
568
|
+
operation_inner: Operation,
|
|
569
|
+
input_len: usize,
|
|
570
|
+
batch_size_limit: u32,
|
|
571
|
+
) u64 {
|
|
572
|
+
return switch (operation_inner) {
|
|
573
|
+
.pulse => Operation.create_transfers.event_max(
|
|
574
|
+
batch_size_limit,
|
|
575
|
+
),
|
|
576
|
+
.create_accounts => @divExact(input_len, @sizeOf(tb.Account)),
|
|
577
|
+
.create_transfers => @divExact(input_len, @sizeOf(tb.Transfer)),
|
|
578
|
+
.lookup_accounts => 0,
|
|
579
|
+
.lookup_transfers => 0,
|
|
580
|
+
.get_account_transfers => 0,
|
|
581
|
+
.get_account_balances => 0,
|
|
582
|
+
.query_accounts => 0,
|
|
583
|
+
.query_transfers => 0,
|
|
584
|
+
.get_change_events => 0,
|
|
585
|
+
else => unreachable,
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
}.prepare_nanoseconds;
|
|
589
|
+
var batch_timestamp: u64 = timestamp - prepare_nanoseconds(
|
|
590
|
+
operation,
|
|
591
|
+
body_decoder.payload.len,
|
|
592
|
+
self.options.batch_size_limit,
|
|
593
|
+
);
|
|
594
|
+
while (body_decoder.pop()) |batch_body| {
|
|
595
|
+
const batch_reply = reply_decoder.pop().?;
|
|
596
|
+
batch_timestamp += prepare_nanoseconds(
|
|
597
|
+
operation,
|
|
598
|
+
batch_body.len,
|
|
599
|
+
self.options.batch_size_limit,
|
|
600
|
+
);
|
|
601
|
+
self.on_reply_batch(
|
|
602
|
+
client_index,
|
|
603
|
+
operation,
|
|
604
|
+
batch_timestamp,
|
|
605
|
+
batch_body,
|
|
606
|
+
batch_reply,
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
assert(reply_decoder.pop() == null);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
pub fn on_reply_batch(
|
|
613
|
+
self: *Workload,
|
|
614
|
+
client_index: usize,
|
|
615
|
+
operation: Operation,
|
|
616
|
+
timestamp: u64,
|
|
617
|
+
request_body: []const u8,
|
|
618
|
+
reply_body: []const u8,
|
|
619
|
+
) void {
|
|
620
|
+
switch (operation) {
|
|
621
|
+
.create_accounts,
|
|
622
|
+
.deprecated_create_accounts_unbatched,
|
|
623
|
+
=> self.auditor.on_create_accounts(
|
|
624
|
+
client_index,
|
|
625
|
+
timestamp,
|
|
626
|
+
stdx.bytes_as_slice(.exact, tb.Account, request_body),
|
|
627
|
+
stdx.bytes_as_slice(.exact, tb.CreateAccountsResult, reply_body),
|
|
628
|
+
),
|
|
629
|
+
.create_transfers,
|
|
630
|
+
.deprecated_create_transfers_unbatched,
|
|
631
|
+
=> self.on_create_transfers(
|
|
632
|
+
client_index,
|
|
633
|
+
timestamp,
|
|
634
|
+
stdx.bytes_as_slice(.exact, tb.Transfer, request_body),
|
|
635
|
+
stdx.bytes_as_slice(.exact, tb.CreateTransfersResult, reply_body),
|
|
636
|
+
),
|
|
637
|
+
.lookup_accounts,
|
|
638
|
+
.deprecated_lookup_accounts_unbatched,
|
|
639
|
+
=> self.auditor.on_lookup_accounts(
|
|
640
|
+
client_index,
|
|
641
|
+
timestamp,
|
|
642
|
+
stdx.bytes_as_slice(.exact, u128, request_body),
|
|
643
|
+
stdx.bytes_as_slice(.exact, tb.Account, reply_body),
|
|
644
|
+
),
|
|
645
|
+
.lookup_transfers,
|
|
646
|
+
.deprecated_lookup_transfers_unbatched,
|
|
647
|
+
=> self.on_lookup_transfers(
|
|
648
|
+
client_index,
|
|
649
|
+
timestamp,
|
|
650
|
+
stdx.bytes_as_slice(.exact, u128, request_body),
|
|
651
|
+
stdx.bytes_as_slice(.exact, tb.Transfer, reply_body),
|
|
652
|
+
),
|
|
653
|
+
inline .get_account_transfers,
|
|
654
|
+
.deprecated_get_account_transfers_unbatched,
|
|
655
|
+
=> |operation_comptime| self.on_get_account_transfers(
|
|
656
|
+
operation_comptime,
|
|
657
|
+
timestamp,
|
|
658
|
+
stdx.bytes_as_slice(.exact, tb.AccountFilter, request_body),
|
|
659
|
+
stdx.bytes_as_slice(.exact, tb.Transfer, reply_body),
|
|
660
|
+
),
|
|
661
|
+
inline .get_account_balances,
|
|
662
|
+
.deprecated_get_account_balances_unbatched,
|
|
663
|
+
=> |operation_comptime| self.on_get_account_balances(
|
|
664
|
+
operation_comptime,
|
|
665
|
+
timestamp,
|
|
666
|
+
stdx.bytes_as_slice(.exact, tb.AccountFilter, request_body),
|
|
667
|
+
stdx.bytes_as_slice(.exact, tb.AccountBalance, reply_body),
|
|
668
|
+
),
|
|
669
|
+
inline .query_accounts,
|
|
670
|
+
.deprecated_query_accounts_unbatched,
|
|
671
|
+
=> |operation_comptime| self.on_query(
|
|
672
|
+
operation_comptime,
|
|
673
|
+
timestamp,
|
|
674
|
+
stdx.bytes_as_slice(.exact, tb.QueryFilter, request_body),
|
|
675
|
+
stdx.bytes_as_slice(.exact, tb.Account, reply_body),
|
|
676
|
+
),
|
|
677
|
+
inline .query_transfers,
|
|
678
|
+
.deprecated_query_transfers_unbatched,
|
|
679
|
+
=> |operation_comptime| self.on_query(
|
|
680
|
+
operation_comptime,
|
|
681
|
+
timestamp,
|
|
682
|
+
stdx.bytes_as_slice(.exact, tb.QueryFilter, request_body),
|
|
683
|
+
stdx.bytes_as_slice(.exact, tb.Transfer, reply_body),
|
|
684
|
+
),
|
|
685
|
+
.get_change_events => self.on_get_change_events(
|
|
686
|
+
timestamp,
|
|
687
|
+
stdx.bytes_as_slice(.exact, tb.ChangeEventsFilter, request_body),
|
|
688
|
+
stdx.bytes_as_slice(.exact, tb.ChangeEvent, reply_body),
|
|
689
|
+
),
|
|
690
|
+
//Not handled by the client.
|
|
691
|
+
.pulse => unreachable,
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/// `on_pulse` is called for pulse operations in commit order.
|
|
696
|
+
pub fn on_pulse(
|
|
697
|
+
self: *Workload,
|
|
698
|
+
operation: Operation,
|
|
699
|
+
timestamp: u64,
|
|
700
|
+
) void {
|
|
701
|
+
assert(timestamp != 0);
|
|
702
|
+
assert(operation == .pulse);
|
|
703
|
+
|
|
704
|
+
self.auditor.expire_pending_transfers(timestamp);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
fn build_create_accounts(
|
|
708
|
+
self: *Workload,
|
|
709
|
+
client_index: usize,
|
|
710
|
+
accounts: []tb.Account,
|
|
711
|
+
) usize {
|
|
712
|
+
const results = self.auditor.expect_create_accounts(client_index);
|
|
713
|
+
for (accounts, 0..) |*account, i| {
|
|
714
|
+
const account_index = self.prng.index(self.auditor.accounts);
|
|
715
|
+
account.* = self.auditor.accounts[account_index];
|
|
716
|
+
account.debits_pending = 0;
|
|
717
|
+
account.debits_posted = 0;
|
|
718
|
+
account.credits_pending = 0;
|
|
719
|
+
account.credits_posted = 0;
|
|
720
|
+
account.timestamp = 0;
|
|
721
|
+
results[i] = accounting_auditor.CreateAccountResultSet{};
|
|
722
|
+
|
|
723
|
+
if (self.prng.chance(self.options.create_account_invalid_probability)) {
|
|
724
|
+
account.ledger = 0;
|
|
725
|
+
// The result depends on whether the id already exists:
|
|
726
|
+
results[i].insert(.exists_with_different_ledger);
|
|
727
|
+
results[i].insert(.ledger_must_not_be_zero);
|
|
728
|
+
} else {
|
|
729
|
+
if (!self.auditor.accounts_state[account_index].created) {
|
|
730
|
+
results[i].insert(.ok);
|
|
731
|
+
}
|
|
732
|
+
// Even if the account doesn't exist yet, we may race another request.
|
|
733
|
+
results[i].insert(.exists);
|
|
734
|
+
}
|
|
735
|
+
assert(results[i].count() > 0);
|
|
736
|
+
}
|
|
737
|
+
return accounts.len;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
fn build_create_transfers(
|
|
741
|
+
self: *Workload,
|
|
742
|
+
client_index: usize,
|
|
743
|
+
transfers: []tb.Transfer,
|
|
744
|
+
) usize {
|
|
745
|
+
const results = self.auditor.expect_create_transfers(client_index);
|
|
746
|
+
assert(results.len >= transfers.len);
|
|
747
|
+
var transfers_count: usize = transfers.len;
|
|
748
|
+
var i: usize = 0;
|
|
749
|
+
while (i < transfers_count) {
|
|
750
|
+
const transfer_index = self.transfers_sent;
|
|
751
|
+
const transfer_plan = self.transfer_index_to_plan(transfer_index);
|
|
752
|
+
const transfer_id = self.transfer_index_to_id(transfer_index);
|
|
753
|
+
results[i] = self.build_transfer(
|
|
754
|
+
transfer_id,
|
|
755
|
+
transfer_plan,
|
|
756
|
+
&transfers[i],
|
|
757
|
+
) orelse {
|
|
758
|
+
// This transfer index can't be built; stop with what we have so far.
|
|
759
|
+
// Hopefully it will be unblocked before the next `create_transfers`.
|
|
760
|
+
transfers_count = i;
|
|
761
|
+
break;
|
|
762
|
+
};
|
|
763
|
+
|
|
764
|
+
if (i != 0 and results[i].count() == 1 and results[i - 1].count() == 1) {
|
|
765
|
+
// To support random `lookup_transfers`, linked transfers can't be planned.
|
|
766
|
+
// Instead, link transfers opportunistically, when consecutive transfers can be
|
|
767
|
+
// linked without altering any of their outcomes.
|
|
768
|
+
|
|
769
|
+
if (results[i].contains(.ok) and results[i - 1].contains(.ok) and
|
|
770
|
+
self.prng.chance(self.options.linked_valid_probability))
|
|
771
|
+
{
|
|
772
|
+
transfers[i - 1].flags.linked = true;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
if (!results[i].contains(.ok) and !results[i - 1].contains(.ok) and
|
|
776
|
+
self.prng.chance(self.options.linked_invalid_probability))
|
|
777
|
+
{
|
|
778
|
+
// Convert the previous transfer to a single-phase no-limit transfer, but
|
|
779
|
+
// link it to the current transfer — it will still fail.
|
|
780
|
+
const result_set_opt = self.build_transfer(transfers[i - 1].id, .{
|
|
781
|
+
.valid = true,
|
|
782
|
+
.limit = false,
|
|
783
|
+
.method = .single_phase,
|
|
784
|
+
}, &transfers[i - 1]);
|
|
785
|
+
if (result_set_opt) |result_set| {
|
|
786
|
+
assert(result_set.count() == 1);
|
|
787
|
+
assert(result_set.contains(.ok));
|
|
788
|
+
|
|
789
|
+
transfers[i - 1].flags.linked = true;
|
|
790
|
+
results[i - 1] = accounting_auditor.CreateTransferResultSet.init(.{
|
|
791
|
+
.linked_event_failed = true,
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
assert(results[i].count() > 0);
|
|
797
|
+
|
|
798
|
+
if (transfers[i].flags.pending) self.transfers_pending_in_flight += 1;
|
|
799
|
+
i += 1;
|
|
800
|
+
self.transfers_sent += 1;
|
|
801
|
+
}
|
|
802
|
+
assert(transfers_count == i);
|
|
803
|
+
assert(transfers_count <= transfers.len);
|
|
804
|
+
|
|
805
|
+
self.build_retry_transfers(transfers[0..transfers_count], results);
|
|
806
|
+
|
|
807
|
+
// Checksum transfers only after the whole batch is ready.
|
|
808
|
+
// The opportunistic linking backtracks to modify transfers.
|
|
809
|
+
for (transfers[0..transfers_count]) |*transfer| {
|
|
810
|
+
transfer.user_data_128 = vsr.checksum(std.mem.asBytes(transfer));
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
return transfers_count;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
fn build_retry_transfers(
|
|
817
|
+
self: *Workload,
|
|
818
|
+
transfers: []tb.Transfer,
|
|
819
|
+
results: []accounting_auditor.CreateTransferResultSet,
|
|
820
|
+
) void {
|
|
821
|
+
assert(results.len >= transfers.len);
|
|
822
|
+
|
|
823
|
+
// Neither the first nor the last id can regress to preserve the
|
|
824
|
+
// `transfers_delivered_recently` and `transfers_delivered_past` logic.
|
|
825
|
+
// So we must insert retries in the middle of the batch.
|
|
826
|
+
if (transfers.len <= 1) return;
|
|
827
|
+
for (1..transfers.len - 1) |i| {
|
|
828
|
+
if (self.transfers_retry_exists.items.len == 0 and
|
|
829
|
+
self.transfers_retry_failed.count() == 0) break;
|
|
830
|
+
|
|
831
|
+
// To support random `lookup_transfers`, we replace the transfer with a retry,
|
|
832
|
+
// without altering the outcome for this specific `transfer_index`.
|
|
833
|
+
const transfer_index = self.transfer_id_to_index(transfers[i].id);
|
|
834
|
+
const transfer_plan = self.transfer_index_to_plan(transfer_index);
|
|
835
|
+
const can_retry = !transfer_plan.valid and
|
|
836
|
+
!transfers[i].flags.linked and
|
|
837
|
+
!transfers[i - 1].flags.linked;
|
|
838
|
+
if (can_retry and
|
|
839
|
+
self.prng.chance(self.options.create_transfer_retry_probability))
|
|
840
|
+
{
|
|
841
|
+
switch (self.prng.chances(.{
|
|
842
|
+
.exists = @intFromBool(self.transfers_retry_exists.items.len > 0),
|
|
843
|
+
.failed = @intFromBool(self.transfers_retry_failed.count() > 0),
|
|
844
|
+
})) {
|
|
845
|
+
.exists => {
|
|
846
|
+
// Retry a successfully completed transfer, result == `exists`.
|
|
847
|
+
const index = self.prng.index(self.transfers_retry_exists.items);
|
|
848
|
+
transfers[i] = self.transfers_retry_exists.swapRemove(index);
|
|
849
|
+
results[i] = .initOne(.exists);
|
|
850
|
+
},
|
|
851
|
+
.failed => {
|
|
852
|
+
// Retry a failed transfer ID, result == `id_already_failed`.
|
|
853
|
+
const index = self.prng.index(self.transfers_retry_failed.keys());
|
|
854
|
+
const id_failed = self.transfers_retry_failed.keys()[index];
|
|
855
|
+
self.transfers_retry_failed.swapRemoveAt(index);
|
|
856
|
+
transfers[i] = std.mem.zeroInit(tb.Transfer, .{ .id = id_failed });
|
|
857
|
+
results[i] = .initOne(.id_already_failed);
|
|
858
|
+
},
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
fn build_lookup_accounts(self: *Workload, lookup_ids: []u128) usize {
|
|
865
|
+
for (lookup_ids) |*id| {
|
|
866
|
+
if (self.prng.chance(self.options.lookup_account_invalid_probability)) {
|
|
867
|
+
// Pick an account with valid index (rather than "random.int(u128)") because the
|
|
868
|
+
// Auditor must decode the id to check for a matching account.
|
|
869
|
+
id.* = self.auditor.account_index_to_id(self.prng.int(usize));
|
|
870
|
+
} else {
|
|
871
|
+
const account_index = self.prng.index(self.auditor.accounts);
|
|
872
|
+
id.* = self.auditor.accounts[account_index].id;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
return lookup_ids.len;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
fn build_lookup_transfers(self: *const Workload, lookup_ids: []u128) usize {
|
|
879
|
+
const delivered = self.transfers_delivered_past;
|
|
880
|
+
const lookup_window = self.prng.enum_weighted(Lookup, self.options.lookup_transfer);
|
|
881
|
+
const lookup_window_start = switch (lookup_window) {
|
|
882
|
+
.delivered => self.prng.int_inclusive(usize, delivered),
|
|
883
|
+
.sending => self.prng.range_inclusive(
|
|
884
|
+
usize,
|
|
885
|
+
delivered,
|
|
886
|
+
self.transfers_sent,
|
|
887
|
+
),
|
|
888
|
+
};
|
|
889
|
+
|
|
890
|
+
// +1 to make the span-max inclusive.
|
|
891
|
+
const lookup_window_size = @min(
|
|
892
|
+
fuzz.random_int_exponential(
|
|
893
|
+
self.prng,
|
|
894
|
+
usize,
|
|
895
|
+
self.options.lookup_transfer_span_mean,
|
|
896
|
+
),
|
|
897
|
+
self.transfers_sent - lookup_window_start,
|
|
898
|
+
);
|
|
899
|
+
if (lookup_window_size == 0) return 0;
|
|
900
|
+
|
|
901
|
+
for (lookup_ids) |*lookup_id| {
|
|
902
|
+
lookup_id.* = self.transfer_index_to_id(
|
|
903
|
+
lookup_window_start + self.prng.int_inclusive(usize, lookup_window_size - 1),
|
|
904
|
+
);
|
|
905
|
+
}
|
|
906
|
+
return lookup_ids.len;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
fn build_get_account_filter(
|
|
910
|
+
self: *const Workload,
|
|
911
|
+
client_index: usize,
|
|
912
|
+
comptime action: Action,
|
|
913
|
+
body: []tb.AccountFilter,
|
|
914
|
+
) usize {
|
|
915
|
+
_ = client_index;
|
|
916
|
+
comptime assert(action == .get_account_transfers or
|
|
917
|
+
action == .get_account_balances or
|
|
918
|
+
action == .deprecated_get_account_transfers_unbatched or
|
|
919
|
+
action == .deprecated_get_account_balances_unbatched);
|
|
920
|
+
assert(body.len == 1);
|
|
921
|
+
const account_filter = &body[0];
|
|
922
|
+
account_filter.* = tb.AccountFilter{
|
|
923
|
+
.account_id = 0,
|
|
924
|
+
.user_data_128 = 0,
|
|
925
|
+
.user_data_64 = 0,
|
|
926
|
+
.user_data_32 = 0,
|
|
927
|
+
.code = 0,
|
|
928
|
+
.limit = 0,
|
|
929
|
+
.flags = .{
|
|
930
|
+
.credits = false,
|
|
931
|
+
.debits = false,
|
|
932
|
+
.reversed = false,
|
|
933
|
+
},
|
|
934
|
+
.timestamp_min = 0,
|
|
935
|
+
.timestamp_max = 0,
|
|
936
|
+
};
|
|
937
|
+
|
|
938
|
+
account_filter.account_id = if (self.auditor.pick_account(.{
|
|
939
|
+
.created = null,
|
|
940
|
+
.debits_must_not_exceed_credits = null,
|
|
941
|
+
.credits_must_not_exceed_debits = null,
|
|
942
|
+
})) |account| account.id else
|
|
943
|
+
// Pick an account with valid index (rather than "random.int(u128)") because the
|
|
944
|
+
// Auditor must decode the id to check for a matching account.
|
|
945
|
+
self.auditor.account_index_to_id(self.prng.int(usize));
|
|
946
|
+
|
|
947
|
+
// It may be an invalid account.
|
|
948
|
+
const account_state: ?*const Auditor.AccountState = self.auditor.get_account_state(
|
|
949
|
+
account_filter.account_id,
|
|
950
|
+
);
|
|
951
|
+
|
|
952
|
+
account_filter.flags.reversed = self.prng.boolean();
|
|
953
|
+
|
|
954
|
+
// The timestamp range is restrictive to the number of transfers inserted at the
|
|
955
|
+
// moment the filter was generated. Only when this filter is in place we can assert
|
|
956
|
+
// the expected result count.
|
|
957
|
+
if (account_state != null and
|
|
958
|
+
self.prng.chance(self.options.account_filter_timestamp_range_probability))
|
|
959
|
+
{
|
|
960
|
+
account_filter.flags.credits = true;
|
|
961
|
+
account_filter.flags.debits = true;
|
|
962
|
+
account_filter.limit = account_state.?.transfers_count(account_filter.flags);
|
|
963
|
+
account_filter.timestamp_min = account_state.?.transfer_timestamp_min;
|
|
964
|
+
account_filter.timestamp_max = account_state.?.transfer_timestamp_max;
|
|
965
|
+
|
|
966
|
+
// Exclude the first or the last result depending on the sort order,
|
|
967
|
+
// if there are more than one single transfer.
|
|
968
|
+
account_filter.timestamp_min += @intFromBool(!account_filter.flags.reversed);
|
|
969
|
+
account_filter.timestamp_max -|= @intFromBool(account_filter.flags.reversed);
|
|
970
|
+
} else {
|
|
971
|
+
switch (self.prng.enum_uniform(enum { none, debits, credits, all })) {
|
|
972
|
+
.none => {}, // Testing invalid flags.
|
|
973
|
+
.debits => account_filter.flags.debits = true,
|
|
974
|
+
.credits => account_filter.flags.credits = true,
|
|
975
|
+
.all => {
|
|
976
|
+
account_filter.flags.debits = true;
|
|
977
|
+
account_filter.flags.credits = true;
|
|
978
|
+
},
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
const operation = comptime std.enums.nameCast(Operation, action);
|
|
982
|
+
const batch_result_max = operation.result_max(self.options.batch_size_limit);
|
|
983
|
+
account_filter.limit = switch (self.prng.enum_uniform(enum {
|
|
984
|
+
zero,
|
|
985
|
+
one,
|
|
986
|
+
random,
|
|
987
|
+
batch_max,
|
|
988
|
+
int_max,
|
|
989
|
+
})) {
|
|
990
|
+
.zero => 0,
|
|
991
|
+
.one => 1,
|
|
992
|
+
.random => self.prng.int_inclusive(u32, batch_result_max),
|
|
993
|
+
.batch_max => batch_result_max,
|
|
994
|
+
.int_max => std.math.maxInt(u32),
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
return 1;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
fn build_query_filter(
|
|
1002
|
+
self: *const Workload,
|
|
1003
|
+
client_index: usize,
|
|
1004
|
+
comptime action: Action,
|
|
1005
|
+
body: []tb.QueryFilter,
|
|
1006
|
+
) usize {
|
|
1007
|
+
_ = client_index;
|
|
1008
|
+
comptime assert(action == .query_accounts or
|
|
1009
|
+
action == .query_transfers or
|
|
1010
|
+
action == .deprecated_query_accounts_unbatched or
|
|
1011
|
+
action == .deprecated_query_transfers_unbatched);
|
|
1012
|
+
assert(body.len == 1);
|
|
1013
|
+
const query_filter = &body[0];
|
|
1014
|
+
|
|
1015
|
+
const operation = comptime std.enums.nameCast(Operation, action);
|
|
1016
|
+
const batch_result_max = operation.result_max(self.options.batch_size_limit);
|
|
1017
|
+
const limit: u32 = switch (self.prng.enum_uniform(enum {
|
|
1018
|
+
zero,
|
|
1019
|
+
one,
|
|
1020
|
+
random,
|
|
1021
|
+
batch_max,
|
|
1022
|
+
int_max,
|
|
1023
|
+
})) {
|
|
1024
|
+
.zero => 0,
|
|
1025
|
+
.one => 1,
|
|
1026
|
+
.random => self.prng.int_inclusive(u32, batch_result_max),
|
|
1027
|
+
.batch_max => batch_result_max,
|
|
1028
|
+
.int_max => std.math.maxInt(u32),
|
|
1029
|
+
};
|
|
1030
|
+
|
|
1031
|
+
if (self.prng.chance(self.options.query_filter_not_found_probability)) {
|
|
1032
|
+
query_filter.* = .{
|
|
1033
|
+
.user_data_128 = 0,
|
|
1034
|
+
.user_data_64 = 0,
|
|
1035
|
+
.user_data_32 = 0,
|
|
1036
|
+
.code = 0,
|
|
1037
|
+
.ledger = 999, // Non-existent ledger
|
|
1038
|
+
.limit = limit,
|
|
1039
|
+
.flags = .{
|
|
1040
|
+
.reversed = false,
|
|
1041
|
+
},
|
|
1042
|
+
.timestamp_min = 0,
|
|
1043
|
+
.timestamp_max = 0,
|
|
1044
|
+
};
|
|
1045
|
+
} else {
|
|
1046
|
+
const query_intersection_index = self.prng.index(self.auditor.query_intersections);
|
|
1047
|
+
const query_intersection =
|
|
1048
|
+
self.auditor.query_intersections[query_intersection_index];
|
|
1049
|
+
|
|
1050
|
+
query_filter.* = .{
|
|
1051
|
+
.user_data_128 = 0,
|
|
1052
|
+
.user_data_64 = query_intersection.user_data_64,
|
|
1053
|
+
.user_data_32 = query_intersection.user_data_32,
|
|
1054
|
+
.code = query_intersection.code,
|
|
1055
|
+
.ledger = 0,
|
|
1056
|
+
.limit = limit,
|
|
1057
|
+
.flags = .{
|
|
1058
|
+
.reversed = self.prng.boolean(),
|
|
1059
|
+
},
|
|
1060
|
+
.timestamp_min = 0,
|
|
1061
|
+
.timestamp_max = 0,
|
|
1062
|
+
};
|
|
1063
|
+
|
|
1064
|
+
// Maybe filter by timestamp:
|
|
1065
|
+
const state = switch (action) {
|
|
1066
|
+
.query_accounts,
|
|
1067
|
+
.deprecated_query_accounts_unbatched,
|
|
1068
|
+
=> &query_intersection.accounts,
|
|
1069
|
+
.query_transfers,
|
|
1070
|
+
.deprecated_query_transfers_unbatched,
|
|
1071
|
+
=> &query_intersection.transfers,
|
|
1072
|
+
else => unreachable,
|
|
1073
|
+
};
|
|
1074
|
+
|
|
1075
|
+
if (state.count > 1 and state.count <= batch_result_max and
|
|
1076
|
+
self.prng.chance(self.options.query_filter_timestamp_range_probability))
|
|
1077
|
+
{
|
|
1078
|
+
// Excluding the first or last object:
|
|
1079
|
+
if (query_filter.flags.reversed) {
|
|
1080
|
+
query_filter.timestamp_min = state.timestamp_min;
|
|
1081
|
+
query_filter.timestamp_max = state.timestamp_max - 1;
|
|
1082
|
+
} else {
|
|
1083
|
+
query_filter.timestamp_min = state.timestamp_min + 1;
|
|
1084
|
+
query_filter.timestamp_max = state.timestamp_max;
|
|
1085
|
+
}
|
|
1086
|
+
// Later we can assert that results.len == count - 1:
|
|
1087
|
+
query_filter.limit = state.count;
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
return 1;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
fn build_get_change_events_filter(
|
|
1095
|
+
self: *Workload,
|
|
1096
|
+
client_index: usize,
|
|
1097
|
+
body: []tb.ChangeEventsFilter,
|
|
1098
|
+
) usize {
|
|
1099
|
+
_ = client_index;
|
|
1100
|
+
assert(body.len == 1);
|
|
1101
|
+
const filter = &body[0];
|
|
1102
|
+
|
|
1103
|
+
const snapshot = self.auditor.changes_tracker.acquire_snapshot() orelse {
|
|
1104
|
+
// We can only track a limited set of events,
|
|
1105
|
+
// so we issue a query with an invalid filter when the results can't be asserted.
|
|
1106
|
+
filter.* = switch (self.prng.enum_uniform(enum {
|
|
1107
|
+
zeroed,
|
|
1108
|
+
invalid_timestamps,
|
|
1109
|
+
})) {
|
|
1110
|
+
.zeroed => .{
|
|
1111
|
+
.limit = 0,
|
|
1112
|
+
.timestamp_min = 0,
|
|
1113
|
+
.timestamp_max = 0,
|
|
1114
|
+
},
|
|
1115
|
+
.invalid_timestamps => filter: {
|
|
1116
|
+
const timestamp: u64 = self.prng.range_inclusive(
|
|
1117
|
+
u64,
|
|
1118
|
+
TimestampRange.timestamp_min,
|
|
1119
|
+
TimestampRange.timestamp_max,
|
|
1120
|
+
);
|
|
1121
|
+
break :filter .{
|
|
1122
|
+
.limit = self.prng.int(u32),
|
|
1123
|
+
.timestamp_min = timestamp + 1,
|
|
1124
|
+
.timestamp_max = timestamp,
|
|
1125
|
+
};
|
|
1126
|
+
},
|
|
1127
|
+
};
|
|
1128
|
+
return 1;
|
|
1129
|
+
};
|
|
1130
|
+
assert(snapshot.count_total() > 0);
|
|
1131
|
+
|
|
1132
|
+
const limit: u32 = switch (self.prng.enum_uniform(enum {
|
|
1133
|
+
exact,
|
|
1134
|
+
batch_max,
|
|
1135
|
+
int_max,
|
|
1136
|
+
})) {
|
|
1137
|
+
.exact => snapshot.count_total(),
|
|
1138
|
+
.batch_max => Operation.get_change_events.result_max(
|
|
1139
|
+
self.options.batch_size_limit,
|
|
1140
|
+
),
|
|
1141
|
+
.int_max => std.math.maxInt(u32),
|
|
1142
|
+
};
|
|
1143
|
+
filter.* = .{
|
|
1144
|
+
.limit = limit,
|
|
1145
|
+
.timestamp_min = snapshot.timestamp_min,
|
|
1146
|
+
.timestamp_max = snapshot.timestamp_max,
|
|
1147
|
+
};
|
|
1148
|
+
return 1;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
/// The transfer built is guaranteed to match the TransferPlan's outcome.
|
|
1152
|
+
/// The transfer built is _not_ guaranteed to match the TransferPlan's method.
|
|
1153
|
+
///
|
|
1154
|
+
/// Returns `null` if the transfer plan cannot be fulfilled (because there aren't enough
|
|
1155
|
+
/// accounts created).
|
|
1156
|
+
fn build_transfer(
|
|
1157
|
+
self: *Workload,
|
|
1158
|
+
transfer_id: u128,
|
|
1159
|
+
transfer_plan: TransferPlan,
|
|
1160
|
+
transfer: *tb.Transfer,
|
|
1161
|
+
) ?accounting_auditor.CreateTransferResultSet {
|
|
1162
|
+
// If the specified method is unavailable, swap it.
|
|
1163
|
+
// Changing the method may narrow the TransferOutcome (unknown→success, unknown→failure)
|
|
1164
|
+
// but never broaden it (success→unknown, success→failure).
|
|
1165
|
+
const method = method: {
|
|
1166
|
+
const default = transfer_plan.method;
|
|
1167
|
+
if (default == .pending and
|
|
1168
|
+
self.auditor.pending_expiries.count() + self.transfers_pending_in_flight ==
|
|
1169
|
+
self.auditor.options.transfers_pending_max)
|
|
1170
|
+
{
|
|
1171
|
+
break :method .single_phase;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
if (default == .post_pending or default == .void_pending) {
|
|
1175
|
+
if (self.auditor.pending_transfers.count() == 0) {
|
|
1176
|
+
break :method .single_phase;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
break :method default;
|
|
1180
|
+
};
|
|
1181
|
+
|
|
1182
|
+
const index_valid = @intFromBool(transfer_plan.valid);
|
|
1183
|
+
const index_limit = @intFromBool(transfer_plan.limit);
|
|
1184
|
+
const index_method = @intFromEnum(method);
|
|
1185
|
+
const transfer_template = &transfer_templates[index_valid][index_limit][index_method];
|
|
1186
|
+
|
|
1187
|
+
const limit_debits = transfer_plan.limit and self.prng.boolean();
|
|
1188
|
+
const limit_credits = transfer_plan.limit and (self.prng.boolean() or !limit_debits);
|
|
1189
|
+
assert(transfer_plan.limit == (limit_debits or limit_credits));
|
|
1190
|
+
|
|
1191
|
+
const debit_account = self.auditor.pick_account(.{
|
|
1192
|
+
.created = true,
|
|
1193
|
+
.debits_must_not_exceed_credits = limit_debits,
|
|
1194
|
+
.credits_must_not_exceed_debits = null,
|
|
1195
|
+
}) orelse return null;
|
|
1196
|
+
assert(!limit_debits or debit_account.flags.debits_must_not_exceed_credits);
|
|
1197
|
+
|
|
1198
|
+
const credit_account = self.auditor.pick_account(.{
|
|
1199
|
+
.created = true,
|
|
1200
|
+
.debits_must_not_exceed_credits = null,
|
|
1201
|
+
.credits_must_not_exceed_debits = limit_credits,
|
|
1202
|
+
.exclude = debit_account.id,
|
|
1203
|
+
}) orelse return null;
|
|
1204
|
+
assert(!limit_credits or credit_account.flags.credits_must_not_exceed_debits);
|
|
1205
|
+
|
|
1206
|
+
const query_intersection_index = self.prng.index(
|
|
1207
|
+
self.auditor.query_intersections,
|
|
1208
|
+
);
|
|
1209
|
+
const query_intersection = self.auditor.query_intersections[query_intersection_index];
|
|
1210
|
+
|
|
1211
|
+
transfer.* = .{
|
|
1212
|
+
.id = transfer_id,
|
|
1213
|
+
.debit_account_id = debit_account.id,
|
|
1214
|
+
.credit_account_id = credit_account.id,
|
|
1215
|
+
// "user_data_128" will be set to a checksum of the Transfer.
|
|
1216
|
+
.user_data_128 = 0,
|
|
1217
|
+
.user_data_64 = query_intersection.user_data_64,
|
|
1218
|
+
.user_data_32 = query_intersection.user_data_32,
|
|
1219
|
+
.code = query_intersection.code,
|
|
1220
|
+
.pending_id = 0,
|
|
1221
|
+
.timeout = 0,
|
|
1222
|
+
.ledger = transfer_template.ledger,
|
|
1223
|
+
.flags = .{},
|
|
1224
|
+
.timestamp = 0,
|
|
1225
|
+
.amount = self.prng.int_inclusive(u128, std.math.maxInt(u8)),
|
|
1226
|
+
};
|
|
1227
|
+
|
|
1228
|
+
switch (method) {
|
|
1229
|
+
.single_phase => {},
|
|
1230
|
+
.pending => {
|
|
1231
|
+
transfer.flags = .{ .pending = true };
|
|
1232
|
+
// Bound the timeout to ensure we never hit `overflows_timeout`.
|
|
1233
|
+
transfer.timeout = 1 + @as(u32, @min(
|
|
1234
|
+
std.math.maxInt(u32) / 2,
|
|
1235
|
+
fuzz.random_int_exponential(
|
|
1236
|
+
self.prng,
|
|
1237
|
+
u32,
|
|
1238
|
+
self.options.pending_timeout_mean,
|
|
1239
|
+
),
|
|
1240
|
+
));
|
|
1241
|
+
},
|
|
1242
|
+
.post_pending, .void_pending => {
|
|
1243
|
+
// Don't depend on `HashMap.keyIterator()` being deterministic.
|
|
1244
|
+
// Pick a random "target" key, then post/void the id it is nearest to.
|
|
1245
|
+
const target = self.prng.int(u128);
|
|
1246
|
+
var previous: ?u128 = null;
|
|
1247
|
+
var iterator = self.auditor.pending_transfers.keyIterator();
|
|
1248
|
+
while (iterator.next()) |id| {
|
|
1249
|
+
if (previous == null or
|
|
1250
|
+
@max(target, id.*) - @min(target, id.*) <
|
|
1251
|
+
@max(target, previous.?) - @min(target, previous.?))
|
|
1252
|
+
{
|
|
1253
|
+
previous = id.*;
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
// If there were no pending ids, the method would have been changed.
|
|
1258
|
+
const pending_id = previous.?;
|
|
1259
|
+
const pending_transfer = self.auditor.pending_transfers.getPtr(previous.?).?;
|
|
1260
|
+
const dr = pending_transfer.debit_account_index;
|
|
1261
|
+
const cr = pending_transfer.credit_account_index;
|
|
1262
|
+
const pending_query_intersection = self.auditor
|
|
1263
|
+
.query_intersections[pending_transfer.query_intersection_index];
|
|
1264
|
+
// Don't use the default '0' parameters because the StateMachine overwrites 0s
|
|
1265
|
+
// with the pending transfer's values, invalidating the post/void transfer
|
|
1266
|
+
// checksum.
|
|
1267
|
+
transfer.debit_account_id = self.auditor.account_index_to_id(dr);
|
|
1268
|
+
transfer.credit_account_id = self.auditor.account_index_to_id(cr);
|
|
1269
|
+
transfer.user_data_64 = pending_query_intersection.user_data_64;
|
|
1270
|
+
transfer.user_data_32 = pending_query_intersection.user_data_32;
|
|
1271
|
+
transfer.code = pending_query_intersection.code;
|
|
1272
|
+
if (method == .post_pending) {
|
|
1273
|
+
transfer.amount =
|
|
1274
|
+
self.prng.range_inclusive(u128, 0, pending_transfer.amount);
|
|
1275
|
+
} else {
|
|
1276
|
+
transfer.amount = pending_transfer.amount;
|
|
1277
|
+
}
|
|
1278
|
+
transfer.pending_id = pending_id;
|
|
1279
|
+
transfer.flags = .{
|
|
1280
|
+
.post_pending_transfer = method == .post_pending,
|
|
1281
|
+
.void_pending_transfer = method == .void_pending,
|
|
1282
|
+
};
|
|
1283
|
+
},
|
|
1284
|
+
}
|
|
1285
|
+
assert(transfer_template.result.count() > 0);
|
|
1286
|
+
return transfer_template.result;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
fn batch(
|
|
1290
|
+
self: *const Workload,
|
|
1291
|
+
comptime T: type,
|
|
1292
|
+
comptime action: Action,
|
|
1293
|
+
body: []u8,
|
|
1294
|
+
event_count_remain: u32,
|
|
1295
|
+
) []T {
|
|
1296
|
+
const batch_min = switch (action) {
|
|
1297
|
+
.create_accounts,
|
|
1298
|
+
.lookup_accounts,
|
|
1299
|
+
.deprecated_create_accounts_unbatched,
|
|
1300
|
+
.deprecated_lookup_accounts_unbatched,
|
|
1301
|
+
=> self.options.accounts_batch_size_min,
|
|
1302
|
+
.create_transfers,
|
|
1303
|
+
.lookup_transfers,
|
|
1304
|
+
.deprecated_create_transfers_unbatched,
|
|
1305
|
+
.deprecated_lookup_transfers_unbatched,
|
|
1306
|
+
=> self.options.transfers_batch_size_min,
|
|
1307
|
+
.get_account_transfers,
|
|
1308
|
+
.get_account_balances,
|
|
1309
|
+
.query_accounts,
|
|
1310
|
+
.query_transfers,
|
|
1311
|
+
.deprecated_get_account_transfers_unbatched,
|
|
1312
|
+
.deprecated_get_account_balances_unbatched,
|
|
1313
|
+
.deprecated_query_accounts_unbatched,
|
|
1314
|
+
.deprecated_query_transfers_unbatched,
|
|
1315
|
+
.get_change_events,
|
|
1316
|
+
=> 1,
|
|
1317
|
+
};
|
|
1318
|
+
const batch_span = switch (action) {
|
|
1319
|
+
.create_accounts,
|
|
1320
|
+
.lookup_accounts,
|
|
1321
|
+
.deprecated_create_accounts_unbatched,
|
|
1322
|
+
.deprecated_lookup_accounts_unbatched,
|
|
1323
|
+
=> self.options.accounts_batch_size_span,
|
|
1324
|
+
.create_transfers,
|
|
1325
|
+
.lookup_transfers,
|
|
1326
|
+
.deprecated_create_transfers_unbatched,
|
|
1327
|
+
.deprecated_lookup_transfers_unbatched,
|
|
1328
|
+
=> self.options.transfers_batch_size_span,
|
|
1329
|
+
.get_account_transfers,
|
|
1330
|
+
.get_account_balances,
|
|
1331
|
+
.query_accounts,
|
|
1332
|
+
.query_transfers,
|
|
1333
|
+
.deprecated_get_account_transfers_unbatched,
|
|
1334
|
+
.deprecated_get_account_balances_unbatched,
|
|
1335
|
+
.deprecated_query_accounts_unbatched,
|
|
1336
|
+
.deprecated_query_transfers_unbatched,
|
|
1337
|
+
.get_change_events,
|
|
1338
|
+
=> 0,
|
|
1339
|
+
};
|
|
1340
|
+
|
|
1341
|
+
const slice = stdx.bytes_as_slice(.inexact, T, body);
|
|
1342
|
+
const batch_size = @min(
|
|
1343
|
+
batch_min + self.prng.int_inclusive(usize, batch_span),
|
|
1344
|
+
event_count_remain,
|
|
1345
|
+
);
|
|
1346
|
+
|
|
1347
|
+
return slice[0..batch_size];
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
fn transfer_id_to_index(self: *const Workload, id: u128) usize {
|
|
1351
|
+
// -1 because id=0 is not valid, so index=0→id=1.
|
|
1352
|
+
return @as(usize, @intCast(self.options.transfer_id_permutation.decode(id))) - 1;
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
fn transfer_index_to_id(self: *const Workload, index: usize) u128 {
|
|
1356
|
+
// +1 so that index=0 is encoded as a valid id.
|
|
1357
|
+
return self.options.transfer_id_permutation.encode(index + 1);
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
/// To support `lookup_transfers`, the `TransferPlan` is deterministic based on:
|
|
1361
|
+
/// * `Workload.transfer_plan_seed`, and
|
|
1362
|
+
/// * the transfer `index`.
|
|
1363
|
+
fn transfer_index_to_plan(self: *const Workload, index: usize) TransferPlan {
|
|
1364
|
+
var prng = stdx.PRNG.from_seed(self.transfer_plan_seed ^ @as(u64, index));
|
|
1365
|
+
const method: TransferPlan.Method = blk: {
|
|
1366
|
+
if (prng.chance(self.options.create_transfer_pending_probability)) {
|
|
1367
|
+
break :blk .pending;
|
|
1368
|
+
}
|
|
1369
|
+
if (prng.chance(self.options.create_transfer_post_probability)) {
|
|
1370
|
+
break :blk .post_pending;
|
|
1371
|
+
}
|
|
1372
|
+
if (prng.chance(self.options.create_transfer_void_probability)) {
|
|
1373
|
+
break :blk .void_pending;
|
|
1374
|
+
}
|
|
1375
|
+
break :blk .single_phase;
|
|
1376
|
+
};
|
|
1377
|
+
return .{
|
|
1378
|
+
.valid = !prng.chance(self.options.create_transfer_invalid_probability),
|
|
1379
|
+
.limit = prng.chance(self.options.create_transfer_limit_probability),
|
|
1380
|
+
.method = method,
|
|
1381
|
+
};
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
fn on_create_transfers(
|
|
1385
|
+
self: *Workload,
|
|
1386
|
+
client_index: usize,
|
|
1387
|
+
timestamp: u64,
|
|
1388
|
+
transfers: []const tb.Transfer,
|
|
1389
|
+
results_sparse: []const tb.CreateTransfersResult,
|
|
1390
|
+
) void {
|
|
1391
|
+
self.auditor.on_create_transfers(client_index, timestamp, transfers, results_sparse);
|
|
1392
|
+
if (transfers.len == 0) return;
|
|
1393
|
+
|
|
1394
|
+
const transfer_index_min = self.transfer_id_to_index(transfers[0].id);
|
|
1395
|
+
const transfer_index_max = self.transfer_id_to_index(transfers[transfers.len - 1].id);
|
|
1396
|
+
assert(transfer_index_min <= transfer_index_max);
|
|
1397
|
+
|
|
1398
|
+
self.transfers_delivered_recently.add(.{
|
|
1399
|
+
.min = transfer_index_min,
|
|
1400
|
+
.max = transfer_index_max,
|
|
1401
|
+
}) catch unreachable;
|
|
1402
|
+
|
|
1403
|
+
while (self.transfers_delivered_recently.peek()) |delivered| {
|
|
1404
|
+
if (self.transfers_delivered_past == delivered.min) {
|
|
1405
|
+
self.transfers_delivered_past = delivered.max + 1;
|
|
1406
|
+
_ = self.transfers_delivered_recently.remove();
|
|
1407
|
+
} else {
|
|
1408
|
+
assert(self.transfers_delivered_past < delivered.min);
|
|
1409
|
+
break;
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
const CreateTransfersResultIterator = IteratorForCreateType(tb.CreateTransfersResult);
|
|
1414
|
+
var results_iterator: CreateTransfersResultIterator = .init(results_sparse);
|
|
1415
|
+
for (transfers, 0..) |*transfer, i| {
|
|
1416
|
+
const result: tb.CreateTransferResult = results_iterator.take(i) orelse .ok;
|
|
1417
|
+
if (transfer.flags.pending and result != .exists) {
|
|
1418
|
+
self.transfers_pending_in_flight -= 1;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
// Add some successfully completed transfers to be retried in the next request.
|
|
1422
|
+
if (result == .ok and !transfer.flags.linked and
|
|
1423
|
+
self.transfers_retry_exists.items.len <
|
|
1424
|
+
self.options.transfers_retry_exists_max and
|
|
1425
|
+
self.prng.chance(self.options.create_transfer_retry_probability))
|
|
1426
|
+
{
|
|
1427
|
+
var transfer_exists = transfer.*;
|
|
1428
|
+
assert(transfer_exists.timestamp == 0);
|
|
1429
|
+
assert(transfer_exists.user_data_128 != 0);
|
|
1430
|
+
|
|
1431
|
+
transfer_exists.user_data_128 = 0; // This will be replaced by the checksum.
|
|
1432
|
+
self.transfers_retry_exists.appendAssumeCapacity(transfer_exists);
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
// Enqueue the `id`s of transient errors to be retried in the next request.
|
|
1436
|
+
if (result != .ok and result.transient() and
|
|
1437
|
+
self.transfers_retry_failed.count() <
|
|
1438
|
+
self.options.transfers_retry_failed_max)
|
|
1439
|
+
{
|
|
1440
|
+
self.transfers_retry_failed.putAssumeCapacityNoClobber(
|
|
1441
|
+
transfer.id,
|
|
1442
|
+
{},
|
|
1443
|
+
);
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
fn on_lookup_transfers(
|
|
1449
|
+
self: *Workload,
|
|
1450
|
+
client_index: usize,
|
|
1451
|
+
timestamp: u64,
|
|
1452
|
+
ids: []const u128,
|
|
1453
|
+
results: []const tb.Transfer,
|
|
1454
|
+
) void {
|
|
1455
|
+
self.auditor.on_lookup_transfers(client_index, timestamp, ids, results);
|
|
1456
|
+
|
|
1457
|
+
var transfers = accounting_auditor.IteratorForLookupType(tb.Transfer).init(results);
|
|
1458
|
+
for (ids) |transfer_id| {
|
|
1459
|
+
const transfer_index = self.transfer_id_to_index(transfer_id);
|
|
1460
|
+
const transfer_outcome = self.transfer_index_to_plan(transfer_index).outcome();
|
|
1461
|
+
const result = transfers.take(transfer_id);
|
|
1462
|
+
|
|
1463
|
+
if (result) |transfer| validate_transfer_checksum(transfer);
|
|
1464
|
+
|
|
1465
|
+
if (transfer_index >= self.transfers_sent) {
|
|
1466
|
+
// This transfer hasn't been created yet.
|
|
1467
|
+
assert(result == null);
|
|
1468
|
+
continue;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
switch (transfer_outcome) {
|
|
1472
|
+
.success => {
|
|
1473
|
+
if (transfer_index < self.transfers_delivered_past) {
|
|
1474
|
+
// The transfer was delivered; it must exist.
|
|
1475
|
+
assert(result != null);
|
|
1476
|
+
} else {
|
|
1477
|
+
var it = self.transfers_delivered_recently.iterator();
|
|
1478
|
+
while (it.next()) |delivered| {
|
|
1479
|
+
if (transfer_index >= delivered.min and
|
|
1480
|
+
transfer_index <= delivered.max)
|
|
1481
|
+
{
|
|
1482
|
+
// The transfer was delivered recently; it must exist.
|
|
1483
|
+
assert(result != null);
|
|
1484
|
+
break;
|
|
1485
|
+
}
|
|
1486
|
+
} else {
|
|
1487
|
+
// The `create_transfers` has not committed (it may be in-flight).
|
|
1488
|
+
assert(result == null);
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
},
|
|
1492
|
+
// An invalid transfer is never persisted.
|
|
1493
|
+
.failure => assert(result == null),
|
|
1494
|
+
// Due to races and timeouts, these transfer types may not succeed.
|
|
1495
|
+
.unknown => {},
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
fn on_get_account_transfers(
|
|
1501
|
+
self: *Workload,
|
|
1502
|
+
comptime operation: Operation,
|
|
1503
|
+
timestamp: u64,
|
|
1504
|
+
body: []const tb.AccountFilter,
|
|
1505
|
+
results: []const tb.Transfer,
|
|
1506
|
+
) void {
|
|
1507
|
+
_ = timestamp;
|
|
1508
|
+
comptime assert(operation == .get_account_transfers or
|
|
1509
|
+
operation == .deprecated_get_account_transfers_unbatched);
|
|
1510
|
+
assert(body.len == 1);
|
|
1511
|
+
|
|
1512
|
+
const batch_result_max = operation.result_max(self.options.batch_size_limit);
|
|
1513
|
+
const account_filter = &body[0];
|
|
1514
|
+
assert(results.len <= account_filter.limit);
|
|
1515
|
+
assert(results.len <= batch_result_max);
|
|
1516
|
+
|
|
1517
|
+
const account_state = self.auditor.get_account_state(
|
|
1518
|
+
account_filter.account_id,
|
|
1519
|
+
) orelse {
|
|
1520
|
+
// Invalid account id.
|
|
1521
|
+
assert(results.len == 0);
|
|
1522
|
+
return;
|
|
1523
|
+
};
|
|
1524
|
+
|
|
1525
|
+
const filter_valid = account_state.created and
|
|
1526
|
+
(account_filter.flags.credits or account_filter.flags.debits) and
|
|
1527
|
+
account_filter.limit > 0 and
|
|
1528
|
+
account_filter.timestamp_min <= account_filter.timestamp_max;
|
|
1529
|
+
if (!filter_valid) {
|
|
1530
|
+
// Invalid filter.
|
|
1531
|
+
assert(results.len == 0);
|
|
1532
|
+
return;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
self.validate_account_filter_result_count(
|
|
1536
|
+
operation,
|
|
1537
|
+
account_state,
|
|
1538
|
+
account_filter,
|
|
1539
|
+
results.len,
|
|
1540
|
+
);
|
|
1541
|
+
|
|
1542
|
+
var timestamp_previous: u64 = if (account_filter.flags.reversed)
|
|
1543
|
+
account_state.transfer_timestamp_max +| 1
|
|
1544
|
+
else
|
|
1545
|
+
account_state.transfer_timestamp_min -| 1;
|
|
1546
|
+
|
|
1547
|
+
for (results) |*transfer| {
|
|
1548
|
+
if (account_filter.flags.reversed) {
|
|
1549
|
+
assert(transfer.timestamp < timestamp_previous);
|
|
1550
|
+
} else {
|
|
1551
|
+
assert(transfer.timestamp > timestamp_previous);
|
|
1552
|
+
}
|
|
1553
|
+
timestamp_previous = transfer.timestamp;
|
|
1554
|
+
|
|
1555
|
+
assert(account_filter.timestamp_min == 0 or
|
|
1556
|
+
transfer.timestamp >= account_filter.timestamp_min);
|
|
1557
|
+
assert(account_filter.timestamp_max == 0 or
|
|
1558
|
+
transfer.timestamp <= account_filter.timestamp_max);
|
|
1559
|
+
|
|
1560
|
+
validate_transfer_checksum(transfer);
|
|
1561
|
+
|
|
1562
|
+
const transfer_index = self.transfer_id_to_index(transfer.id);
|
|
1563
|
+
assert(transfer_index < self.transfers_sent);
|
|
1564
|
+
|
|
1565
|
+
const transfer_plan = self.transfer_index_to_plan(transfer_index);
|
|
1566
|
+
assert(transfer_plan.valid);
|
|
1567
|
+
assert(transfer_plan.outcome() != .failure);
|
|
1568
|
+
if (transfer.flags.pending) assert(transfer_plan.method == .pending);
|
|
1569
|
+
if (transfer.flags.post_pending_transfer) {
|
|
1570
|
+
assert(transfer_plan.method == .post_pending);
|
|
1571
|
+
}
|
|
1572
|
+
if (transfer.flags.void_pending_transfer) {
|
|
1573
|
+
assert(transfer_plan.method == .void_pending);
|
|
1574
|
+
}
|
|
1575
|
+
if (transfer_plan.method == .single_phase) assert(!transfer.flags.pending and
|
|
1576
|
+
!transfer.flags.post_pending_transfer and
|
|
1577
|
+
!transfer.flags.void_pending_transfer);
|
|
1578
|
+
|
|
1579
|
+
assert(transfer.debit_account_id == account_filter.account_id or
|
|
1580
|
+
transfer.credit_account_id == account_filter.account_id);
|
|
1581
|
+
assert(account_filter.flags.credits or account_filter.flags.debits);
|
|
1582
|
+
assert(account_filter.flags.credits or
|
|
1583
|
+
transfer.debit_account_id == account_filter.account_id);
|
|
1584
|
+
assert(account_filter.flags.debits or
|
|
1585
|
+
transfer.credit_account_id == account_filter.account_id);
|
|
1586
|
+
|
|
1587
|
+
if (transfer_plan.limit) {
|
|
1588
|
+
// The plan does not guarantee the "limit" flag for posting
|
|
1589
|
+
// or voiding pending transfers.
|
|
1590
|
+
const post_or_void_pending_transfer = transfer.flags.post_pending_transfer or
|
|
1591
|
+
transfer.flags.void_pending_transfer;
|
|
1592
|
+
assert(post_or_void_pending_transfer == (transfer.pending_id != 0));
|
|
1593
|
+
|
|
1594
|
+
const dr_account = self.auditor.get_account(transfer.debit_account_id).?;
|
|
1595
|
+
const cr_account = self.auditor.get_account(transfer.credit_account_id).?;
|
|
1596
|
+
assert(
|
|
1597
|
+
post_or_void_pending_transfer or
|
|
1598
|
+
dr_account.flags.debits_must_not_exceed_credits or
|
|
1599
|
+
cr_account.flags.credits_must_not_exceed_debits,
|
|
1600
|
+
);
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
fn on_get_account_balances(
|
|
1606
|
+
self: *Workload,
|
|
1607
|
+
comptime operation: Operation,
|
|
1608
|
+
timestamp: u64,
|
|
1609
|
+
body: []const tb.AccountFilter,
|
|
1610
|
+
results: []const tb.AccountBalance,
|
|
1611
|
+
) void {
|
|
1612
|
+
_ = timestamp;
|
|
1613
|
+
comptime assert(operation == .get_account_balances or
|
|
1614
|
+
operation == .deprecated_get_account_balances_unbatched);
|
|
1615
|
+
assert(body.len == 1);
|
|
1616
|
+
|
|
1617
|
+
const batch_result_max = operation.result_max(self.options.batch_size_limit);
|
|
1618
|
+
const account_filter = &body[0];
|
|
1619
|
+
assert(results.len <= account_filter.limit);
|
|
1620
|
+
assert(results.len <= batch_result_max);
|
|
1621
|
+
|
|
1622
|
+
const account_state = self.auditor.get_account_state(
|
|
1623
|
+
account_filter.account_id,
|
|
1624
|
+
) orelse {
|
|
1625
|
+
// Invalid account id.
|
|
1626
|
+
assert(results.len == 0);
|
|
1627
|
+
return;
|
|
1628
|
+
};
|
|
1629
|
+
|
|
1630
|
+
const filter_valid = account_state.created and
|
|
1631
|
+
self.auditor.get_account(account_filter.account_id).?.flags.history and
|
|
1632
|
+
(account_filter.flags.credits or account_filter.flags.debits) and
|
|
1633
|
+
account_filter.limit > 0 and
|
|
1634
|
+
account_filter.timestamp_min <= account_filter.timestamp_max;
|
|
1635
|
+
if (!filter_valid) {
|
|
1636
|
+
// Invalid filter.
|
|
1637
|
+
assert(results.len == 0);
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
self.validate_account_filter_result_count(
|
|
1642
|
+
operation,
|
|
1643
|
+
account_state,
|
|
1644
|
+
account_filter,
|
|
1645
|
+
results.len,
|
|
1646
|
+
);
|
|
1647
|
+
|
|
1648
|
+
var timestamp_last: u64 = if (account_filter.flags.reversed)
|
|
1649
|
+
account_state.transfer_timestamp_max +| 1
|
|
1650
|
+
else
|
|
1651
|
+
account_state.transfer_timestamp_min -| 1;
|
|
1652
|
+
|
|
1653
|
+
for (results) |*balance| {
|
|
1654
|
+
assert(if (account_filter.flags.reversed)
|
|
1655
|
+
balance.timestamp < timestamp_last
|
|
1656
|
+
else
|
|
1657
|
+
balance.timestamp > timestamp_last);
|
|
1658
|
+
timestamp_last = balance.timestamp;
|
|
1659
|
+
|
|
1660
|
+
assert(account_filter.timestamp_min == 0 or
|
|
1661
|
+
balance.timestamp >= account_filter.timestamp_min);
|
|
1662
|
+
assert(account_filter.timestamp_max == 0 or
|
|
1663
|
+
balance.timestamp <= account_filter.timestamp_max);
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
fn validate_account_filter_result_count(
|
|
1668
|
+
self: *const Workload,
|
|
1669
|
+
comptime operation: Operation,
|
|
1670
|
+
account_state: *const Auditor.AccountState,
|
|
1671
|
+
account_filter: *const tb.AccountFilter,
|
|
1672
|
+
result_count: usize,
|
|
1673
|
+
) void {
|
|
1674
|
+
comptime assert(operation == .get_account_transfers or
|
|
1675
|
+
operation == .get_account_balances or
|
|
1676
|
+
operation == .deprecated_get_account_transfers_unbatched or
|
|
1677
|
+
operation == .deprecated_get_account_balances_unbatched);
|
|
1678
|
+
maybe(account_filter.limit == 0);
|
|
1679
|
+
|
|
1680
|
+
const batch_result_max = operation.result_max(self.options.batch_size_limit);
|
|
1681
|
+
const transfer_count = account_state.transfers_count(account_filter.flags);
|
|
1682
|
+
if (account_filter.timestamp_min == 0 and account_filter.timestamp_max == 0) {
|
|
1683
|
+
assert(account_filter.limit <= batch_result_max or
|
|
1684
|
+
account_filter.limit == std.math.maxInt(u32));
|
|
1685
|
+
assert(result_count ==
|
|
1686
|
+
@min(account_filter.limit, batch_result_max, transfer_count));
|
|
1687
|
+
} else {
|
|
1688
|
+
// If timestamp range is set, then the limit is exactly the number of transfer
|
|
1689
|
+
// at the time the filter was generated, but new transfers could have been
|
|
1690
|
+
// inserted since then.
|
|
1691
|
+
assert(account_filter.limit <= transfer_count);
|
|
1692
|
+
assert(account_filter.timestamp_max >= account_filter.timestamp_min);
|
|
1693
|
+
if (account_filter.flags.reversed) {
|
|
1694
|
+
// This filter is only set if there is at least one transfer, so the first
|
|
1695
|
+
// transfer timestamp never changes.
|
|
1696
|
+
assert(account_filter.timestamp_min == account_state.transfer_timestamp_min);
|
|
1697
|
+
// The filter `timestamp_max` was decremented to skip one result.
|
|
1698
|
+
assert(account_filter.timestamp_max < account_state.transfer_timestamp_max);
|
|
1699
|
+
} else {
|
|
1700
|
+
// The filter `timestamp_min` was incremented to skip one result.
|
|
1701
|
+
assert(account_filter.timestamp_min > account_state.transfer_timestamp_min);
|
|
1702
|
+
// New transfers can update `transfer_timestamp_max`.
|
|
1703
|
+
assert(account_filter.timestamp_max <= account_state.transfer_timestamp_max);
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
// Either `transfer_count` is greater than the batch size (so removing a result
|
|
1707
|
+
// doesn't make a difference) or there is exactly one less result that was
|
|
1708
|
+
// excluded by the timestamp filter.
|
|
1709
|
+
assert((result_count == batch_result_max and transfer_count > batch_result_max) or
|
|
1710
|
+
result_count == account_filter.limit - 1);
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
fn on_query(
|
|
1715
|
+
self: *Workload,
|
|
1716
|
+
comptime operation: Operation,
|
|
1717
|
+
timestamp: u64,
|
|
1718
|
+
body: []const tb.QueryFilter,
|
|
1719
|
+
results: []const operation.ResultType(),
|
|
1720
|
+
) void {
|
|
1721
|
+
_ = timestamp;
|
|
1722
|
+
comptime assert(operation == .query_accounts or
|
|
1723
|
+
operation == .query_transfers or
|
|
1724
|
+
operation == .deprecated_query_accounts_unbatched or
|
|
1725
|
+
operation == .deprecated_query_transfers_unbatched);
|
|
1726
|
+
assert(body.len == 1);
|
|
1727
|
+
|
|
1728
|
+
const batch_result_max: u32 = operation.result_max(self.options.batch_size_limit);
|
|
1729
|
+
const filter = &body[0];
|
|
1730
|
+
|
|
1731
|
+
if (filter.ledger != 0) {
|
|
1732
|
+
// No results expected.
|
|
1733
|
+
assert(results.len == 0);
|
|
1734
|
+
return;
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
assert(filter.user_data_64 != 0);
|
|
1738
|
+
assert(filter.user_data_32 != 0);
|
|
1739
|
+
assert(filter.code != 0);
|
|
1740
|
+
assert(filter.user_data_128 == 0);
|
|
1741
|
+
assert(filter.ledger == 0);
|
|
1742
|
+
maybe(filter.limit == 0);
|
|
1743
|
+
maybe(filter.timestamp_min == 0);
|
|
1744
|
+
maybe(filter.timestamp_max == 0);
|
|
1745
|
+
|
|
1746
|
+
const query_intersection_index = filter.code - 1;
|
|
1747
|
+
const query_intersection = self.auditor.query_intersections[query_intersection_index];
|
|
1748
|
+
const state = switch (operation) {
|
|
1749
|
+
.query_accounts,
|
|
1750
|
+
.deprecated_query_accounts_unbatched,
|
|
1751
|
+
=> &query_intersection.accounts,
|
|
1752
|
+
.query_transfers,
|
|
1753
|
+
.deprecated_query_transfers_unbatched,
|
|
1754
|
+
=> &query_intersection.transfers,
|
|
1755
|
+
else => unreachable,
|
|
1756
|
+
};
|
|
1757
|
+
|
|
1758
|
+
assert(results.len <= filter.limit);
|
|
1759
|
+
assert(results.len <= batch_result_max);
|
|
1760
|
+
|
|
1761
|
+
if (filter.timestamp_min > 0 or filter.timestamp_max > 0) {
|
|
1762
|
+
assert(filter.limit <= state.count);
|
|
1763
|
+
assert(filter.timestamp_min > 0);
|
|
1764
|
+
assert(filter.timestamp_max > 0);
|
|
1765
|
+
assert(filter.timestamp_min <= filter.timestamp_max);
|
|
1766
|
+
|
|
1767
|
+
// Filtering by timestamp always exclude one single result.
|
|
1768
|
+
assert(results.len == filter.limit - 1);
|
|
1769
|
+
} else {
|
|
1770
|
+
assert(results.len == @min(
|
|
1771
|
+
filter.limit,
|
|
1772
|
+
batch_result_max,
|
|
1773
|
+
state.count,
|
|
1774
|
+
));
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
var timestamp_previous: u64 = if (filter.flags.reversed)
|
|
1778
|
+
std.math.maxInt(u64)
|
|
1779
|
+
else
|
|
1780
|
+
0;
|
|
1781
|
+
|
|
1782
|
+
for (results) |*result| {
|
|
1783
|
+
if (filter.flags.reversed) {
|
|
1784
|
+
assert(result.timestamp < timestamp_previous);
|
|
1785
|
+
} else {
|
|
1786
|
+
assert(result.timestamp > timestamp_previous);
|
|
1787
|
+
}
|
|
1788
|
+
timestamp_previous = result.timestamp;
|
|
1789
|
+
|
|
1790
|
+
if (filter.timestamp_min > 0) {
|
|
1791
|
+
assert(result.timestamp >= filter.timestamp_min);
|
|
1792
|
+
}
|
|
1793
|
+
if (filter.timestamp_max > 0) {
|
|
1794
|
+
assert(result.timestamp <= filter.timestamp_max);
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
assert(result.user_data_64 == filter.user_data_64);
|
|
1798
|
+
assert(result.user_data_32 == filter.user_data_32);
|
|
1799
|
+
assert(result.code == filter.code);
|
|
1800
|
+
|
|
1801
|
+
if (operation == .query_transfers or
|
|
1802
|
+
operation == .deprecated_query_transfers_unbatched)
|
|
1803
|
+
{
|
|
1804
|
+
validate_transfer_checksum(result);
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
fn on_get_change_events(
|
|
1810
|
+
self: *Workload,
|
|
1811
|
+
timestamp: u64,
|
|
1812
|
+
body: []const tb.ChangeEventsFilter,
|
|
1813
|
+
results: []const tb.ChangeEvent,
|
|
1814
|
+
) void {
|
|
1815
|
+
assert(body.len == 1);
|
|
1816
|
+
self.auditor.on_get_change_events(timestamp, body[0], results);
|
|
1817
|
+
|
|
1818
|
+
for (results) |*result| {
|
|
1819
|
+
assert(stdx.zeroed(&result.reserved));
|
|
1820
|
+
switch (result.type) {
|
|
1821
|
+
.single_phase => {
|
|
1822
|
+
assert(result.timestamp == result.transfer_timestamp);
|
|
1823
|
+
assert(!result.transfer_flags.pending);
|
|
1824
|
+
assert(!result.transfer_flags.post_pending_transfer);
|
|
1825
|
+
assert(!result.transfer_flags.void_pending_transfer);
|
|
1826
|
+
assert(result.transfer_pending_id == 0);
|
|
1827
|
+
assert(result.transfer_amount <= result.debit_account_debits_posted);
|
|
1828
|
+
assert(result.transfer_amount <= result.credit_account_credits_posted);
|
|
1829
|
+
},
|
|
1830
|
+
.two_phase_pending => {
|
|
1831
|
+
assert(result.timestamp == result.transfer_timestamp);
|
|
1832
|
+
assert(result.transfer_flags.pending);
|
|
1833
|
+
assert(!result.transfer_flags.post_pending_transfer);
|
|
1834
|
+
assert(!result.transfer_flags.void_pending_transfer);
|
|
1835
|
+
assert(result.transfer_pending_id == 0);
|
|
1836
|
+
assert(result.transfer_amount <= result.debit_account_debits_pending);
|
|
1837
|
+
assert(result.transfer_amount <= result.credit_account_credits_pending);
|
|
1838
|
+
},
|
|
1839
|
+
.two_phase_posted => {
|
|
1840
|
+
assert(result.timestamp == result.transfer_timestamp);
|
|
1841
|
+
assert(result.transfer_flags.post_pending_transfer);
|
|
1842
|
+
assert(!result.transfer_flags.pending);
|
|
1843
|
+
assert(!result.transfer_flags.void_pending_transfer);
|
|
1844
|
+
assert(result.transfer_pending_id != 0);
|
|
1845
|
+
assert(result.transfer_amount <= result.debit_account_debits_posted);
|
|
1846
|
+
assert(result.transfer_amount <= result.credit_account_credits_posted);
|
|
1847
|
+
},
|
|
1848
|
+
.two_phase_voided => {
|
|
1849
|
+
assert(result.timestamp == result.transfer_timestamp);
|
|
1850
|
+
assert(result.transfer_flags.void_pending_transfer);
|
|
1851
|
+
assert(!result.transfer_flags.pending);
|
|
1852
|
+
assert(!result.transfer_flags.post_pending_transfer);
|
|
1853
|
+
assert(result.transfer_pending_id != 0);
|
|
1854
|
+
},
|
|
1855
|
+
.two_phase_expired => {
|
|
1856
|
+
assert(result.transfer_timeout > 0);
|
|
1857
|
+
const timeout_ns: u64 =
|
|
1858
|
+
@as(u64, result.transfer_timeout) * std.time.ns_per_s;
|
|
1859
|
+
assert(result.timestamp >= result.transfer_timestamp + timeout_ns);
|
|
1860
|
+
assert(result.transfer_flags.pending);
|
|
1861
|
+
assert(!result.transfer_flags.post_pending_transfer);
|
|
1862
|
+
assert(!result.transfer_flags.void_pending_transfer);
|
|
1863
|
+
assert(result.transfer_pending_id == 0);
|
|
1864
|
+
},
|
|
1865
|
+
}
|
|
1866
|
+
assert(result.transfer_flags.closing_debit == result.debit_account_flags.closed);
|
|
1867
|
+
assert(result.transfer_flags.closing_credit == result.credit_account_flags.closed);
|
|
1868
|
+
validate_get_event_checksum(result);
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
/// Verify the transfer's integrity.
|
|
1873
|
+
fn validate_transfer_checksum(transfer: *const tb.Transfer) void {
|
|
1874
|
+
const checksum_actual = transfer.user_data_128;
|
|
1875
|
+
var check = transfer.*;
|
|
1876
|
+
check.user_data_128 = 0;
|
|
1877
|
+
check.timestamp = 0;
|
|
1878
|
+
const checksum_expect = vsr.checksum(std.mem.asBytes(&check));
|
|
1879
|
+
assert(checksum_expect == checksum_actual);
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
fn validate_get_event_checksum(event: *const tb.ChangeEvent) void {
|
|
1883
|
+
const transfer: tb.Transfer = .{
|
|
1884
|
+
.id = event.transfer_id,
|
|
1885
|
+
.debit_account_id = event.debit_account_id,
|
|
1886
|
+
.credit_account_id = event.credit_account_id,
|
|
1887
|
+
.amount = event.transfer_amount,
|
|
1888
|
+
.pending_id = event.transfer_pending_id,
|
|
1889
|
+
.user_data_128 = event.transfer_user_data_128,
|
|
1890
|
+
.user_data_64 = event.transfer_user_data_64,
|
|
1891
|
+
.user_data_32 = event.transfer_user_data_32,
|
|
1892
|
+
.timeout = event.transfer_timeout,
|
|
1893
|
+
.ledger = event.ledger,
|
|
1894
|
+
.code = event.transfer_code,
|
|
1895
|
+
.flags = event.transfer_flags,
|
|
1896
|
+
.timestamp = event.timestamp,
|
|
1897
|
+
};
|
|
1898
|
+
validate_transfer_checksum(&transfer);
|
|
1899
|
+
}
|
|
1900
|
+
};
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
fn OptionsType(
|
|
1904
|
+
comptime AccountingStateMachine: type,
|
|
1905
|
+
comptime Action: type,
|
|
1906
|
+
comptime Lookup: type,
|
|
1907
|
+
) type {
|
|
1908
|
+
return struct {
|
|
1909
|
+
batch_size_limit: u32,
|
|
1910
|
+
multi_batch_per_request_limit: u32,
|
|
1911
|
+
|
|
1912
|
+
auditor_options: Auditor.Options,
|
|
1913
|
+
transfer_id_permutation: IdPermutation,
|
|
1914
|
+
|
|
1915
|
+
operations: stdx.PRNG.EnumWeightsType(Action),
|
|
1916
|
+
|
|
1917
|
+
create_account_invalid_probability: Ratio,
|
|
1918
|
+
create_transfer_invalid_probability: Ratio,
|
|
1919
|
+
create_transfer_limit_probability: Ratio,
|
|
1920
|
+
create_transfer_pending_probability: Ratio,
|
|
1921
|
+
create_transfer_post_probability: Ratio,
|
|
1922
|
+
create_transfer_void_probability: Ratio,
|
|
1923
|
+
create_transfer_retry_probability: Ratio,
|
|
1924
|
+
lookup_account_invalid_probability: Ratio,
|
|
1925
|
+
|
|
1926
|
+
account_filter_invalid_account_probability: Ratio,
|
|
1927
|
+
account_filter_timestamp_range_probability: Ratio,
|
|
1928
|
+
|
|
1929
|
+
query_filter_not_found_probability: Ratio,
|
|
1930
|
+
query_filter_timestamp_range_probability: Ratio,
|
|
1931
|
+
lookup_transfer: stdx.PRNG.EnumWeightsType(Lookup),
|
|
1932
|
+
|
|
1933
|
+
// Size of timespan for querying, measured in transfers
|
|
1934
|
+
lookup_transfer_span_mean: usize,
|
|
1935
|
+
|
|
1936
|
+
account_limit_probability: Ratio,
|
|
1937
|
+
account_history_probability: Ratio,
|
|
1938
|
+
|
|
1939
|
+
/// This probability is only checked for consecutive guaranteed-successful transfers.
|
|
1940
|
+
linked_valid_probability: Ratio,
|
|
1941
|
+
/// This probability is only checked for consecutive invalid transfers.
|
|
1942
|
+
linked_invalid_probability: Ratio,
|
|
1943
|
+
|
|
1944
|
+
pending_timeout_mean: u32,
|
|
1945
|
+
|
|
1946
|
+
accounts_batch_size_min: usize,
|
|
1947
|
+
accounts_batch_size_span: usize, // inclusive
|
|
1948
|
+
transfers_batch_size_min: usize,
|
|
1949
|
+
transfers_batch_size_span: usize, // inclusive
|
|
1950
|
+
|
|
1951
|
+
/// Maximum number of failed transfer IDs to keep in the retry list.
|
|
1952
|
+
transfers_retry_failed_max: usize,
|
|
1953
|
+
|
|
1954
|
+
/// Maximum number of successfully completed transfers to keep in the retry list.
|
|
1955
|
+
transfers_retry_exists_max: usize,
|
|
1956
|
+
|
|
1957
|
+
const Options = @This();
|
|
1958
|
+
const Operation = AccountingStateMachine.Operation;
|
|
1959
|
+
|
|
1960
|
+
pub fn generate(prng: *stdx.PRNG, options: struct {
|
|
1961
|
+
batch_size_limit: u32,
|
|
1962
|
+
multi_batch_per_request_limit: u32,
|
|
1963
|
+
client_count: usize,
|
|
1964
|
+
in_flight_max: usize,
|
|
1965
|
+
}) Options {
|
|
1966
|
+
assert(
|
|
1967
|
+
options.batch_size_limit <= constants.message_body_size_max,
|
|
1968
|
+
);
|
|
1969
|
+
|
|
1970
|
+
const batch_create_accounts_limit = @min(
|
|
1971
|
+
Operation.create_accounts.event_max(options.batch_size_limit),
|
|
1972
|
+
Operation.deprecated_create_accounts_unbatched.event_max(options.batch_size_limit),
|
|
1973
|
+
);
|
|
1974
|
+
assert(batch_create_accounts_limit > 0);
|
|
1975
|
+
assert(batch_create_accounts_limit <=
|
|
1976
|
+
AccountingStateMachine.batch_max.create_accounts);
|
|
1977
|
+
|
|
1978
|
+
const batch_create_transfers_limit = @min(
|
|
1979
|
+
Operation.create_transfers.event_max(options.batch_size_limit),
|
|
1980
|
+
Operation.deprecated_create_transfers_unbatched.event_max(
|
|
1981
|
+
options.batch_size_limit,
|
|
1982
|
+
),
|
|
1983
|
+
);
|
|
1984
|
+
assert(batch_create_transfers_limit > 0);
|
|
1985
|
+
assert(batch_create_transfers_limit <=
|
|
1986
|
+
AccountingStateMachine.batch_max.create_transfers);
|
|
1987
|
+
return .{
|
|
1988
|
+
.batch_size_limit = options.batch_size_limit,
|
|
1989
|
+
.multi_batch_per_request_limit = options.multi_batch_per_request_limit,
|
|
1990
|
+
.auditor_options = .{
|
|
1991
|
+
.accounts_max = prng.range_inclusive(usize, 2, 128),
|
|
1992
|
+
.account_id_permutation = IdPermutation.generate(prng),
|
|
1993
|
+
.client_count = options.client_count,
|
|
1994
|
+
.transfers_pending_max = 256,
|
|
1995
|
+
.changes_events_max = Operation
|
|
1996
|
+
.get_change_events.event_max(options.batch_size_limit),
|
|
1997
|
+
.in_flight_max = options.in_flight_max,
|
|
1998
|
+
.pulse_expiries_max = @max(
|
|
1999
|
+
Operation.create_transfers.event_max(options.batch_size_limit),
|
|
2000
|
+
Operation.deprecated_create_transfers_unbatched.event_max(
|
|
2001
|
+
options.batch_size_limit,
|
|
2002
|
+
),
|
|
2003
|
+
),
|
|
2004
|
+
},
|
|
2005
|
+
.transfer_id_permutation = IdPermutation.generate(prng),
|
|
2006
|
+
.operations = .{
|
|
2007
|
+
.create_accounts = prng.range_inclusive(u64, 1, 10),
|
|
2008
|
+
.create_transfers = prng.range_inclusive(u64, 1, 100),
|
|
2009
|
+
.lookup_accounts = prng.range_inclusive(u64, 1, 20),
|
|
2010
|
+
.lookup_transfers = prng.range_inclusive(u64, 1, 20),
|
|
2011
|
+
.get_account_transfers = prng.range_inclusive(u64, 1, 20),
|
|
2012
|
+
.get_account_balances = prng.range_inclusive(u64, 1, 20),
|
|
2013
|
+
.query_accounts = prng.range_inclusive(u64, 1, 20),
|
|
2014
|
+
.query_transfers = prng.range_inclusive(u64, 1, 20),
|
|
2015
|
+
.get_change_events = prng.range_inclusive(u64, 1, 20),
|
|
2016
|
+
|
|
2017
|
+
.deprecated_create_accounts_unbatched = prng.range_inclusive(u64, 1, 10),
|
|
2018
|
+
.deprecated_create_transfers_unbatched = prng.range_inclusive(u64, 1, 100),
|
|
2019
|
+
.deprecated_lookup_accounts_unbatched = prng.range_inclusive(u64, 1, 20),
|
|
2020
|
+
.deprecated_lookup_transfers_unbatched = prng.range_inclusive(u64, 1, 20),
|
|
2021
|
+
.deprecated_get_account_transfers_unbatched = prng.range_inclusive(u64, 1, 20),
|
|
2022
|
+
.deprecated_get_account_balances_unbatched = prng.range_inclusive(u64, 1, 20),
|
|
2023
|
+
.deprecated_query_accounts_unbatched = prng.range_inclusive(u64, 1, 20),
|
|
2024
|
+
.deprecated_query_transfers_unbatched = prng.range_inclusive(u64, 1, 20),
|
|
2025
|
+
},
|
|
2026
|
+
.create_account_invalid_probability = ratio(1, 100),
|
|
2027
|
+
.create_transfer_invalid_probability = ratio(1, 100),
|
|
2028
|
+
.create_transfer_limit_probability = ratio(prng.int_inclusive(u8, 100), 100),
|
|
2029
|
+
.create_transfer_pending_probability = ratio(prng.range_inclusive(u8, 1, 100), 100),
|
|
2030
|
+
.create_transfer_post_probability = ratio(prng.range_inclusive(u8, 1, 50), 100),
|
|
2031
|
+
.create_transfer_void_probability = ratio(prng.range_inclusive(u8, 1, 50), 100),
|
|
2032
|
+
.create_transfer_retry_probability = ratio(prng.range_inclusive(u8, 1, 10), 100),
|
|
2033
|
+
.lookup_account_invalid_probability = ratio(1, 100),
|
|
2034
|
+
|
|
2035
|
+
.account_filter_invalid_account_probability = ratio(
|
|
2036
|
+
prng.range_inclusive(u8, 1, 20),
|
|
2037
|
+
100,
|
|
2038
|
+
),
|
|
2039
|
+
.account_filter_timestamp_range_probability = ratio(
|
|
2040
|
+
prng.range_inclusive(u8, 1, 80),
|
|
2041
|
+
100,
|
|
2042
|
+
),
|
|
2043
|
+
|
|
2044
|
+
.query_filter_not_found_probability = ratio(prng.range_inclusive(u8, 1, 20), 100),
|
|
2045
|
+
.query_filter_timestamp_range_probability = ratio(
|
|
2046
|
+
prng.range_inclusive(u8, 1, 80),
|
|
2047
|
+
100,
|
|
2048
|
+
),
|
|
2049
|
+
|
|
2050
|
+
.lookup_transfer = .{
|
|
2051
|
+
.delivered = prng.range_inclusive(u64, 1, 10),
|
|
2052
|
+
.sending = prng.range_inclusive(u64, 1, 10),
|
|
2053
|
+
},
|
|
2054
|
+
.lookup_transfer_span_mean = prng.range_inclusive(usize, 10, 1000),
|
|
2055
|
+
.account_limit_probability = ratio(prng.int_inclusive(u8, 80), 100),
|
|
2056
|
+
.account_history_probability = ratio(prng.int_inclusive(u8, 80), 100),
|
|
2057
|
+
.linked_valid_probability = ratio(prng.int_inclusive(u8, 100), 100),
|
|
2058
|
+
// 100% chance: this only applies to consecutive invalid transfers, which are rare.
|
|
2059
|
+
.linked_invalid_probability = ratio(100, 100),
|
|
2060
|
+
// One second.
|
|
2061
|
+
.pending_timeout_mean = 1,
|
|
2062
|
+
.accounts_batch_size_min = 0,
|
|
2063
|
+
.accounts_batch_size_span = prng.range_inclusive(
|
|
2064
|
+
usize,
|
|
2065
|
+
1,
|
|
2066
|
+
batch_create_accounts_limit,
|
|
2067
|
+
),
|
|
2068
|
+
.transfers_batch_size_min = 0,
|
|
2069
|
+
.transfers_batch_size_span = prng.range_inclusive(
|
|
2070
|
+
usize,
|
|
2071
|
+
1,
|
|
2072
|
+
batch_create_transfers_limit,
|
|
2073
|
+
),
|
|
2074
|
+
.transfers_retry_failed_max = 128,
|
|
2075
|
+
.transfers_retry_exists_max = 128,
|
|
2076
|
+
};
|
|
2077
|
+
}
|
|
2078
|
+
};
|
|
2079
|
+
}
|