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.
Files changed (249) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +10 -0
  3. data/ext/tb_client/extconf.rb +13 -13
  4. data/ext/tb_client/tigerbeetle/LICENSE +177 -0
  5. data/ext/tb_client/tigerbeetle/build.zig +2327 -0
  6. data/ext/tb_client/tigerbeetle/src/aof.zig +1000 -0
  7. data/ext/tb_client/tigerbeetle/src/build_multiversion.zig +808 -0
  8. data/ext/tb_client/tigerbeetle/src/cdc/amqp/protocol.zig +1283 -0
  9. data/ext/tb_client/tigerbeetle/src/cdc/amqp/spec.zig +1704 -0
  10. data/ext/tb_client/tigerbeetle/src/cdc/amqp/types.zig +341 -0
  11. data/ext/tb_client/tigerbeetle/src/cdc/amqp.zig +1450 -0
  12. data/ext/tb_client/tigerbeetle/src/cdc/runner.zig +1659 -0
  13. data/ext/tb_client/tigerbeetle/src/clients/c/samples/main.c +406 -0
  14. data/ext/tb_client/tigerbeetle/src/clients/c/tb_client/context.zig +1084 -0
  15. data/ext/tb_client/tigerbeetle/src/clients/c/tb_client/echo_client.zig +286 -0
  16. data/ext/tb_client/tigerbeetle/src/clients/c/tb_client/packet.zig +158 -0
  17. data/ext/tb_client/tigerbeetle/src/clients/c/tb_client/signal.zig +229 -0
  18. data/ext/tb_client/tigerbeetle/src/clients/c/tb_client/signal_fuzz.zig +110 -0
  19. data/ext/tb_client/tigerbeetle/src/clients/c/tb_client.h +386 -0
  20. data/ext/tb_client/tigerbeetle/src/clients/c/tb_client.zig +34 -0
  21. data/ext/tb_client/tigerbeetle/src/clients/c/tb_client_exports.zig +281 -0
  22. data/ext/tb_client/tigerbeetle/src/clients/c/tb_client_header.zig +312 -0
  23. data/ext/tb_client/tigerbeetle/src/clients/c/tb_client_header_test.zig +138 -0
  24. data/ext/tb_client/tigerbeetle/src/clients/c/test.zig +466 -0
  25. data/ext/tb_client/tigerbeetle/src/clients/docs_samples.zig +157 -0
  26. data/ext/tb_client/tigerbeetle/src/clients/docs_types.zig +90 -0
  27. data/ext/tb_client/tigerbeetle/src/clients/dotnet/ci.zig +203 -0
  28. data/ext/tb_client/tigerbeetle/src/clients/dotnet/docs.zig +79 -0
  29. data/ext/tb_client/tigerbeetle/src/clients/dotnet/dotnet_bindings.zig +542 -0
  30. data/ext/tb_client/tigerbeetle/src/clients/go/ci.zig +109 -0
  31. data/ext/tb_client/tigerbeetle/src/clients/go/docs.zig +86 -0
  32. data/ext/tb_client/tigerbeetle/src/clients/go/go_bindings.zig +370 -0
  33. data/ext/tb_client/tigerbeetle/src/clients/go/pkg/native/tb_client.h +386 -0
  34. data/ext/tb_client/tigerbeetle/src/clients/java/ci.zig +167 -0
  35. data/ext/tb_client/tigerbeetle/src/clients/java/docs.zig +126 -0
  36. data/ext/tb_client/tigerbeetle/src/clients/java/java_bindings.zig +996 -0
  37. data/ext/tb_client/tigerbeetle/src/clients/java/src/client.zig +748 -0
  38. data/ext/tb_client/tigerbeetle/src/clients/java/src/jni.zig +3238 -0
  39. data/ext/tb_client/tigerbeetle/src/clients/java/src/jni_tests.zig +1718 -0
  40. data/ext/tb_client/tigerbeetle/src/clients/java/src/jni_thread_cleaner.zig +190 -0
  41. data/ext/tb_client/tigerbeetle/src/clients/node/ci.zig +104 -0
  42. data/ext/tb_client/tigerbeetle/src/clients/node/docs.zig +75 -0
  43. data/ext/tb_client/tigerbeetle/src/clients/node/node.zig +522 -0
  44. data/ext/tb_client/tigerbeetle/src/clients/node/node_bindings.zig +267 -0
  45. data/ext/tb_client/tigerbeetle/src/clients/node/src/c.zig +3 -0
  46. data/ext/tb_client/tigerbeetle/src/clients/node/src/translate.zig +379 -0
  47. data/ext/tb_client/tigerbeetle/src/clients/python/ci.zig +131 -0
  48. data/ext/tb_client/tigerbeetle/src/clients/python/docs.zig +63 -0
  49. data/ext/tb_client/tigerbeetle/src/clients/python/python_bindings.zig +588 -0
  50. data/ext/tb_client/tigerbeetle/src/clients/rust/assets/tb_client.h +386 -0
  51. data/ext/tb_client/tigerbeetle/src/clients/rust/ci.zig +73 -0
  52. data/ext/tb_client/tigerbeetle/src/clients/rust/docs.zig +106 -0
  53. data/ext/tb_client/tigerbeetle/src/clients/rust/rust_bindings.zig +305 -0
  54. data/ext/tb_client/tigerbeetle/src/config.zig +296 -0
  55. data/ext/tb_client/tigerbeetle/src/constants.zig +790 -0
  56. data/ext/tb_client/tigerbeetle/src/copyhound.zig +202 -0
  57. data/ext/tb_client/tigerbeetle/src/counting_allocator.zig +72 -0
  58. data/ext/tb_client/tigerbeetle/src/direction.zig +11 -0
  59. data/ext/tb_client/tigerbeetle/src/docs_website/build.zig +158 -0
  60. data/ext/tb_client/tigerbeetle/src/docs_website/src/content.zig +156 -0
  61. data/ext/tb_client/tigerbeetle/src/docs_website/src/docs.zig +252 -0
  62. data/ext/tb_client/tigerbeetle/src/docs_website/src/file_checker.zig +313 -0
  63. data/ext/tb_client/tigerbeetle/src/docs_website/src/html.zig +87 -0
  64. data/ext/tb_client/tigerbeetle/src/docs_website/src/page_writer.zig +63 -0
  65. data/ext/tb_client/tigerbeetle/src/docs_website/src/redirects.zig +47 -0
  66. data/ext/tb_client/tigerbeetle/src/docs_website/src/search_index_writer.zig +28 -0
  67. data/ext/tb_client/tigerbeetle/src/docs_website/src/service_worker_writer.zig +61 -0
  68. data/ext/tb_client/tigerbeetle/src/docs_website/src/single_page_writer.zig +169 -0
  69. data/ext/tb_client/tigerbeetle/src/docs_website/src/website.zig +46 -0
  70. data/ext/tb_client/tigerbeetle/src/ewah.zig +445 -0
  71. data/ext/tb_client/tigerbeetle/src/ewah_benchmark.zig +128 -0
  72. data/ext/tb_client/tigerbeetle/src/ewah_fuzz.zig +171 -0
  73. data/ext/tb_client/tigerbeetle/src/fuzz_tests.zig +179 -0
  74. data/ext/tb_client/tigerbeetle/src/integration_tests.zig +662 -0
  75. data/ext/tb_client/tigerbeetle/src/io/common.zig +155 -0
  76. data/ext/tb_client/tigerbeetle/src/io/darwin.zig +1093 -0
  77. data/ext/tb_client/tigerbeetle/src/io/linux.zig +1880 -0
  78. data/ext/tb_client/tigerbeetle/src/io/test.zig +1005 -0
  79. data/ext/tb_client/tigerbeetle/src/io/windows.zig +1598 -0
  80. data/ext/tb_client/tigerbeetle/src/io.zig +34 -0
  81. data/ext/tb_client/tigerbeetle/src/iops.zig +134 -0
  82. data/ext/tb_client/tigerbeetle/src/list.zig +236 -0
  83. data/ext/tb_client/tigerbeetle/src/lsm/binary_search.zig +848 -0
  84. data/ext/tb_client/tigerbeetle/src/lsm/binary_search_benchmark.zig +179 -0
  85. data/ext/tb_client/tigerbeetle/src/lsm/cache_map.zig +424 -0
  86. data/ext/tb_client/tigerbeetle/src/lsm/cache_map_fuzz.zig +420 -0
  87. data/ext/tb_client/tigerbeetle/src/lsm/compaction.zig +2117 -0
  88. data/ext/tb_client/tigerbeetle/src/lsm/composite_key.zig +182 -0
  89. data/ext/tb_client/tigerbeetle/src/lsm/forest.zig +1119 -0
  90. data/ext/tb_client/tigerbeetle/src/lsm/forest_fuzz.zig +1102 -0
  91. data/ext/tb_client/tigerbeetle/src/lsm/forest_table_iterator.zig +200 -0
  92. data/ext/tb_client/tigerbeetle/src/lsm/groove.zig +1495 -0
  93. data/ext/tb_client/tigerbeetle/src/lsm/k_way_merge.zig +739 -0
  94. data/ext/tb_client/tigerbeetle/src/lsm/k_way_merge_benchmark.zig +166 -0
  95. data/ext/tb_client/tigerbeetle/src/lsm/manifest.zig +754 -0
  96. data/ext/tb_client/tigerbeetle/src/lsm/manifest_level.zig +1294 -0
  97. data/ext/tb_client/tigerbeetle/src/lsm/manifest_level_fuzz.zig +510 -0
  98. data/ext/tb_client/tigerbeetle/src/lsm/manifest_log.zig +1263 -0
  99. data/ext/tb_client/tigerbeetle/src/lsm/manifest_log_fuzz.zig +628 -0
  100. data/ext/tb_client/tigerbeetle/src/lsm/node_pool.zig +247 -0
  101. data/ext/tb_client/tigerbeetle/src/lsm/scan_buffer.zig +116 -0
  102. data/ext/tb_client/tigerbeetle/src/lsm/scan_builder.zig +543 -0
  103. data/ext/tb_client/tigerbeetle/src/lsm/scan_fuzz.zig +938 -0
  104. data/ext/tb_client/tigerbeetle/src/lsm/scan_lookup.zig +293 -0
  105. data/ext/tb_client/tigerbeetle/src/lsm/scan_merge.zig +362 -0
  106. data/ext/tb_client/tigerbeetle/src/lsm/scan_range.zig +99 -0
  107. data/ext/tb_client/tigerbeetle/src/lsm/scan_state.zig +17 -0
  108. data/ext/tb_client/tigerbeetle/src/lsm/scan_tree.zig +1036 -0
  109. data/ext/tb_client/tigerbeetle/src/lsm/schema.zig +617 -0
  110. data/ext/tb_client/tigerbeetle/src/lsm/scratch_memory.zig +84 -0
  111. data/ext/tb_client/tigerbeetle/src/lsm/segmented_array.zig +1500 -0
  112. data/ext/tb_client/tigerbeetle/src/lsm/segmented_array_benchmark.zig +149 -0
  113. data/ext/tb_client/tigerbeetle/src/lsm/segmented_array_fuzz.zig +7 -0
  114. data/ext/tb_client/tigerbeetle/src/lsm/set_associative_cache.zig +865 -0
  115. data/ext/tb_client/tigerbeetle/src/lsm/table.zig +607 -0
  116. data/ext/tb_client/tigerbeetle/src/lsm/table_memory.zig +843 -0
  117. data/ext/tb_client/tigerbeetle/src/lsm/table_value_iterator.zig +105 -0
  118. data/ext/tb_client/tigerbeetle/src/lsm/timestamp_range.zig +40 -0
  119. data/ext/tb_client/tigerbeetle/src/lsm/tree.zig +630 -0
  120. data/ext/tb_client/tigerbeetle/src/lsm/tree_fuzz.zig +933 -0
  121. data/ext/tb_client/tigerbeetle/src/lsm/zig_zag_merge.zig +557 -0
  122. data/ext/tb_client/tigerbeetle/src/message_buffer.zig +469 -0
  123. data/ext/tb_client/tigerbeetle/src/message_bus.zig +1214 -0
  124. data/ext/tb_client/tigerbeetle/src/message_bus_fuzz.zig +936 -0
  125. data/ext/tb_client/tigerbeetle/src/message_pool.zig +343 -0
  126. data/ext/tb_client/tigerbeetle/src/multiversion.zig +2195 -0
  127. data/ext/tb_client/tigerbeetle/src/queue.zig +390 -0
  128. data/ext/tb_client/tigerbeetle/src/repl/completion.zig +201 -0
  129. data/ext/tb_client/tigerbeetle/src/repl/parser.zig +1356 -0
  130. data/ext/tb_client/tigerbeetle/src/repl/terminal.zig +496 -0
  131. data/ext/tb_client/tigerbeetle/src/repl.zig +1034 -0
  132. data/ext/tb_client/tigerbeetle/src/scripts/amqp.zig +973 -0
  133. data/ext/tb_client/tigerbeetle/src/scripts/cfo.zig +1866 -0
  134. data/ext/tb_client/tigerbeetle/src/scripts/changelog.zig +304 -0
  135. data/ext/tb_client/tigerbeetle/src/scripts/ci.zig +227 -0
  136. data/ext/tb_client/tigerbeetle/src/scripts/client_readmes.zig +658 -0
  137. data/ext/tb_client/tigerbeetle/src/scripts/devhub.zig +466 -0
  138. data/ext/tb_client/tigerbeetle/src/scripts/release.zig +1058 -0
  139. data/ext/tb_client/tigerbeetle/src/scripts.zig +105 -0
  140. data/ext/tb_client/tigerbeetle/src/shell.zig +1195 -0
  141. data/ext/tb_client/tigerbeetle/src/stack.zig +260 -0
  142. data/ext/tb_client/tigerbeetle/src/state_machine/auditor.zig +911 -0
  143. data/ext/tb_client/tigerbeetle/src/state_machine/workload.zig +2079 -0
  144. data/ext/tb_client/tigerbeetle/src/state_machine.zig +4872 -0
  145. data/ext/tb_client/tigerbeetle/src/state_machine_fuzz.zig +288 -0
  146. data/ext/tb_client/tigerbeetle/src/state_machine_tests.zig +3128 -0
  147. data/ext/tb_client/tigerbeetle/src/static_allocator.zig +82 -0
  148. data/ext/tb_client/tigerbeetle/src/stdx/bit_set.zig +157 -0
  149. data/ext/tb_client/tigerbeetle/src/stdx/bounded_array.zig +292 -0
  150. data/ext/tb_client/tigerbeetle/src/stdx/debug.zig +65 -0
  151. data/ext/tb_client/tigerbeetle/src/stdx/flags.zig +1414 -0
  152. data/ext/tb_client/tigerbeetle/src/stdx/mlock.zig +92 -0
  153. data/ext/tb_client/tigerbeetle/src/stdx/prng.zig +677 -0
  154. data/ext/tb_client/tigerbeetle/src/stdx/radix.zig +336 -0
  155. data/ext/tb_client/tigerbeetle/src/stdx/ring_buffer.zig +511 -0
  156. data/ext/tb_client/tigerbeetle/src/stdx/sort_test.zig +112 -0
  157. data/ext/tb_client/tigerbeetle/src/stdx/stdx.zig +1160 -0
  158. data/ext/tb_client/tigerbeetle/src/stdx/testing/low_level_hash_vectors.zig +142 -0
  159. data/ext/tb_client/tigerbeetle/src/stdx/testing/snaptest.zig +361 -0
  160. data/ext/tb_client/tigerbeetle/src/stdx/time_units.zig +275 -0
  161. data/ext/tb_client/tigerbeetle/src/stdx/unshare.zig +295 -0
  162. data/ext/tb_client/tigerbeetle/src/stdx/vendored/aegis.zig +436 -0
  163. data/ext/tb_client/tigerbeetle/src/stdx/windows.zig +48 -0
  164. data/ext/tb_client/tigerbeetle/src/stdx/zipfian.zig +402 -0
  165. data/ext/tb_client/tigerbeetle/src/storage.zig +489 -0
  166. data/ext/tb_client/tigerbeetle/src/storage_fuzz.zig +180 -0
  167. data/ext/tb_client/tigerbeetle/src/testing/bench.zig +146 -0
  168. data/ext/tb_client/tigerbeetle/src/testing/cluster/grid_checker.zig +53 -0
  169. data/ext/tb_client/tigerbeetle/src/testing/cluster/journal_checker.zig +61 -0
  170. data/ext/tb_client/tigerbeetle/src/testing/cluster/manifest_checker.zig +76 -0
  171. data/ext/tb_client/tigerbeetle/src/testing/cluster/message_bus.zig +110 -0
  172. data/ext/tb_client/tigerbeetle/src/testing/cluster/network.zig +412 -0
  173. data/ext/tb_client/tigerbeetle/src/testing/cluster/state_checker.zig +331 -0
  174. data/ext/tb_client/tigerbeetle/src/testing/cluster/storage_checker.zig +458 -0
  175. data/ext/tb_client/tigerbeetle/src/testing/cluster.zig +1198 -0
  176. data/ext/tb_client/tigerbeetle/src/testing/exhaustigen.zig +128 -0
  177. data/ext/tb_client/tigerbeetle/src/testing/fixtures.zig +181 -0
  178. data/ext/tb_client/tigerbeetle/src/testing/fuzz.zig +144 -0
  179. data/ext/tb_client/tigerbeetle/src/testing/id.zig +97 -0
  180. data/ext/tb_client/tigerbeetle/src/testing/io.zig +317 -0
  181. data/ext/tb_client/tigerbeetle/src/testing/marks.zig +126 -0
  182. data/ext/tb_client/tigerbeetle/src/testing/packet_simulator.zig +533 -0
  183. data/ext/tb_client/tigerbeetle/src/testing/reply_sequence.zig +154 -0
  184. data/ext/tb_client/tigerbeetle/src/testing/state_machine.zig +389 -0
  185. data/ext/tb_client/tigerbeetle/src/testing/storage.zig +1247 -0
  186. data/ext/tb_client/tigerbeetle/src/testing/table.zig +249 -0
  187. data/ext/tb_client/tigerbeetle/src/testing/time.zig +98 -0
  188. data/ext/tb_client/tigerbeetle/src/testing/tmp_tigerbeetle.zig +212 -0
  189. data/ext/tb_client/tigerbeetle/src/testing/vortex/constants.zig +26 -0
  190. data/ext/tb_client/tigerbeetle/src/testing/vortex/faulty_network.zig +580 -0
  191. data/ext/tb_client/tigerbeetle/src/testing/vortex/java_driver/ci.zig +39 -0
  192. data/ext/tb_client/tigerbeetle/src/testing/vortex/logged_process.zig +214 -0
  193. data/ext/tb_client/tigerbeetle/src/testing/vortex/rust_driver/ci.zig +34 -0
  194. data/ext/tb_client/tigerbeetle/src/testing/vortex/supervisor.zig +766 -0
  195. data/ext/tb_client/tigerbeetle/src/testing/vortex/workload.zig +543 -0
  196. data/ext/tb_client/tigerbeetle/src/testing/vortex/zig_driver.zig +181 -0
  197. data/ext/tb_client/tigerbeetle/src/tidy.zig +1448 -0
  198. data/ext/tb_client/tigerbeetle/src/tigerbeetle/benchmark_driver.zig +227 -0
  199. data/ext/tb_client/tigerbeetle/src/tigerbeetle/benchmark_load.zig +1069 -0
  200. data/ext/tb_client/tigerbeetle/src/tigerbeetle/cli.zig +1422 -0
  201. data/ext/tb_client/tigerbeetle/src/tigerbeetle/inspect.zig +1658 -0
  202. data/ext/tb_client/tigerbeetle/src/tigerbeetle/inspect_integrity.zig +518 -0
  203. data/ext/tb_client/tigerbeetle/src/tigerbeetle/libtb_client.zig +36 -0
  204. data/ext/tb_client/tigerbeetle/src/tigerbeetle/main.zig +646 -0
  205. data/ext/tb_client/tigerbeetle/src/tigerbeetle.zig +958 -0
  206. data/ext/tb_client/tigerbeetle/src/time.zig +236 -0
  207. data/ext/tb_client/tigerbeetle/src/trace/event.zig +745 -0
  208. data/ext/tb_client/tigerbeetle/src/trace/statsd.zig +462 -0
  209. data/ext/tb_client/tigerbeetle/src/trace.zig +556 -0
  210. data/ext/tb_client/tigerbeetle/src/unit_tests.zig +321 -0
  211. data/ext/tb_client/tigerbeetle/src/vopr.zig +1785 -0
  212. data/ext/tb_client/tigerbeetle/src/vortex.zig +101 -0
  213. data/ext/tb_client/tigerbeetle/src/vsr/checkpoint_trailer.zig +473 -0
  214. data/ext/tb_client/tigerbeetle/src/vsr/checksum.zig +208 -0
  215. data/ext/tb_client/tigerbeetle/src/vsr/checksum_benchmark.zig +43 -0
  216. data/ext/tb_client/tigerbeetle/src/vsr/client.zig +768 -0
  217. data/ext/tb_client/tigerbeetle/src/vsr/client_replies.zig +532 -0
  218. data/ext/tb_client/tigerbeetle/src/vsr/client_sessions.zig +338 -0
  219. data/ext/tb_client/tigerbeetle/src/vsr/clock.zig +1019 -0
  220. data/ext/tb_client/tigerbeetle/src/vsr/fault_detector.zig +279 -0
  221. data/ext/tb_client/tigerbeetle/src/vsr/free_set.zig +1381 -0
  222. data/ext/tb_client/tigerbeetle/src/vsr/free_set_fuzz.zig +315 -0
  223. data/ext/tb_client/tigerbeetle/src/vsr/grid.zig +1460 -0
  224. data/ext/tb_client/tigerbeetle/src/vsr/grid_blocks_missing.zig +757 -0
  225. data/ext/tb_client/tigerbeetle/src/vsr/grid_scrubber.zig +797 -0
  226. data/ext/tb_client/tigerbeetle/src/vsr/journal.zig +2586 -0
  227. data/ext/tb_client/tigerbeetle/src/vsr/marzullo.zig +308 -0
  228. data/ext/tb_client/tigerbeetle/src/vsr/message_header.zig +1777 -0
  229. data/ext/tb_client/tigerbeetle/src/vsr/multi_batch.zig +715 -0
  230. data/ext/tb_client/tigerbeetle/src/vsr/multi_batch_fuzz.zig +185 -0
  231. data/ext/tb_client/tigerbeetle/src/vsr/repair_budget.zig +333 -0
  232. data/ext/tb_client/tigerbeetle/src/vsr/replica.zig +12355 -0
  233. data/ext/tb_client/tigerbeetle/src/vsr/replica_format.zig +416 -0
  234. data/ext/tb_client/tigerbeetle/src/vsr/replica_reformat.zig +165 -0
  235. data/ext/tb_client/tigerbeetle/src/vsr/replica_test.zig +2910 -0
  236. data/ext/tb_client/tigerbeetle/src/vsr/routing.zig +1075 -0
  237. data/ext/tb_client/tigerbeetle/src/vsr/superblock.zig +1603 -0
  238. data/ext/tb_client/tigerbeetle/src/vsr/superblock_fuzz.zig +484 -0
  239. data/ext/tb_client/tigerbeetle/src/vsr/superblock_quorums.zig +405 -0
  240. data/ext/tb_client/tigerbeetle/src/vsr/superblock_quorums_fuzz.zig +355 -0
  241. data/ext/tb_client/tigerbeetle/src/vsr/sync.zig +29 -0
  242. data/ext/tb_client/tigerbeetle/src/vsr.zig +1727 -0
  243. data/lib/tb_client/shared_lib.rb +12 -5
  244. data/lib/tigerbeetle/client.rb +1 -1
  245. data/lib/tigerbeetle/platforms.rb +9 -0
  246. data/lib/tigerbeetle/version.rb +2 -2
  247. data/tigerbeetle.gemspec +22 -5
  248. metadata +242 -3
  249. 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
+ }